- 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
152 lines
7.5 KiB
JavaScript
152 lines
7.5 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 TicketsPage = {
|
|
tab: 'all',
|
|
filters: {},
|
|
|
|
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 = `
|
|
<button class="btn btn-p btn-sm" onclick="App.navigate('tickets','create?type=report')"><i class="fas fa-flag"></i> 举报</button>
|
|
<button class="btn btn-s btn-sm" onclick="App.navigate('tickets','create?type=suggestion')"><i class="fas fa-lightbulb"></i> 建议</button>
|
|
`;
|
|
this.renderLayout();
|
|
await this.loadTab(this.tab);
|
|
},
|
|
|
|
renderLayout() {
|
|
const ct = document.getElementById('page-content');
|
|
const isStaff = Auth.isAdmin();
|
|
ct.innerHTML = `
|
|
${isStaff ? `<div class="tab-nav" id="ticket-tabs">
|
|
<button data-tab="all" class="active">全部</button>
|
|
<button data-tab="pending">待处理</button>
|
|
<button data-tab="claimed">已接单</button>
|
|
</div>` : ''}
|
|
<div class="filter-bar">
|
|
${isStaff ? `<select id="ft-type" onchange="TicketsPage.applyFilters()"><option value="">全部类型</option>${Object.entries(U.TYPE).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>` : ''}
|
|
<select id="ft-status" onchange="TicketsPage.applyFilters()"><option value="">全部状态</option>${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>
|
|
<input id="ft-search" placeholder="搜索..." onkeyup="if(event.key==='Enter')TicketsPage.applyFilters()">
|
|
<button class="btn btn-o btn-sm" onclick="TicketsPage.applyFilters()"><i class="fas fa-search"></i></button>
|
|
<button class="btn btn-o btn-sm" onclick="TicketsPage.resetFilters()"><i class="fas fa-undo"></i></button>
|
|
</div>
|
|
<div id="ticket-list"></div>
|
|
`;
|
|
|
|
if (document.getElementById('ticket-tabs')) {
|
|
document.querySelectorAll('#ticket-tabs button').forEach(btn => {
|
|
btn.onclick = () => {
|
|
document.querySelectorAll('#ticket-tabs button').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
this.tab = btn.dataset.tab;
|
|
this.loadTab(this.tab);
|
|
};
|
|
});
|
|
}
|
|
},
|
|
|
|
async loadTab(tab) {
|
|
const list = document.getElementById('ticket-list');
|
|
U.loading(list);
|
|
try {
|
|
const params = new URLSearchParams();
|
|
if (this.filters.type) params.set('type', this.filters.type);
|
|
if (this.filters.status) params.set('status', this.filters.status);
|
|
if (tab === 'pending') params.set('claimed', '0');
|
|
else if (tab === 'claimed') params.set('claimed', '1');
|
|
if (this.filters.search) params.set('search', this.filters.search);
|
|
const qs = params.toString();
|
|
const tickets = await API.get('/tickets' + (qs ? '?' + qs : ''));
|
|
this.renderList(list, tickets);
|
|
} catch (e) { list.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
|
|
},
|
|
|
|
renderList(ct, tickets) {
|
|
const isStaff = Auth.isAdmin();
|
|
ct.innerHTML = tickets.length === 0
|
|
? `<div class="empty"><i class="fas fa-inbox"></i><p>暂无工单</p></div>`
|
|
: `${isStaff ? `<div class="filter-bar">
|
|
<button class="btn btn-o btn-sm" onclick="TicketsPage.selectAll()">全选</button>
|
|
<button class="btn btn-w btn-sm" onclick="TicketsPage.batchAction('close')">批量关闭</button>
|
|
<button class="btn btn-d btn-sm" onclick="TicketsPage.batchAction('delete')">批量删除</button>
|
|
<select id="batch-status" class="btn-o btn-sm" style="padding:5px 10px;font-size:12px;border:1px solid var(--g300);border-radius:var(--r)">
|
|
<option value="resolved">已解决</option><option value="rejected">已驳回</option><option value="processing">处理中</option><option value="pending">待处理</option>
|
|
</select>
|
|
<button class="btn btn-o btn-sm" onclick="TicketsPage.batchAction('status')">批量改状态</button>
|
|
</div>` : ''}
|
|
<div class="card table-wrap"><table><thead><tr>
|
|
${isStaff ? '<th><input type="checkbox" id="select-all" onchange="TicketsPage.toggleAll()"></th>' : ''}
|
|
<th>#</th><th>类型</th><th>标题</th><th>提交者</th><th>对象</th><th>状态</th><th>优先级</th><th>负责人</th><th>时间</th><th>操作</th>
|
|
</tr></thead><tbody>${tickets.map(t => `
|
|
<tr>
|
|
${isStaff ? `<td><input type="checkbox" class="ticket-cb" value="${t.id}"></td>` : ''}
|
|
<td>${t.id}</td>
|
|
<td>${U.badge(t.type,'type')}</td>
|
|
<td><a href="#" onclick="App.navigate('ticket',${t.id});return false" style="color:var(--p);text-decoration:none;font-weight:600">${U.esc(t.title)}</a></td>
|
|
<td>${U.esc(t.reporter_game_name)}<br><span class="tm ts">UID:${U.esc(t.reporter_game_uid)}</span></td>
|
|
<td>${t.target_game_name||t.target_game_uid?`${U.esc(t.target_game_name||'?')}${t.target_game_uid?`<br><span class="tm ts">UID:${U.esc(t.target_game_uid)}</span>`:''}`:'-'}</td>
|
|
<td>${U.badge(t.status,'status')}</td>
|
|
<td>${U.badge(t.priority,'priority')}</td>
|
|
<td>${t.assignee_name?`<span class="badge bg-admin">${U.esc(t.assignee_name)}</span>`:`<span class="tm">待认领</span>`}</td>
|
|
<td class="ts">${U.date(t.updated_at)}</td>
|
|
<td><button class="btn btn-o btn-sm" onclick="App.navigate('ticket',${t.id})"><i class="fas fa-eye"></i></button></td>
|
|
</tr>`).join('')}</tbody></table></div>`;
|
|
},
|
|
|
|
applyFilters() {
|
|
this.filters = {
|
|
type: document.getElementById('ft-type')?.value,
|
|
status: document.getElementById('ft-status').value,
|
|
search: document.getElementById('ft-search').value,
|
|
};
|
|
this.loadTab(this.tab);
|
|
},
|
|
|
|
resetFilters() {
|
|
this.filters = {};
|
|
document.querySelectorAll('.filter-bar select, .filter-bar input').forEach(el => { if (el.tagName === 'SELECT' || el.tagName === 'INPUT') el.value = ''; });
|
|
this.loadTab(this.tab);
|
|
},
|
|
|
|
toggleAll() {
|
|
const checked = document.getElementById('select-all').checked;
|
|
document.querySelectorAll('.ticket-cb').forEach(cb => cb.checked = checked);
|
|
},
|
|
|
|
selectAll() {
|
|
document.querySelectorAll('.ticket-cb').forEach(cb => cb.checked = true);
|
|
document.getElementById('select-all').checked = true;
|
|
},
|
|
|
|
selectedIds() {
|
|
return Array.from(document.querySelectorAll('.ticket-cb:checked')).map(cb => parseInt(cb.value));
|
|
},
|
|
|
|
async batchAction(action) {
|
|
const ids = this.selectedIds();
|
|
if (!ids.length) return alert('请先选择工单');
|
|
const status = document.getElementById('batch-status')?.value;
|
|
if (!confirm(`确认${action==='delete'?'删除':'操作'} ${ids.length} 个工单?`)) return;
|
|
try { await API.post('/tickets/batch', { ids, action, status }); this.loadTab(this.tab); } catch(ex){alert(ex.message);}
|
|
}
|
|
};
|