feat: poll groups/servers, timed polls, features sort+filter by group
This commit is contained in:
140
public/js/pages/features.js
Normal file
140
public/js/pages/features.js
Normal file
@@ -0,0 +1,140 @@
|
||||
const FeaturesPage = {
|
||||
sort: 'time',
|
||||
filterGroup: '',
|
||||
filterServer: '',
|
||||
|
||||
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="FeaturesPage.showCreate()"><i class="fas fa-plus"></i> 添加</button>'
|
||||
: '';
|
||||
this.renderLayout();
|
||||
await this.load();
|
||||
},
|
||||
|
||||
renderLayout() {
|
||||
const ct = document.getElementById('page-content');
|
||||
ct.innerHTML = `<div id="feat-filters" style="display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
|
||||
<select id="feat-group" onchange="FeaturesPage.setFilter()"><option value="">全部分组</option></select>
|
||||
<select id="feat-server" onchange="FeaturesPage.setFilter()"><option value="">全部子服</option></select>
|
||||
<span style="flex:1"></span>
|
||||
<button class="btn btn-o btn-sm ${this.sort==='time'?'active':''}" id="sort-time" onclick="FeaturesPage.setSort('time')">按时间</button>
|
||||
<button class="btn btn-o btn-sm ${this.sort==='votes'?'active':''}" id="sort-votes" onclick="FeaturesPage.setSort('votes')">按热度</button>
|
||||
</div>
|
||||
<div id="feat-list"></div>`;
|
||||
this.loadGroupOptions();
|
||||
},
|
||||
|
||||
async loadGroupOptions() {
|
||||
try {
|
||||
const groups = await API.get('/polls/groups');
|
||||
const groupSel = document.getElementById('feat-group');
|
||||
const serverSel = document.getElementById('feat-server');
|
||||
const uniqueGroups = [...new Set(groups.map(g => g.group_name))];
|
||||
groupSel.innerHTML = '<option value="">全部分组</option>' + uniqueGroups.map(g => `<option value="${g}" ${this.filterGroup===g?'selected':''}>${g}</option>`).join('');
|
||||
const servers = this.filterGroup ? groups.filter(g => g.group_name === this.filterGroup) : groups;
|
||||
const uniqueServers = [...new Set(servers.map(g => g.server_name))];
|
||||
serverSel.innerHTML = '<option value="">全部子服</option>' + uniqueServers.map(s => `<option value="${s}" ${this.filterServer===s?'selected':''}>${s}</option>`).join('');
|
||||
} catch {}
|
||||
},
|
||||
|
||||
setFilter() {
|
||||
this.filterGroup = document.getElementById('feat-group').value;
|
||||
this.filterServer = document.getElementById('feat-server').value;
|
||||
this.loadGroupOptions();
|
||||
this.load();
|
||||
},
|
||||
|
||||
setSort(s) {
|
||||
this.sort = s;
|
||||
document.getElementById('sort-time').className = 'btn btn-o btn-sm ' + (s==='time'?'btn-p':'');
|
||||
document.getElementById('sort-votes').className = 'btn btn-o btn-sm ' + (s==='votes'?'btn-p':'');
|
||||
this.load();
|
||||
},
|
||||
|
||||
async load() {
|
||||
const ct = document.getElementById('feat-list');
|
||||
U.loading(ct);
|
||||
try {
|
||||
const params = new URLSearchParams({ sort: this.sort, group_name: this.filterGroup, server_name: this.filterServer });
|
||||
const items = await API.get('/features?' + params);
|
||||
ct.innerHTML = items.length === 0 ? '<div class="empty"><i class="fas fa-lightbulb"></i><p>暂无</p></div>'
|
||||
: items.map(i => this.renderItem(i)).join('');
|
||||
for (const i of items) if (Auth.logged()) this.bindVote(i);
|
||||
} catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
|
||||
},
|
||||
|
||||
renderItem(i) {
|
||||
const sc = i.status==='done'?'var(--s)':i.status==='planned'?'var(--p)':'var(--g5)';
|
||||
const sl = i.status==='done'?'已完成':i.status==='planned'?'计划中':'待定';
|
||||
return `<div class="card" id="feat-${i.id}">
|
||||
<div class="card-h">
|
||||
<span>${U.esc(i.title)} ${i.group_name ? `<span class="ts tm">— ${U.esc(i.group_name)}/${U.esc(i.server_name)}</span>` : ''}</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<span style="font-size:11px;color:${sc}">● ${sl}</span>
|
||||
${Auth.isOwner() ? `<button class="btn btn-o btn-sm" onclick="FeaturesPage.showEdit(${i.id},'${U.escJs(i.title)}','${U.escJs(i.description||'')}','${i.status}','${U.escJs(i.group_name||'')}','${U.escJs(i.server_name||'')}')"><i class="fas fa-edit"></i></button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-b">
|
||||
${i.description ? `<p class="tm mb-4">${U.esc(i.description)}</p>` : ''}
|
||||
<div style="display:flex;align-items:center;gap:12px">
|
||||
<button class="btn btn-${i.my_vote?'s':'o'} btn-sm feat-vote" data-id="${i.id}">
|
||||
<i class="fas fa-chevron-up"></i> ${i.votes_count || 0}
|
||||
</button>
|
||||
<span class="ts tm">${i.my_vote ? '已投票' : '投票'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
bindVote(i) {
|
||||
const card = document.getElementById('feat-'+i.id);
|
||||
if (!card) return;
|
||||
card.querySelector('.feat-vote').addEventListener('click', async () => {
|
||||
try { await API.post(`/features/${i.id}/vote`); this.load(); } catch (ex) { alert(ex.message); }
|
||||
});
|
||||
},
|
||||
|
||||
async loadGroupsForForm() { try { return await API.get('/polls/groups'); } catch { return []; } },
|
||||
|
||||
async showCreate() {
|
||||
const groups = await this.loadGroupsForForm();
|
||||
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="feat-form">
|
||||
<div class="grid-2"><div class="form-group"><label>标题 *</label><input id="ft-title" required></div>
|
||||
<div class="form-group"><label>分组/子服</label><select id="ft-gs"><option value="::">无</option>${opts}</select></div></div>
|
||||
<div class="form-group"><label>描述</label><textarea id="ft-desc" rows="2"></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('feat-form').onsubmit = async e => { e.preventDefault();
|
||||
const gs = document.getElementById('ft-gs').value;
|
||||
const [group_name, server_name] = gs === '::' ? ['',''] : gs.split('::');
|
||||
try { await API.post('/features', { title: document.getElementById('ft-title').value, description: document.getElementById('ft-desc').value, group_name, server_name }); App.closeModal(); this.load(); } catch(ex){alert(ex.message);}
|
||||
};
|
||||
},
|
||||
|
||||
async showEdit(id, title, desc, status, gn, sn) {
|
||||
const groups = await this.loadGroupsForForm();
|
||||
const opts = groups.map(g => `<option value="${g.group_name}::${g.server_name}" ${gn===g.group_name&&sn===g.server_name?'selected':''}>${g.group_name} / ${g.server_name}</option>`).join('');
|
||||
U.modal('编辑', `
|
||||
<form id="fe-edit">
|
||||
<div class="form-group"><label>标题</label><input id="fe-title" value="${title}"></div>
|
||||
<div class="grid-2"><div class="form-group"><label>状态</label><select id="fe-status"><option value="pending" ${status==='pending'?'selected':''}>待定</option><option value="planned" ${status==='planned'?'selected':''}>计划中</option><option value="done" ${status==='done'?'selected':''}>已完成</option></select></div>
|
||||
<div class="form-group"><label>分组/子服</label><select id="fe-gs"><option value="::">无</option>${opts}</select></div></div>
|
||||
<div class="form-group"><label>描述</label><textarea id="fe-desc" rows="2">${desc}</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="FeaturesPage.del(${id})"><i class="fas fa-trash"></i></button><button type="submit" class="btn btn-p">保存</button></div>
|
||||
</form>
|
||||
`);
|
||||
document.getElementById('fe-edit').onsubmit = async e => { e.preventDefault();
|
||||
const gs = document.getElementById('fe-gs').value;
|
||||
const [group_name, server_name] = gs === '::' ? ['',''] : gs.split('::');
|
||||
try { await API.put('/features/'+id, { title: document.getElementById('fe-title').value, description: document.getElementById('fe-desc').value, status: document.getElementById('fe-status').value, group_name, server_name }); App.closeModal(); this.load(); } catch(ex){alert(ex.message);}
|
||||
};
|
||||
},
|
||||
|
||||
async del(id) { if (!await U.confirm('确认删除?')) return; try { await API.req('DELETE', '/features/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} }
|
||||
};
|
||||
Reference in New Issue
Block a user