feat: batch ticket operations (close/delete/change status)

This commit is contained in:
2026-07-17 21:55:15 +08:00
parent ed6fe9dcf1
commit 5497de9aac
2 changed files with 61 additions and 1 deletions

View File

@@ -259,6 +259,32 @@ router.post('/:id/transfer', authenticate, requireRole('owner','admin'), async (
res.json({ message: '转交成功' });
});
router.post('/batch', authenticate, requireRole('owner','admin'), async (req, res) => {
const { ids, action, status } = req.body;
if (!ids || !Array.isArray(ids) || ids.length === 0) return res.status(400).json({ error: '请选择工单' });
if (!['delete','status','close'].includes(action)) return res.status(400).json({ error: '无效操作' });
const pool = await getPool();
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
for (const id of ids) {
if (action === 'delete') {
await conn.execute('DELETE FROM responses WHERE ticket_id = ?', [id]);
await conn.execute('DELETE FROM attachments WHERE ticket_id = ?', [id]);
await conn.execute('DELETE FROM ticket_transfers WHERE ticket_id = ?', [id]);
await conn.execute('DELETE FROM tickets WHERE id = ?', [id]);
} else {
const s = action === 'close' ? 'closed' : (status || 'closed');
await conn.execute('UPDATE tickets SET status = ? WHERE id = ?', [s, id]);
await conn.execute("INSERT INTO responses(ticket_id, user_id, content, is_staff) VALUES (?,?,?,1)", [id, req.user.id, `【批量】状态变更为: ${SL[s]||s}`]);
}
}
await conn.commit();
res.json({ message: `已处理 ${ids.length} 个工单` });
} catch(e) { await conn.rollback(); conn.release(); return res.status(500).json({ error: e.message }); }
conn.release();
});
router.post('/:id/response', authenticate, async (req, res) => {
const { content } = req.body;
if (!content) return res.status(400).json({ error: '内容不能为空' });

View File

@@ -62,12 +62,24 @@ const TicketsPage = {
},
renderList(ct, tickets) {
const isStaff = Auth.isAdmin();
ct.innerHTML = tickets.length === 0
? `<div class="empty"><i class="fas fa-inbox"></i><p>暂无工单</p></div>`
: `<div class="card table-wrap"><table><thead><tr>
: `${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>
@@ -94,5 +106,27 @@ const TicketsPage = {
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);}
}
};