Files
MC_Report/public/js/pages/features.js
canglan 8c5ce78fd0 chore: add AGPLv3 copyright header to all source files
- 52 JS files (backend + public/js): header with
  Copyright (C) 2026 Sea Network Technology Studio
  Author: CangLan <admin@sea-studio.top>
  + AGPLv3 notice
- idempotent (skips if header present), all syntax-checked
2026-08-19 20:27:05 +08:00

200 lines
11 KiB
JavaScript

/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const FeaturesPage = {
sort: 'time',
filterGroup: '',
filterServer: '',
filterStatus: '',
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 class="filter-bar">
<select id="feat-group" onchange="FeaturesPage.setFilter()"><option value="">全部分组</option></select>
<select id="feat-server" onchange="FeaturesPage.setFilter()"><option value="">全部子服</option></select>
<select id="feat-status" onchange="FeaturesPage.setFilter()"><option value="all">全部状态</option><option value="pending">待定</option><option value="planned">计划中</option><option value="done">已完成</option></select>
<span style="flex:1"></span>
<button class="btn btn-o btn-sm ${this.sort==='time'?'btn-p':''}" id="sort-time" onclick="FeaturesPage.setSort('time')">时间</button>
<button class="btn btn-o btn-sm ${this.sort==='votes'?'btn-p':''}" 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.filterStatus = document.getElementById('feat-status').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, status: this.filterStatus });
const items = await API.get('/features?' + params);
ct.innerHTML = items.length === 0 ? '<div class="empty"><i class="fas fa-inbox"></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 statusLabel = i.status==='done'?'已完成':i.status==='planned'?'计划中':'待定';
return `<div class="card feat-item" id="feat-${i.id}">
<div class="card-h">
<span><a href="#/feature/${i.id}" style="color:inherit;text-decoration:none"><span class="status-dot" style="color:${sc}">${i.status==='done'?'\u2713':i.status==='planned'?'\u25C9':'\u25CB'}</span> ${U.esc(i.title)}</a></span>
<div style="display:flex;gap:8px;align-items:center">
${i.group_name ? `<span class="badge bg-player">${U.esc(i.group_name)}</span>` : ''}
${Auth.isOwner() ? `<button class="btn btn-o btn-sm" data-action="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 style="white-space:pre-wrap;margin-bottom:12px">${U.esc(i.description)}</p>` : ''}
<div class="feat-bar">
<button class="btn btn-${i.my_vote?'s':'o'} btn-sm feat-vote-btn" data-id="${i.id}"><i class="fas fa-thumbs-up"></i> ${i.votes_count||0}</button>
<span class="ts tm"><i class="fas fa-comment"></i> <a href="#/feature/${i.id}" style="color:inherit;text-decoration:none">${i.comments_count||0} 讨论</a></span>
<span class="ts tm">${U.date(i.created_at)}</span>
</div>
</div>
</div>`;
},
toggleDiscuss(id) {
const el = document.getElementById('discuss-'+id);
if (el.style.display === 'none') {
el.style.display = 'block';
this.loadComments(id);
} else {
el.style.display = 'none';
}
},
async loadComments(id) {
const ct = document.getElementById('comments-'+id);
try {
const item = await API.get('/features/'+id);
ct.innerHTML = item.comments.length === 0 ? '<p class="tm ts">暂无评论</p>'
: item.comments.map(c => `<div style="padding:6px 0;border-bottom:1px solid var(--g100)">
<span class="fb">${U.esc(c.username||'用户')}</span>
${c.role ? U.badge(c.role, 'role') : ''}
<span class="ts tm">${U.date(c.created_at)}</span>
<div style="margin-top:4px;white-space:pre-wrap">${U.esc(c.content)}</div>
</div>`).join('');
} catch { ct.innerHTML = '<p class="tm">加载失败</p>'; }
},
bindComment(i) {
const form = document.getElementById('comment-form-'+i.id);
if (!form) return;
form.onsubmit = async e => {
e.preventDefault();
const input = document.getElementById('comment-input-'+i.id);
const content = input.value.trim();
if (!content) return;
try { await API.post(`/features/${i.id}/comment`, { content }); input.value = ''; this.loadComments(i.id); this.load(); } catch(ex){alert(ex.message);}
};
},
bindVote(i) {
const card = document.getElementById('feat-'+i.id);
if (!card) return;
const btn = card.querySelector('.feat-vote');
if (!btn) return;
btn.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="3"></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="3">${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);} }
};