Files
MC_Report/public/js/pages/tickets.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

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);}
}
};