feat: batch ticket operations (close/delete/change status)
This commit is contained in:
@@ -259,6 +259,32 @@ router.post('/:id/transfer', authenticate, requireRole('owner','admin'), async (
|
|||||||
res.json({ message: '转交成功' });
|
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) => {
|
router.post('/:id/response', authenticate, async (req, res) => {
|
||||||
const { content } = req.body;
|
const { content } = req.body;
|
||||||
if (!content) return res.status(400).json({ error: '内容不能为空' });
|
if (!content) return res.status(400).json({ error: '内容不能为空' });
|
||||||
|
|||||||
@@ -62,12 +62,24 @@ const TicketsPage = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderList(ct, tickets) {
|
renderList(ct, tickets) {
|
||||||
|
const isStaff = Auth.isAdmin();
|
||||||
ct.innerHTML = tickets.length === 0
|
ct.innerHTML = tickets.length === 0
|
||||||
? `<div class="empty"><i class="fas fa-inbox"></i><p>暂无工单</p></div>`
|
? `<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>
|
<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></thead><tbody>${tickets.map(t => `
|
||||||
<tr>
|
<tr>
|
||||||
|
${isStaff ? `<td><input type="checkbox" class="ticket-cb" value="${t.id}"></td>` : ''}
|
||||||
<td>${t.id}</td>
|
<td>${t.id}</td>
|
||||||
<td>${U.badge(t.type,'type')}</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><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 = {};
|
this.filters = {};
|
||||||
document.querySelectorAll('.filter-bar select, .filter-bar input').forEach(el => { if (el.tagName === 'SELECT' || el.tagName === 'INPUT') el.value = ''; });
|
document.querySelectorAll('.filter-bar select, .filter-bar input').forEach(el => { if (el.tagName === 'SELECT' || el.tagName === 'INPUT') el.value = ''; });
|
||||||
this.loadTab(this.tab);
|
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);}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user