143 lines
8.8 KiB
JavaScript
143 lines
8.8 KiB
JavaScript
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><button class="btn btn-o btn-sm" onclick="PollsPage.showGroupMgr()"><i class="fas fa-cog"></i> 分组</button>'
|
||
: '';
|
||
await this.load();
|
||
},
|
||
|
||
async load() {
|
||
const ct = document.getElementById('page-content');
|
||
try {
|
||
const groups = await API.get('/polls/active');
|
||
const entries = Object.entries(groups);
|
||
if (entries.length === 0) {
|
||
ct.innerHTML = '<div class="empty"><i class="fas fa-poll"></i><p>暂无进行中的投票</p></div>';
|
||
return;
|
||
}
|
||
ct.innerHTML = entries.map(([group, polls]) => `
|
||
<div style="margin-bottom:24px">
|
||
<h3 style="font-size:16px;font-weight:700;margin-bottom:12px;color:var(--p)"><i class="fas fa-server"></i> ${U.esc(group)}</h3>
|
||
${polls.map(p => this.renderPoll(p)).join('')}
|
||
</div>
|
||
`).join('');
|
||
for (const [group, polls] of entries) 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 timed = p.start_time || p.end_time;
|
||
return `<div class="card" id="poll-${p.id}">
|
||
<div class="card-h"><span>${U.esc(p.server_name)} — ${U.esc(p.title)}</span>
|
||
<span style="display:flex;gap:8px;align-items:center">
|
||
${timed ? `<span class="ts tm">${p.start_time?'起: '+U.date(p.start_time):''} ${p.end_time?'至: '+U.date(p.end_time):''}</span>`:''}
|
||
<span class="ts tm">${p.total||0} 票</span>
|
||
${Auth.isOwner() ? `<button class="btn btn-o btn-sm" onclick="PollsPage.showEdit(${p.id})"><i class="fas fa-edit"></i></button>` : ''}
|
||
</span>
|
||
</div>
|
||
<div class="card-b">
|
||
${p.description?`<p class="tm mb-4">${U.esc(p.description)}</p>`:''}
|
||
${p.voted ? `<div class="alert alert-s"><i class="fas fa-check-circle"></i> 已投票</div>` : ''}
|
||
<div style="display:flex;flex-direction:column;gap:8px">
|
||
${opts.map((o,i) => {
|
||
const v = p.voted ? 0 : (p.votes?.[i] || 0);
|
||
const pct = p.voted ? 0 : (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);${p.voted?'cursor:default':'cursor:pointer'}" data-poll="${p.id}" data-idx="${i}">
|
||
${!p.voted ? `<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>
|
||
${!p.voted ? `<span class="fb">${v}票 ${pct}%</span>` : ''}
|
||
</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
},
|
||
|
||
bindVote(p) {
|
||
if (p.voted) return;
|
||
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() {
|
||
this.loadGroups().then(groups => {
|
||
const opts = groups.map(g => `<option value="${g.group_name}::${g.server_name}">${g.group_name} / ${g.server_name}</option>`).join('');
|
||
U.modal('创建投票', `
|
||
<form id="poll-form">
|
||
<div class="grid-2"><div class="form-group"><label>分组/子服 *</label><select id="pl-gs"><option value="">请选择</option>${opts}</select></div>
|
||
<div class="form-group"><label>标题 *</label><input id="pl-title" required></div></div>
|
||
<div class="form-group"><label>描述</label><textarea id="pl-desc" rows="2"></textarea></div>
|
||
<div class="grid-2"><div class="form-group"><label>开始时间(可选)</label><input type="datetime-local" id="pl-start"></div>
|
||
<div class="form-group"><label>结束时间(可选)</label><input type="datetime-local" id="pl-end"></div></div>
|
||
<div class="form-group"><label>选项 (每行一个,至少2个) *</label><textarea id="pl-opts" rows="4" placeholder="选项A 选项B 选项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 [group_name, server_name] = document.getElementById('pl-gs').value.split('::');
|
||
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, group_name, server_name, start_time: document.getElementById('pl-start').value||null, end_time: document.getElementById('pl-end').value||null });
|
||
App.closeModal(); this.load();
|
||
} catch (ex) { alert(ex.message); }
|
||
};
|
||
});
|
||
},
|
||
|
||
async loadGroups() { try { return await API.get('/polls/groups'); } catch { return []; } },
|
||
|
||
showGroupMgr() {
|
||
this.loadGroups().then(groups => {
|
||
const list = groups.map(g => `<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--g200)">
|
||
<span>${U.esc(g.group_name)} / ${U.esc(g.server_name)}</span>
|
||
<button class="btn btn-d btn-sm" onclick="PollsPage.delGroup(${g.id})"><i class="fas fa-trash"></i></button>
|
||
</div>`).join('');
|
||
U.modal('管理分组', `
|
||
<form id="gm-form"><div class="grid-2"><div class="form-group"><label>分组名</label><input id="gm-group" required></div><div class="form-group"><label>子服名</label><input id="gm-server" required></div></div>
|
||
<button type="submit" class="btn btn-p btn-sm mb-4"><i class="fas fa-plus"></i> 添加</button></form>
|
||
<div>${list||'<p class="tm">暂无</p>'}</div>
|
||
`);
|
||
document.getElementById('gm-form').onsubmit = async e => { e.preventDefault();
|
||
try { await API.post('/polls/groups', { group_name: document.getElementById('gm-group').value, server_name: document.getElementById('gm-server').value }); App.closeModal(); this.showGroupMgr(); } catch(ex){alert(ex.message);}
|
||
};
|
||
});
|
||
},
|
||
|
||
async delGroup(id) { try { await API.req('DELETE', '/polls/groups/'+id); App.closeModal(); this.showGroupMgr(); } catch(ex){alert(ex.message);} },
|
||
|
||
async showEdit(id) {
|
||
const p = await API.get('/polls/'+id);
|
||
document.getElementById('modal-title').textContent = '编辑投票';
|
||
document.getElementById('modal-body').innerHTML = `
|
||
<form id="pe-form">
|
||
<div class="form-group"><label>状态</label><select id="pe-active"><option value="1" ${p.active?'selected':''}>启用</option><option value="0" ${!p.active?'selected':''}>关闭</option></select></div>
|
||
<div class="form-group"><label>标题</label><input id="pe-title" value="${U.esc(p.title)}"></div>
|
||
<div class="form-group"><label>选项 (每行一个)</label><textarea id="pe-opts" rows="4">${U.esc((JSON.parse(p.options||'[]')).join('\n'))}</textarea></div>
|
||
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="button" class="btn btn-d" onclick="PollsPage.deletePoll(${id})"><i class="fas fa-trash"></i></button><button type="submit" class="btn btn-p">保存</button></div>
|
||
</form>
|
||
`;
|
||
document.getElementById('modal-overlay').classList.remove('hidden');
|
||
document.getElementById('pe-form').onsubmit = async e => {
|
||
e.preventDefault();
|
||
const opts = document.getElementById('pe-opts').value.split('\n').map(s=>s.trim()).filter(s=>s);
|
||
try { await API.put('/polls/'+id, { title: document.getElementById('pe-title').value, active: document.getElementById('pe-active').value==='1', options: opts.length>=2?opts:undefined }); App.closeModal(); this.load(); } catch (ex) { alert(ex.message); }
|
||
};
|
||
},
|
||
|
||
async deletePoll(id) { if (!await U.confirm('确认删除?')) return; try { await API.req('DELETE', '/polls/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} }
|
||
};
|