68 lines
11 KiB
JavaScript
68 lines
11 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
var root = document.getElementById('app');
|
|
var apiBase = (window.PK_CONFIG.apiBase || '').replace(/\/$/, '');
|
|
var state = { tests: [], test: null, bracket: null, result: null, template: 'celebration', sticker: true };
|
|
|
|
function toast(message) { var el = document.getElementById('toast'); el.textContent = message; el.classList.add('show'); setTimeout(function () { el.classList.remove('show'); }, 2200); }
|
|
function escapeHtml(value) { return String(value).replace(/[&<>"']/g, function (char) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[char]; }); }
|
|
function request(path, options) {
|
|
options = options || {}; var token = localStorage.getItem('pk_token');
|
|
var headers = {'Content-Type':'application/json'}; if (token) headers.Authorization = 'Bearer ' + token;
|
|
return fetch(apiBase + path, {method:options.method || 'GET',headers:headers,body:options.data ? JSON.stringify(options.data) : undefined}).then(function (response) {
|
|
return response.json().catch(function () { return {}; }).then(function (data) { if (!response.ok) throw new Error(data.detail || '请求失败'); return data; });
|
|
});
|
|
}
|
|
function ensureLogin() {
|
|
if (localStorage.getItem('pk_token')) return Promise.resolve();
|
|
var id = localStorage.getItem('pk_preview_id') || (Date.now().toString(36) + Math.random().toString(36).slice(2)); localStorage.setItem('pk_preview_id', id);
|
|
return request('/api/v1/auth/xhs', {method:'POST',data:{app_key:'hub',code:'dev:web-' + id}}).then(function (data) { localStorage.setItem('pk_token', data.token); });
|
|
}
|
|
function loadHome() {
|
|
root.innerHTML = '<div class="loading">正在摆好擂台…</div>';
|
|
request('/api/v1/tests').then(function (tests) { state.tests = tests; renderHome(); }).catch(renderError);
|
|
}
|
|
function renderHome() {
|
|
root.innerHTML = '<div class="shell"><section class="hero"><div class="eyebrow">TASTE LAB · PUBLIC PREVIEW</div><h1>今天,认真选一个。</h1><p>没有“都可以”。每轮只选一个,让你的偏爱一路赢到底。</p></section><div class="section-head"><h2>全部测试</h2><span class="muted">' + state.tests.length + ' 个题库</span></div><section class="grid">' + state.tests.map(function (test, index) {
|
|
return '<button class="test-card" data-slug="' + test.slug + '"><div class="art" style="background:' + test.accent + '"><small>0' + (index + 1) + '</small><span class="emoji">' + test.emoji + '</span></div><div class="card-body"><h3>' + escapeHtml(test.title) + '</h3><p>' + escapeHtml(test.subtitle) + '</p><div class="meta"><span>' + test.candidate_count + ' 强</span><span>' + test.run_count + ' 人完成 →</span></div></div></button>';
|
|
}).join('') + '</section></div>';
|
|
root.querySelectorAll('.test-card').forEach(function (el) { el.addEventListener('click', function () { loadTest(el.dataset.slug); }); });
|
|
}
|
|
function loadTest(slug) { root.innerHTML = '<div class="loading">正在抽签…</div>'; request('/api/v1/tests/' + slug).then(function (test) { state.test = test; state.bracket = {round:1,pool:test.candidates.slice(),index:0,winners:[],eliminated:[],decisions:[]}; renderPair(); }).catch(renderError); }
|
|
function pair() { return [state.bracket.pool[state.bracket.index], state.bracket.pool[state.bracket.index + 1] || null]; }
|
|
function renderPair() {
|
|
var items = pair(), done = state.bracket.decisions.length, total = state.test.candidates.length - 1;
|
|
root.innerHTML = '<div class="shell arena"><div class="arena-top"><span>第 ' + state.bracket.round + ' 轮</span><span class="muted">' + done + ' / ' + total + '</span></div><div class="progress"><span style="width:' + Math.round(done/total*100) + '%"></span></div><h1>此刻只能留一个,<br>你会选谁?</h1><div class="pair"><button class="choice" data-side="0"><span class="choice-letter">A</span><span class="choice-emoji">' + state.test.emoji + '</span><strong class="choice-name">' + escapeHtml(items[0].name) + '</strong><span class="choice-hint">点它晋级 ↗</span></button><span class="vs">VS</span><button class="choice" data-side="1"><span class="choice-letter">B</span><span class="choice-emoji">' + state.test.emoji + '</span><strong class="choice-name">' + escapeHtml(items[1].name) + '</strong><span class="choice-hint">点它晋级 ↗</span></button></div></div>';
|
|
root.querySelectorAll('.choice').forEach(function (el) { el.addEventListener('click', function () { choose(items[Number(el.dataset.side)]); }); });
|
|
}
|
|
function choose(winner) {
|
|
var b = state.bracket, items = pair(), loser = items[0].id === winner.id ? items[1] : items[0];
|
|
b.winners.push(winner); b.eliminated.unshift(loser); b.decisions.push({round:b.round,left_id:items[0].id,right_id:items[1].id,winner_id:winner.id}); b.index += 2;
|
|
if (b.index < b.pool.length && !b.pool[b.index + 1]) { b.winners.push(b.pool[b.index]); b.index += 1; }
|
|
if (b.index >= b.pool.length) { b.pool = b.winners; b.winners = []; b.index = 0; b.round += 1; }
|
|
if (b.pool.length === 1) complete(); else renderPair();
|
|
}
|
|
function complete() {
|
|
var b = state.bracket, champion = b.pool[0], final = b.decisions[b.decisions.length - 1], runner = final.left_id === champion.id ? final.right_id : final.left_id;
|
|
root.innerHTML = '<div class="loading">正在保存你的冠军…</div>';
|
|
ensureLogin().then(function () { return request('/api/v1/tests/' + state.test.slug + '/runs',{method:'POST',data:{app_key:'hub',champion_id:champion.id,runner_up_id:runner,ranking_ids:b.pool.concat(b.eliminated).map(function(i){return i.id;}),decisions:b.decisions}}); }).then(function (result) { state.result = result; renderResult(); }).catch(renderError);
|
|
}
|
|
function renderResult() {
|
|
var r = state.result;
|
|
root.innerHTML = '<div class="shell result-shell"><section class="champion-panel"><small>FINAL ANSWER · ' + escapeHtml(r.test_title) + '</small><h1>' + escapeHtml(r.champion_name) + '</h1><p>' + (r.runner_up_name ? '决赛击败 ' + escapeHtml(r.runner_up_name) : '一路赢到最后') + '</p></section><div class="result-layout"><canvas id="poster" class="poster" width="750" height="1000"></canvas><aside class="control"><h2>分享卡片</h2><p class="muted">换模板、加贴纸,保存后直接发小红书。</p><button class="theme active" data-theme="celebration">庆祝派对</button><button class="theme" data-theme="editorial">杂志封面</button><button class="theme" data-theme="sticker">贴纸手账</button><button id="stickerToggle" class="theme">手绘贴纸:开启</button><button id="savePoster" class="button wide">保存 PNG</button><button id="replay" class="button outline wide">再测一次</button></aside></div></div>';
|
|
drawPoster(); root.querySelectorAll('[data-theme]').forEach(function (el) { el.addEventListener('click', function () { state.template = el.dataset.theme; root.querySelectorAll('[data-theme]').forEach(function(x){x.classList.remove('active');});el.classList.add('active');drawPoster(); }); });
|
|
document.getElementById('stickerToggle').addEventListener('click',function(){state.sticker=!state.sticker;this.textContent='手绘贴纸:'+(state.sticker?'开启':'关闭');drawPoster();});
|
|
document.getElementById('savePoster').addEventListener('click',function(){var a=document.createElement('a');a.download='我的PK冠军-'+r.champion_name+'.png';a.href=document.getElementById('poster').toDataURL('image/png');a.click();toast('分享图已保存');});
|
|
document.getElementById('replay').addEventListener('click',function(){loadTest(r.test_slug);});
|
|
}
|
|
function drawPoster() {
|
|
var canvas=document.getElementById('poster'),ctx=canvas.getContext('2d'),r=state.result,themes={celebration:['#ff6b35','#fffaf4','#25211f','#ffe568'],editorial:['#f3eadf','#201d1b','#fff','#285ee8'],sticker:['#c9f06b','#1e2810','#ff72a7','#fff']},t=themes[state.template];
|
|
ctx.fillStyle=t[0];ctx.fillRect(0,0,750,1000);ctx.fillStyle=t[1];ctx.font='700 24px system-ui';ctx.fillText('PK 研究所 / MY PICK',54,72);ctx.font='900 58px system-ui';ctx.fillText('我的冠军是',54,156);ctx.fillStyle=t[2];ctx.fillRect(46,205,658,440);ctx.fillStyle=state.template==='celebration'?'#fffaf4':t[1];ctx.font='600 26px system-ui';ctx.fillText(r.test_title,84,270);ctx.font='900 '+(r.champion_name.length>7?62:82)+'px system-ui';ctx.fillText(r.champion_name,84,405);ctx.fillStyle=t[3];ctx.fillRect(84,440,Math.min(500,r.champion_name.length*80),18);ctx.fillStyle=state.template==='celebration'?'#fffaf4':t[1];ctx.font='24px system-ui';ctx.fillText('一路二选一,它赢到了最后。',84,525);ctx.fillStyle=t[1];ctx.font='700 25px system-ui';ctx.fillText('我的 TOP 4',54,720);(r.ranking_names||[]).slice(0,4).forEach(function(name,i){ctx.font='26px system-ui';ctx.fillText((i+1)+'. '+name,64,774+i*48);});if(state.sticker){ctx.fillStyle=t[3];ctx.beginPath();ctx.arc(645,125,58,0,Math.PI*2);ctx.fill();ctx.fillStyle=t[2];ctx.font='900 28px system-ui';ctx.fillText('WIN!',607,134);ctx.font='52px system-ui';ctx.fillText('✦',640,700);ctx.fillText('♡',550,900);}ctx.fillStyle=t[1];ctx.font='20px system-ui';ctx.fillText('保存这张图,把答案分享给同频的人',54,956);
|
|
}
|
|
function loadHistory() { root.innerHTML='<div class="loading">正在翻档案…</div>';ensureLogin().then(function(){return request('/api/v1/me/results');}).then(renderHistory).catch(renderError); }
|
|
function renderHistory(results) { root.innerHTML='<div class="shell history"><div class="hero"><div class="eyebrow">MY TASTE ARCHIVE</div><h1>我的偏爱档案</h1><p>每一次纠结,都算数。</p></div><div class="section-head"><h2>做过的测试</h2><span class="muted">'+results.length+' 条结果</span></div><div class="link-box"><strong>连接独立饭类版</strong><p class="muted">两个小程序需要一次确认才能合并身份。</p><button id="linkCode" class="button">生成 10 分钟绑定码</button><div id="code" class="link-code"></div></div><div id="rows">'+(results.length?results.map(function(r,i){return '<button class="result-row" data-i="'+i+'"><span class="medal">#1</span><span><small>'+escapeHtml(r.test_title)+'</small><strong>'+escapeHtml(r.champion_name)+'</strong></span><span>→</span></button>';}).join(''):'<div class="empty">还没有结果,去选第一个冠军吧。</div>')+'</div></div>';document.getElementById('linkCode').addEventListener('click',function(){request('/api/v1/me/link-code',{method:'POST'}).then(function(d){document.getElementById('code').textContent=d.code;}).catch(renderError);});root.querySelectorAll('.result-row').forEach(function(el){el.addEventListener('click',function(){state.result=results[Number(el.dataset.i)];renderResult();});}); }
|
|
function renderError(error) { root.innerHTML='<div class="shell empty"><h2>暂时没接上服务</h2><p>'+escapeHtml(error.message||'未知错误')+'</p><button class="button" id="retry">重新加载</button></div>';document.getElementById('retry').addEventListener('click',loadHome); }
|
|
document.getElementById('homeButton').addEventListener('click',loadHome); document.getElementById('meButton').addEventListener('click',loadHistory); loadHome();
|
|
}());
|
|
|