feat: poll/voting system with live results and bar charts

This commit is contained in:
2026-07-13 21:37:16 +08:00
parent eed4591059
commit 116fe89ac8
6 changed files with 173 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ const App = {
{ id: 'templates', label: '邮件模板', icon: 'fa-envelope', roles: ['owner','admin'] },
{ id: 'settings', label: '系统设置', icon: 'fa-cog', roles: ['owner','admin'] },
{ id: 'export', label: '数据导出', icon: 'fa-download', roles: ['owner'] },
{ id: 'polls', label: '投票', icon: 'fa-poll', roles: ['owner','admin','player'] },
],
async init() {
@@ -76,6 +77,7 @@ const App = {
case 'templates': this.renderMain('邮件模板', TemplatesPage, param); break;
case 'settings': this.renderMain('系统设置', SettingsPage, param); break;
case 'export': this.renderMain('数据导出', ExportPage, param); break;
case 'polls': this.renderMain('投票', PollsPage, param); break;
default: this.renderMain('控制台', Dashboard, param);
}
},
@@ -98,6 +100,7 @@ const App = {
const u = Auth.user();
el.innerHTML = `<span class="tm ts">${U.esc(u.game_name||u.username)}</span>
<a href="#/dashboard" class="btn btn-p btn-sm"><i class="fas fa-chart-simple"></i> 控制台</a>
<a href="#/polls" class="btn btn-o btn-sm"><i class="fas fa-poll"></i></a>
${Auth.isAdmin() ? `<a href="#/unclaimed" class="btn btn-o btn-sm">待处理</a>`:''}
<button class="btn btn-o btn-sm" onclick="App.logout()"><i class="fas fa-sign-out-alt"></i> 退出</button>`;
} else {
@@ -144,6 +147,7 @@ window.TemplatesPage = TemplatesPage;
window.NotificationsPage = NotificationsPage;
window.SettingsPage = SettingsPage;
window.ExportPage = ExportPage;
window.PollsPage = PollsPage;
window.LoginPage = LoginPage;
window.RegisterPage = RegisterPage;
window.HomePage = HomePage;

81
public/js/pages/polls.js Normal file
View File

@@ -0,0 +1,81 @@
const PollsPage = {
async render() { return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
async mount() {
document.getElementById('page-title').textContent = '投票';
document.getElementById('page-actions').innerHTML = Auth.isOwner()
? '<button class="btn btn-p btn-sm" onclick="PollsPage.showCreate()"><i class="fas fa-plus"></i> 创建投票</button>'
: '';
await this.load();
},
async load() {
const ct = document.getElementById('page-content');
try {
const polls = await API.get('/polls/active');
ct.innerHTML = polls.length === 0
? '<div class="empty"><i class="fas fa-poll"></i><p>暂无进行中的投票</p></div>'
: polls.map(p => this.renderPoll(p)).join('');
for (const p of polls) this.bindVote(p);
} catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
},
renderPoll(p) {
const opts = p.options || [];
const max = Math.max(1, ...Object.values(p.votes || {}));
return `<div class="card" id="poll-${p.id}">
<div class="card-h"><span>${U.esc(p.title)}</span><span class="ts tm">${p.total||0} 票</span></div>
<div class="card-b">
${p.description?`<p class="tm mb-4">${U.esc(p.description)}</p>`:''}
<div style="display:flex;flex-direction:column;gap:8px">
${opts.map((o,i) => {
const v = p.votes?.[i] || 0;
const pct = p.total ? Math.round(v/p.total*100) : 0;
const my = p.my_vote === i;
return `<div class="poll-opt" style="position:relative;overflow:hidden;border:1px solid var(--g200);border-radius:var(--r);cursor:pointer" data-poll="${p.id}" data-idx="${i}">
<div style="position:absolute;top:0;left:0;bottom:0;width:${pct}%;background:${my?'var(--pl)':'var(--g2)'};transition:width .3s;z-index:0"></div>
<div style="position:relative;z-index:1;display:flex;justify-content:space-between;align-items:center;padding:10px 14px;font-size:13px">
<span>${my?'<i class="fas fa-check" style="color:var(--p)"></i> ':''}${U.esc(o)}</span>
<span class="fb">${v}${pct}%</span>
</div>
</div>`;
}).join('')}
</div>
</div>
</div>`;
},
bindVote(p) {
const card = document.getElementById('poll-'+p.id);
if (!card) return;
card.querySelectorAll('.poll-opt').forEach(el => {
el.addEventListener('click', async () => {
try {
await API.post(`/polls/${p.id}/vote`, { option_index: parseInt(el.dataset.idx) });
this.load();
} catch (ex) { alert(ex.message); }
});
});
},
showCreate() {
U.modal('创建投票', `
<form id="poll-form">
<div class="form-group"><label>标题 *</label><input id="pl-title" required></div>
<div class="form-group"><label>描述</label><textarea id="pl-desc" rows="2"></textarea></div>
<div class="form-group"><label>选项 (每行一个至少2个) *</label><textarea id="pl-opts" rows="4" placeholder="选项A&#10;选项B&#10;选项C" required></textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">创建</button></div>
</form>
`);
document.getElementById('poll-form').onsubmit = async e => {
e.preventDefault();
const opts = document.getElementById('pl-opts').value.split('\n').map(s=>s.trim()).filter(s=>s);
if (opts.length < 2) return alert('至少需要2个选项');
try {
await API.post('/polls', { title: document.getElementById('pl-title').value, description: document.getElementById('pl-desc').value, options: opts });
App.closeModal();
this.load();
} catch (ex) { alert(ex.message); }
};
}
};