diff --git a/backend/routes/tickets.js b/backend/routes/tickets.js index c686931..4d6c4d1 100644 --- a/backend/routes/tickets.js +++ b/backend/routes/tickets.js @@ -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: '内容不能为空' }); diff --git a/public/js/pages/tickets.js b/public/js/pages/tickets.js index 81d7f8c..6cca9d2 100644 --- a/public/js/pages/tickets.js +++ b/public/js/pages/tickets.js @@ -62,12 +62,24 @@ const TicketsPage = { }, renderList(ct, tickets) { + const isStaff = Auth.isAdmin(); ct.innerHTML = tickets.length === 0 ? `
暂无工单
| ' : ''} | # | 类型 | 标题 | 提交者 | 对象 | 状态 | 优先级 | 负责人 | 时间 | 操作 |
|---|---|---|---|---|---|---|---|---|---|---|
| ` : ''} | ${t.id} | ${U.badge(t.type,'type')} | ${U.esc(t.title)} | @@ -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);} } };