chore: remove stale files from v2
This commit is contained in:
@@ -1,40 +0,0 @@
|
|||||||
const crypto = require('crypto');
|
|
||||||
const { getDb } = require('./db');
|
|
||||||
|
|
||||||
function generate() {
|
|
||||||
const db = getDb();
|
|
||||||
db.prepare("DELETE FROM captchas WHERE datetime(created_at) < datetime('now','localtime','-5 minutes')").run();
|
|
||||||
|
|
||||||
const id = crypto.randomUUID();
|
|
||||||
const ops = ['+', '-', '\u00d7'];
|
|
||||||
const op = ops[Math.floor(Math.random() * ops.length)];
|
|
||||||
let a, b, answer;
|
|
||||||
|
|
||||||
if (op === '\u00d7') {
|
|
||||||
a = Math.floor(Math.random() * 9) + 1;
|
|
||||||
b = Math.floor(Math.random() * 9) + 1;
|
|
||||||
} else {
|
|
||||||
a = Math.floor(Math.random() * 20) + 1;
|
|
||||||
b = Math.floor(Math.random() * 10) + 1;
|
|
||||||
if (op === '-' && a < b) [a, b] = [b, a];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op === '+') answer = a + b;
|
|
||||||
else if (op === '-') answer = a - b;
|
|
||||||
else answer = a * b;
|
|
||||||
|
|
||||||
const question = `${a} ${op} ${b} = ?`;
|
|
||||||
db.prepare('INSERT INTO captchas(id, question, answer) VALUES (?,?,?)').run(id, question, String(answer));
|
|
||||||
|
|
||||||
return { id, question };
|
|
||||||
}
|
|
||||||
|
|
||||||
function verify(id, answer) {
|
|
||||||
const db = getDb();
|
|
||||||
const row = db.prepare("SELECT * FROM captchas WHERE id = ? AND datetime(created_at) > datetime('now','localtime','-5 minutes')").get(id);
|
|
||||||
if (!row) return false;
|
|
||||||
db.prepare('DELETE FROM captchas WHERE id = ?').run(id);
|
|
||||||
return String(answer).trim() === row.answer;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { generate, verify };
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const { getDb } = require('../db');
|
|
||||||
const { authenticate, requireRole, optionalAuth } = require('../middleware/auth');
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.get('/', optionalAuth, (req, res) => {
|
|
||||||
const db = getDb();
|
|
||||||
let query = `SELECT c.*,
|
|
||||||
p.display_name as planner_name, i.display_name as implementer_name, r.display_name as reviewer_name
|
|
||||||
FROM complaints c
|
|
||||||
LEFT JOIN users p ON c.assigned_planner_id = p.id
|
|
||||||
LEFT JOIN users i ON c.assigned_implementer_id = i.id
|
|
||||||
LEFT JOIN users r ON c.assigned_reviewer_id = r.id`;
|
|
||||||
let conditions = [];
|
|
||||||
let params = [];
|
|
||||||
|
|
||||||
const { status, priority, category, search } = req.query;
|
|
||||||
|
|
||||||
if (!req.user) {
|
|
||||||
conditions.push("c.status != 'pending'");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
conditions.push('c.status = ?');
|
|
||||||
params.push(status);
|
|
||||||
}
|
|
||||||
if (priority) {
|
|
||||||
conditions.push('c.priority = ?');
|
|
||||||
params.push(priority);
|
|
||||||
}
|
|
||||||
if (category) {
|
|
||||||
conditions.push('c.category = ?');
|
|
||||||
params.push(category);
|
|
||||||
}
|
|
||||||
if (search) {
|
|
||||||
conditions.push("(c.title LIKE ? OR c.description LIKE ? OR c.player_name LIKE ?)");
|
|
||||||
const s = `%${search}%`;
|
|
||||||
params.push(s, s, s);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.user && req.user.role === 'planner') {
|
|
||||||
conditions.push('(c.assigned_planner_id = ? OR c.assigned_planner_id IS NULL)');
|
|
||||||
params.push(req.user.id);
|
|
||||||
}
|
|
||||||
if (req.user && req.user.role === 'implementer') {
|
|
||||||
conditions.push('c.assigned_implementer_id = ?');
|
|
||||||
params.push(req.user.id);
|
|
||||||
}
|
|
||||||
if (req.user && req.user.role === 'reviewer') {
|
|
||||||
conditions.push('c.assigned_reviewer_id = ?');
|
|
||||||
params.push(req.user.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conditions.length > 0) {
|
|
||||||
query += ' WHERE ' + conditions.join(' AND ');
|
|
||||||
}
|
|
||||||
query += ' ORDER BY c.updated_at DESC';
|
|
||||||
|
|
||||||
const complaints = db.prepare(query).all(...params);
|
|
||||||
res.json(complaints);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/stats', authenticate, (req, res) => {
|
|
||||||
const db = getDb();
|
|
||||||
const total = db.prepare('SELECT COUNT(*) as count FROM complaints').get();
|
|
||||||
const byStatus = db.prepare('SELECT status, COUNT(*) as count FROM complaints GROUP BY status').all();
|
|
||||||
const byPriority = db.prepare('SELECT priority, COUNT(*) as count FROM complaints GROUP BY priority').all();
|
|
||||||
const today = db.prepare("SELECT COUNT(*) as count FROM complaints WHERE date(created_at) = date('now','localtime')").get();
|
|
||||||
|
|
||||||
let myStats = {};
|
|
||||||
if (['planner', 'implementer', 'reviewer'].includes(req.user.role)) {
|
|
||||||
const roleMap = { planner: 'assigned_planner_id', implementer: 'assigned_implementer_id', reviewer: 'assigned_reviewer_id' };
|
|
||||||
const col = roleMap[req.user.role];
|
|
||||||
myStats.total = db.prepare(`SELECT COUNT(*) as count FROM complaints WHERE ${col} = ?`).get(req.user.id).count;
|
|
||||||
myStats.pending = db.prepare(`SELECT COUNT(*) as count FROM complaints WHERE ${col} = ? AND status NOT IN ('resolved','closed','rejected')`).get(req.user.id).count;
|
|
||||||
myStats.completed = db.prepare(`SELECT COUNT(*) as count FROM complaints WHERE ${col} = ? AND status IN ('resolved','closed')`).get(req.user.id).count;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({ total: total.count, byStatus, byPriority, today: today.count, myStats });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', optionalAuth, (req, res) => {
|
|
||||||
const db = getDb();
|
|
||||||
const complaint = db.prepare(`SELECT c.*,
|
|
||||||
p.display_name as planner_name, i.display_name as implementer_name, r.display_name as reviewer_name
|
|
||||||
FROM complaints c
|
|
||||||
LEFT JOIN users p ON c.assigned_planner_id = p.id
|
|
||||||
LEFT JOIN users i ON c.assigned_implementer_id = i.id
|
|
||||||
LEFT JOIN users r ON c.assigned_reviewer_id = r.id
|
|
||||||
WHERE c.id = ?`).get(req.params.id);
|
|
||||||
if (!complaint) return res.status(404).json({ error: '投诉不存在' });
|
|
||||||
|
|
||||||
const responses = db.prepare(`SELECT r.*, u.display_name as user_name, u.role as user_role
|
|
||||||
FROM responses r LEFT JOIN users u ON r.user_id = u.id
|
|
||||||
WHERE r.complaint_id = ? ORDER BY r.created_at ASC`).all(req.params.id);
|
|
||||||
complaint.responses = responses;
|
|
||||||
|
|
||||||
if (complaint.plan_id) {
|
|
||||||
const plan = db.prepare(`SELECT p.*, u.display_name as planner_name FROM plans p
|
|
||||||
LEFT JOIN users u ON p.planner_id = u.id WHERE p.id = ?`).get(complaint.plan_id);
|
|
||||||
complaint.plan = plan;
|
|
||||||
}
|
|
||||||
if (complaint.implementation_id) {
|
|
||||||
const impl = db.prepare(`SELECT i.*, u.display_name as implementer_name FROM implementations i
|
|
||||||
LEFT JOIN users u ON i.implementer_id = u.id WHERE i.id = ?`).get(complaint.implementation_id);
|
|
||||||
complaint.implementation = impl;
|
|
||||||
}
|
|
||||||
if (complaint.review_id) {
|
|
||||||
const review = db.prepare(`SELECT r.*, u.display_name as reviewer_name FROM reviews r
|
|
||||||
LEFT JOIN users u ON r.reviewer_id = u.id WHERE r.id = ?`).get(complaint.review_id);
|
|
||||||
complaint.review = review;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(complaint);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/', optionalAuth, (req, res) => {
|
|
||||||
const { title, description, category, priority, player_name, player_email, player_contact } = req.body;
|
|
||||||
if (!title || !description || !player_name) {
|
|
||||||
return res.status(400).json({ error: '标题、描述和玩家名称为必填项' });
|
|
||||||
}
|
|
||||||
const db = getDb();
|
|
||||||
const result = db.prepare(`INSERT INTO complaints(title, description, category, priority, player_name, player_email, player_contact, status)
|
|
||||||
VALUES (?,?,?,?,?,?,?,'pending')`).run(
|
|
||||||
title, description, category || 'other', priority || 'medium', player_name, player_email || '', player_contact || ''
|
|
||||||
);
|
|
||||||
const complaintId = result.lastInsertRowid;
|
|
||||||
|
|
||||||
db.prepare(`INSERT INTO responses(complaint_id, user_id, response_type, content) VALUES (?,?,?,?)`).run(
|
|
||||||
complaintId, req.user?.id || null, 'system', '投诉已提交,等待处理'
|
|
||||||
);
|
|
||||||
db.prepare(`INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)`).run(
|
|
||||||
req.user?.id || null, req.user?.username || player_name, 'create', 'complaint', complaintId,
|
|
||||||
`投诉已创建: ${title}`
|
|
||||||
);
|
|
||||||
res.status(201).json({ id: complaintId, message: '投诉提交成功' });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.put('/:id', authenticate, (req, res) => {
|
|
||||||
const db = getDb();
|
|
||||||
const complaint = db.prepare('SELECT * FROM complaints WHERE id = ?').get(req.params.id);
|
|
||||||
if (!complaint) return res.status(404).json({ error: '投诉不存在' });
|
|
||||||
|
|
||||||
const allowedFields = {};
|
|
||||||
if (req.body.title) allowedFields.title = req.body.title;
|
|
||||||
if (req.body.description) allowedFields.description = req.body.description;
|
|
||||||
if (req.body.category) allowedFields.category = req.body.category;
|
|
||||||
if (req.body.priority) allowedFields.priority = req.body.priority;
|
|
||||||
if (req.body.status) allowedFields.status = req.body.status;
|
|
||||||
|
|
||||||
if (Object.keys(allowedFields).length === 0) {
|
|
||||||
return res.status(400).json({ error: '没有可更新的字段' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignments = {};
|
|
||||||
if (req.body.assigned_planner_id !== undefined) assignments.assigned_planner_id = req.body.assigned_planner_id;
|
|
||||||
if (req.body.assigned_implementer_id !== undefined) assignments.assigned_implementer_id = req.body.assigned_implementer_id;
|
|
||||||
if (req.body.assigned_reviewer_id !== undefined) assignments.assigned_reviewer_id = req.body.assigned_reviewer_id;
|
|
||||||
|
|
||||||
const allFields = { ...allowedFields, ...assignments };
|
|
||||||
const setClauses = Object.keys(allFields).map(k => `${k} = ?`).join(', ');
|
|
||||||
const values = Object.values(allFields);
|
|
||||||
|
|
||||||
db.prepare(`UPDATE complaints SET ${setClauses}, updated_at = datetime('now','localtime') WHERE id = ?`).run(...values, req.params.id);
|
|
||||||
|
|
||||||
if (req.body.status && req.body.status !== complaint.status) {
|
|
||||||
db.prepare(`INSERT INTO responses(complaint_id, user_id, response_type, content) VALUES (?,?,?,?)`).run(
|
|
||||||
req.params.id, req.user.id, 'system', `状态变更为: ${getStatusLabel(req.body.status)}`
|
|
||||||
);
|
|
||||||
db.prepare(`INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)`).run(
|
|
||||||
req.user.id, req.user.username, 'update_status', 'complaint', req.params.id,
|
|
||||||
`状态从 ${complaint.status} 变更为 ${req.body.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
db.prepare(`INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)`).run(
|
|
||||||
req.user.id, req.user.username, 'update', 'complaint', req.params.id, '更新投诉信息'
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({ message: '更新成功' });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/:id/response', authenticate, (req, res) => {
|
|
||||||
const { content, response_type } = req.body;
|
|
||||||
if (!content) return res.status(400).json({ error: '内容不能为空' });
|
|
||||||
const db = getDb();
|
|
||||||
const complaint = db.prepare('SELECT * FROM complaints WHERE id = ?').get(req.params.id);
|
|
||||||
if (!complaint) return res.status(404).json({ error: '投诉不存在' });
|
|
||||||
|
|
||||||
db.prepare(`INSERT INTO responses(complaint_id, user_id, response_type, content) VALUES (?,?,?,?)`).run(
|
|
||||||
req.params.id, req.user.id, response_type || 'comment', content
|
|
||||||
);
|
|
||||||
db.prepare(`UPDATE complaints SET updated_at = datetime('now','localtime') WHERE id = ?`).run(req.params.id);
|
|
||||||
|
|
||||||
res.status(201).json({ message: '回复添加成功' });
|
|
||||||
});
|
|
||||||
|
|
||||||
function getStatusLabel(status) {
|
|
||||||
const labels = {
|
|
||||||
pending: '待处理', under_review: '审查中', planned: '已计划',
|
|
||||||
in_progress: '实施中', in_review: '审核中', resolved: '已解决',
|
|
||||||
rejected: '已驳回', closed: '已关闭'
|
|
||||||
};
|
|
||||||
return labels[status] || status;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ses_0b2c2409dffegYwIl1HBMSlSLL
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
const ComplaintDetailPage = {
|
|
||||||
complaintId: null,
|
|
||||||
|
|
||||||
async render(id) {
|
|
||||||
this.complaintId = id;
|
|
||||||
return '<div class="loading"><i class="fas fa-spinner"></i><p>加载中...</p></div>';
|
|
||||||
},
|
|
||||||
|
|
||||||
async mount() {
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
const backBtn = '<button class="btn btn-outline btn-sm" onclick="App.navigate(\'complaints\')"><i class="fas fa-arrow-left"></i> 返回</button>';
|
|
||||||
document.getElementById('page-title').textContent = `投诉 #${this.complaintId}`;
|
|
||||||
document.getElementById('page-actions').innerHTML = backBtn;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const complaint = await API.get(`/complaints/${this.complaintId}`);
|
|
||||||
this.renderDetail(container, complaint);
|
|
||||||
} catch (err) {
|
|
||||||
container.innerHTML = `<div class="alert alert-danger">加载失败: ${err.message}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDetail(container, c) {
|
|
||||||
const user = Auth.getUser();
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="detail-grid">
|
|
||||||
<div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>基本信息</span>
|
|
||||||
<div>${Utils.statusBadge(c.status)} ${Utils.priorityBadge(c.priority)}</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="detail-row"><span class="label">标题</span><span class="value font-bold">${Utils.escapeHtml(c.title)}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">描述</span><span class="value">${Utils.escapeHtml(c.description)}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">分类</span><span class="value">${Utils.categoryBadge(c.category)}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">玩家</span><span class="value">${Utils.escapeHtml(c.player_name)}</span></div>
|
|
||||||
${c.player_email ? `<div class="detail-row"><span class="label">邮箱</span><span class="value">${Utils.escapeHtml(c.player_email)}</span></div>` : ''}
|
|
||||||
${c.player_contact ? `<div class="detail-row"><span class="label">联系方式</span><span class="value">${Utils.escapeHtml(c.player_contact)}</span></div>` : ''}
|
|
||||||
<div class="detail-row"><span class="label">创建时间</span><span class="value">${Utils.formatDate(c.created_at)}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">更新时间</span><span class="value">${Utils.formatDate(c.updated_at)}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${c.plan ? this.renderPlanSection(c) : ''}
|
|
||||||
${c.implementation ? this.renderImplementationSection(c) : ''}
|
|
||||||
${c.review ? this.renderReviewSection(c) : ''}
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">沟通记录</div>
|
|
||||||
<div class="card-body">
|
|
||||||
${c.responses && c.responses.length > 0 ? `
|
|
||||||
<div class="timeline">
|
|
||||||
${c.responses.map(r => `
|
|
||||||
<div class="timeline-item ${r.response_type === 'system' ? 'system' : ''}">
|
|
||||||
<div class="timeline-header">
|
|
||||||
<span class="name">${Utils.escapeHtml(r.user_name) || '系统'}</span>
|
|
||||||
${r.user_role ? `<span class="badge badge-${r.user_role}">${Utils.roleLabels[r.user_role]}</span>` : ''}
|
|
||||||
<span>${Utils.formatDate(r.created_at)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-content">${Utils.escapeHtml(r.content)}</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
` : '<p class="text-muted">暂无记录</p>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">添加回复</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form id="add-response-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<textarea id="response-content" rows="3" placeholder="输入回复内容..." required></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary btn-sm"><i class="fas fa-reply"></i> 发送</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">人员分配</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="detail-row"><span class="label">规划者</span><span class="value">${c.planner_name || '<span class="text-muted">未分配</span>'}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">实施者</span><span class="value">${c.implementer_name || '<span class="text-muted">未分配</span>'}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">审查者</span><span class="value">${c.reviewer_name || '<span class="text-muted">未分配</span>'}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">操作</div>
|
|
||||||
<div class="card-body" style="display:flex;flex-direction:column;gap:8px">
|
|
||||||
${user && (user.role === 'planner' || user.role === 'admin') && !c.plan ? `
|
|
||||||
<button class="btn btn-primary btn-sm w-full" onclick="ComplaintDetailPage.createPlan(${c.id})">
|
|
||||||
<i class="fas fa-clipboard-list"></i> 创建处理计划
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
${user && (user.role === 'planner' || user.role === 'admin') && c.plan && c.plan.status === 'draft' ? `
|
|
||||||
<button class="btn btn-success btn-sm w-full" onclick="ComplaintDetailPage.assignImplementer(${c.id}, ${c.plan.id})">
|
|
||||||
<i class="fas fa-user-tag"></i> 分配实施者
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
${user && (user.role === 'implementer' || user.role === 'admin') && c.plan && c.implementer_name === user.display_name && !c.implementation ? `
|
|
||||||
<button class="btn btn-info btn-sm w-full" onclick="ComplaintDetailPage.startImplementation(${c.id}, ${c.plan.id})">
|
|
||||||
<i class="fas fa-code"></i> 开始实施
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
${user && (user.role === 'reviewer' || user.role === 'admin') && c.implementation && c.implementation.status === 'completed' && !c.review ? `
|
|
||||||
<button class="btn btn-warning btn-sm w-full" onclick="ComplaintDetailPage.startReview(${c.id}, ${c.implementation.id})">
|
|
||||||
<i class="fas fa-search"></i> 开始审查
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
${user && (user.role === 'admin') ? `
|
|
||||||
<button class="btn btn-outline btn-sm w-full" onclick="ComplaintDetailPage.showUpdateStatusModal(${c.id})">
|
|
||||||
<i class="fas fa-edit"></i> 修改状态
|
|
||||||
</button>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('add-response-form')?.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const content = document.getElementById('response-content').value.trim();
|
|
||||||
if (!content) return;
|
|
||||||
try {
|
|
||||||
await API.post(`/complaints/${c.id}/response`, { content });
|
|
||||||
document.getElementById('response-content').value = '';
|
|
||||||
this.mount();
|
|
||||||
} catch (err) {
|
|
||||||
alert(err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPlanSection(c) {
|
|
||||||
const p = c.plan;
|
|
||||||
return `
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>处理计划</span>
|
|
||||||
<span>${Utils.statusBadge(p.status)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="detail-row"><span class="label">标题</span><span class="value font-bold">${Utils.escapeHtml(p.title)}</span></div>
|
|
||||||
<div class="detail-row"><span class="label">描述</span><span class="value">${Utils.escapeHtml(p.description)}</span></div>
|
|
||||||
${p.technical_approach ? `<div class="detail-row"><span class="label">技术方案</span><span class="value" style="white-space:pre-wrap">${Utils.escapeHtml(p.technical_approach)}</span></div>` : ''}
|
|
||||||
${p.expected_effort ? `<div class="detail-row"><span class="label">预计工时</span><span class="value">${p.expected_effort}</span></div>` : ''}
|
|
||||||
${p.reject_reason ? `<div class="detail-row"><span class="label">驳回原因</span><span class="value" style="color:var(--danger)">${Utils.escapeHtml(p.reject_reason)}</span></div>` : ''}
|
|
||||||
<div class="detail-row"><span class="label">规划者</span><span class="value">${p.planner_name || '-'}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderImplementationSection(c) {
|
|
||||||
const i = c.implementation;
|
|
||||||
return `
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>实施情况</span>
|
|
||||||
<span>${Utils.statusBadge(i.status)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
${i.code_reference ? `<div class="detail-row"><span class="label">代码参考</span><span class="value" style="white-space:pre-wrap">${Utils.escapeHtml(i.code_reference)}</span></div>` : ''}
|
|
||||||
${i.implementation_notes ? `<div class="detail-row"><span class="label">实施说明</span><span class="value" style="white-space:pre-wrap">${Utils.escapeHtml(i.implementation_notes)}</span></div>` : ''}
|
|
||||||
${i.reject_reason ? `<div class="detail-row"><span class="label">驳回原因</span><span class="value" style="color:var(--danger)">${Utils.escapeHtml(i.reject_reason)}</span></div>` : ''}
|
|
||||||
<div class="detail-row"><span class="label">实施者</span><span class="value">${i.implementer_name || '-'}</span></div>
|
|
||||||
${i.status === 'in_progress' ? `
|
|
||||||
<div class="mt-4">
|
|
||||||
<form id="complete-implementation-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>代码参考/PR链接</label>
|
|
||||||
<input type="text" id="impl-code" placeholder="Git提交号/PR链接等">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>实施说明</label>
|
|
||||||
<textarea id="impl-notes" rows="3" placeholder="实施过程中的说明..."></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success btn-sm"><i class="fas fa-check"></i> 标记完成</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderReviewSection(c) {
|
|
||||||
const r = c.review;
|
|
||||||
return `
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>审查结果</span>
|
|
||||||
<span>${Utils.statusBadge(r.status)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
${r.review_notes ? `<div class="detail-row"><span class="label">审查意见</span><span class="value" style="white-space:pre-wrap">${Utils.escapeHtml(r.review_notes)}</span></div>` : ''}
|
|
||||||
${r.issues_found ? `<div class="detail-row"><span class="label">发现的问题</span><span class="value" style="white-space:pre-wrap">${Utils.escapeHtml(r.issues_found)}</span></div>` : ''}
|
|
||||||
${r.reject_reason ? `<div class="detail-row"><span class="label">驳回原因</span><span class="value" style="color:var(--danger)">${Utils.escapeHtml(r.reject_reason)}</span></div>` : ''}
|
|
||||||
<div class="detail-row"><span class="label">审查者</span><span class="value">${r.reviewer_name || '-'}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async refresh() {
|
|
||||||
await this.mount();
|
|
||||||
},
|
|
||||||
|
|
||||||
async createPlan(complaintId) {
|
|
||||||
Utils.openModal('创建处理计划', `
|
|
||||||
<form id="create-plan-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>计划标题 *</label>
|
|
||||||
<input type="text" id="plan-title" required placeholder="例如: 修复闪退问题">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>计划描述 *</label>
|
|
||||||
<textarea id="plan-description" required rows="4" placeholder="详细描述处理方案..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>技术方案</label>
|
|
||||||
<textarea id="plan-tech" rows="3" placeholder="技术实现方案..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>预计工时</label>
|
|
||||||
<input type="text" id="plan-effort" placeholder="例如: 2小时">
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" style="padding:16px 0 0;border-top:1px solid var(--gray-200)">
|
|
||||||
<button type="button" class="btn btn-outline" onclick="App.closeModal()">取消</button>
|
|
||||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> 创建</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
document.getElementById('create-plan-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await API.post('/plans', {
|
|
||||||
complaint_id: complaintId,
|
|
||||||
title: document.getElementById('plan-title').value,
|
|
||||||
description: document.getElementById('plan-description').value,
|
|
||||||
technical_approach: document.getElementById('plan-tech').value,
|
|
||||||
expected_effort: document.getElementById('plan-effort').value,
|
|
||||||
});
|
|
||||||
App.closeModal();
|
|
||||||
this.refresh();
|
|
||||||
} catch (err) { alert(err.message); }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async assignImplementer(complaintId, planId) {
|
|
||||||
const users = await API.get('/users/implementers');
|
|
||||||
if (!users.length) { alert('暂无可用实施者'); return; }
|
|
||||||
Utils.openModal('分配实施者', `
|
|
||||||
<form id="assign-impl-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>选择实施者</label>
|
|
||||||
<select id="assign-impl-user">
|
|
||||||
${users.map(u => `<option value="${u.id}">${u.display_name} (${u.username})</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" style="padding:16px 0 0;border-top:1px solid var(--gray-200)">
|
|
||||||
<button type="button" class="btn btn-outline" onclick="App.closeModal()">取消</button>
|
|
||||||
<button type="submit" class="btn btn-primary"><i class="fas fa-user-tag"></i> 分配</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
document.getElementById('assign-impl-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await API.post(`/plans/${planId}/assign`, { implementer_id: parseInt(document.getElementById('assign-impl-user').value) });
|
|
||||||
App.closeModal();
|
|
||||||
this.refresh();
|
|
||||||
} catch (err) { alert(err.message); }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async startImplementation(complaintId, planId) {
|
|
||||||
try {
|
|
||||||
await API.post('/implementations', { plan_id: planId });
|
|
||||||
this.refresh();
|
|
||||||
} catch (err) { alert(err.message); }
|
|
||||||
},
|
|
||||||
|
|
||||||
async startReview(complaintId, implementationId) {
|
|
||||||
try {
|
|
||||||
await API.post('/reviews', { implementation_id: implementationId });
|
|
||||||
this.refresh();
|
|
||||||
} catch (err) { alert(err.message); }
|
|
||||||
},
|
|
||||||
|
|
||||||
showUpdateStatusModal(complaintId) {
|
|
||||||
Utils.openModal('修改状态', `
|
|
||||||
<form id="update-status-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>新状态</label>
|
|
||||||
<select id="new-status">
|
|
||||||
${Object.entries(Utils.statusLabels).map(([k,v]) => `<option value="${k}">${v}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" style="padding:16px 0 0;border-top:1px solid var(--gray-200)">
|
|
||||||
<button type="button" class="btn btn-outline" onclick="App.closeModal()">取消</button>
|
|
||||||
<button type="submit" class="btn btn-primary"><i class="fas fa-save"></i> 保存</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
document.getElementById('update-status-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
try {
|
|
||||||
await API.put(`/complaints/${complaintId}`, { status: document.getElementById('new-status').value });
|
|
||||||
App.closeModal();
|
|
||||||
this.refresh();
|
|
||||||
} catch (err) { alert(err.message); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
const ComplaintsPage = {
|
|
||||||
async render() {
|
|
||||||
return '<div class="loading"><i class="fas fa-spinner"></i><p>加载中...</p></div>';
|
|
||||||
},
|
|
||||||
|
|
||||||
async mount() {
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
document.getElementById('page-title').textContent = '投诉列表';
|
|
||||||
document.getElementById('page-actions').innerHTML = '<button class="btn btn-primary" onclick="ComplaintsPage.showCreateModal()"><i class="fas fa-plus"></i> 提交投诉</button>';
|
|
||||||
|
|
||||||
await this.loadList(container);
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadList(container, filters = {}) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (filters.status) params.set('status', filters.status);
|
|
||||||
if (filters.priority) params.set('priority', filters.priority);
|
|
||||||
if (filters.category) params.set('category', filters.category);
|
|
||||||
if (filters.search) params.set('search', filters.search);
|
|
||||||
const qs = params.toString();
|
|
||||||
|
|
||||||
container.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i><p>加载中...</p></div>';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const complaints = await API.get('/complaints' + (qs ? '?' + qs : ''));
|
|
||||||
this.renderList(container, complaints, filters);
|
|
||||||
} catch (err) {
|
|
||||||
container.innerHTML = `<div class="alert alert-danger">加载失败: ${err.message}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderList(container, complaints, filters = {}) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="filter-bar">
|
|
||||||
<select id="filter-status" onchange="ComplaintsPage.applyFilters()">
|
|
||||||
<option value="">全部状态</option>
|
|
||||||
${Object.entries(Utils.statusLabels).map(([k,v]) => `<option value="${k}" ${filters.status === k ? 'selected' : ''}>${v}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
<select id="filter-priority" onchange="ComplaintsPage.applyFilters()">
|
|
||||||
<option value="">全部优先级</option>
|
|
||||||
${Object.entries(Utils.priorityLabels).map(([k,v]) => `<option value="${k}" ${filters.priority === k ? 'selected' : ''}>${v}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
<select id="filter-category" onchange="ComplaintsPage.applyFilters()">
|
|
||||||
<option value="">全部分类</option>
|
|
||||||
${Object.entries(Utils.categoryLabels).map(([k,v]) => `<option value="${k}" ${filters.category === k ? 'selected' : ''}>${v}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
<input id="filter-search" placeholder="搜索标题/描述/玩家..." value="${filters.search || ''}" onkeyup="if(event.key==='Enter')ComplaintsPage.applyFilters()">
|
|
||||||
<button class="btn btn-outline btn-sm" onclick="ComplaintsPage.applyFilters()"><i class="fas fa-search"></i> 搜索</button>
|
|
||||||
<button class="btn btn-outline btn-sm" onclick="ComplaintsPage.resetFilters()"><i class="fas fa-undo"></i> 重置</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${complaints.length === 0 ? `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-inbox"></i>
|
|
||||||
<p>暂无投诉记录</p>
|
|
||||||
</div>
|
|
||||||
` : `
|
|
||||||
<div class="card">
|
|
||||||
<div class="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<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>
|
|
||||||
${complaints.map(c => `
|
|
||||||
<tr>
|
|
||||||
<td>${c.id}</td>
|
|
||||||
<td><strong><a href="#" onclick="App.navigate('complaint',${c.id});return false" style="color:var(--primary);text-decoration:none">${Utils.escapeHtml(c.title)}</a></strong></td>
|
|
||||||
<td>${Utils.escapeHtml(c.player_name)}</td>
|
|
||||||
<td>${Utils.categoryBadge(c.category)}</td>
|
|
||||||
<td>${Utils.priorityBadge(c.priority)}</td>
|
|
||||||
<td>${Utils.statusBadge(c.status)}</td>
|
|
||||||
<td>${c.planner_name || '-'}</td>
|
|
||||||
<td>${c.implementer_name || '-'}</td>
|
|
||||||
<td>${c.reviewer_name || '-'}</td>
|
|
||||||
<td class="date">${Utils.formatDate(c.updated_at)}</td>
|
|
||||||
<td><button class="btn btn-sm btn-outline" onclick="App.navigate('complaint',${c.id})"><i class="fas fa-eye"></i></button></td>
|
|
||||||
</tr>
|
|
||||||
`).join('')}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
|
|
||||||
applyFilters() {
|
|
||||||
const filters = {
|
|
||||||
status: document.getElementById('filter-status').value,
|
|
||||||
priority: document.getElementById('filter-priority').value,
|
|
||||||
category: document.getElementById('filter-category').value,
|
|
||||||
search: document.getElementById('filter-search').value,
|
|
||||||
};
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
this.loadList(container, filters);
|
|
||||||
},
|
|
||||||
|
|
||||||
resetFilters() {
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
this.loadList(container, {});
|
|
||||||
},
|
|
||||||
|
|
||||||
showCreateModal() {
|
|
||||||
Utils.openModal('提交投诉', `
|
|
||||||
<form id="create-complaint-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>标题 *</label>
|
|
||||||
<input type="text" id="c-title" required placeholder="投诉标题">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>描述 *</label>
|
|
||||||
<textarea id="c-description" required placeholder="请详细描述问题..." rows="4"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="grid-2">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>分类</label>
|
|
||||||
<select id="c-category">
|
|
||||||
${Object.entries(Utils.categoryLabels).map(([k,v]) => `<option value="${k}">${v}</option>`).join('')}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>优先级</label>
|
|
||||||
<select id="c-priority">
|
|
||||||
<option value="low">低</option>
|
|
||||||
<option value="medium" selected>中</option>
|
|
||||||
<option value="high">高</option>
|
|
||||||
<option value="critical">紧急</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>玩家名称 *</label>
|
|
||||||
<input type="text" id="c-player_name" required placeholder="游戏内昵称">
|
|
||||||
</div>
|
|
||||||
<div class="grid-2">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>邮箱</label>
|
|
||||||
<input type="email" id="c-player_email" placeholder="选填">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>联系方式</label>
|
|
||||||
<input type="text" id="c-player_contact" placeholder="手机/微信/QQ">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer" style="padding:16px 0 0;border-top:1px solid var(--gray-200)">
|
|
||||||
<button type="button" class="btn btn-outline" onclick="App.closeModal()">取消</button>
|
|
||||||
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i> 提交</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
`);
|
|
||||||
document.getElementById('create-complaint-form').addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const btn = e.target.querySelector('button[type="submit"]');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 提交中...';
|
|
||||||
try {
|
|
||||||
await API.post('/complaints', {
|
|
||||||
title: document.getElementById('c-title').value,
|
|
||||||
description: document.getElementById('c-description').value,
|
|
||||||
category: document.getElementById('c-category').value,
|
|
||||||
priority: document.getElementById('c-priority').value,
|
|
||||||
player_name: document.getElementById('c-player_name').value,
|
|
||||||
player_email: document.getElementById('c-player_email').value,
|
|
||||||
player_contact: document.getElementById('c-player_contact').value,
|
|
||||||
});
|
|
||||||
App.closeModal();
|
|
||||||
this.mount();
|
|
||||||
} catch (err) {
|
|
||||||
alert(err.message);
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> 提交';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const ImplementationsPage = {
|
|
||||||
async render() {
|
|
||||||
return '<div class="loading"><i class="fas fa-spinner"></i><p>加载中...</p></div>';
|
|
||||||
},
|
|
||||||
|
|
||||||
async mount() {
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
document.getElementById('page-title').textContent = '实施管理';
|
|
||||||
document.getElementById('page-actions').innerHTML = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const impls = await API.get('/implementations');
|
|
||||||
this.renderList(container, impls);
|
|
||||||
} catch (err) {
|
|
||||||
container.innerHTML = `<div class="alert alert-danger">加载失败: ${err.message}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderList(container, impls) {
|
|
||||||
container.innerHTML = `
|
|
||||||
${impls.length === 0 ? `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-code"></i>
|
|
||||||
<p>暂无实施记录</p>
|
|
||||||
</div>
|
|
||||||
` : `
|
|
||||||
<div class="card">
|
|
||||||
<div class="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>关联投诉</th>
|
|
||||||
<th>关联计划</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>实施者</th>
|
|
||||||
<th>创建时间</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${impls.map(i => `
|
|
||||||
<tr>
|
|
||||||
<td>${i.id}</td>
|
|
||||||
<td><a href="#" onclick="App.navigate('complaint',${i.complaint_id});return false" style="color:var(--primary)">#${i.complaint_id} ${Utils.escapeHtml(i.complaint_title)}</a></td>
|
|
||||||
<td class="text-sm text-muted">${Utils.escapeHtml(i.plan_title)}</td>
|
|
||||||
<td>${Utils.statusBadge(i.status)}</td>
|
|
||||||
<td>${i.implementer_name || '-'}</td>
|
|
||||||
<td class="date">${Utils.formatDate(i.created_at)}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="App.navigate('complaint',${i.complaint_id})"><i class="fas fa-eye"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('')}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const PlansPage = {
|
|
||||||
async render() {
|
|
||||||
return '<div class="loading"><i class="fas fa-spinner"></i><p>加载中...</p></div>';
|
|
||||||
},
|
|
||||||
|
|
||||||
async mount() {
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
document.getElementById('page-title').textContent = '计划管理';
|
|
||||||
document.getElementById('page-actions').innerHTML = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const plans = await API.get('/plans');
|
|
||||||
this.renderList(container, plans);
|
|
||||||
} catch (err) {
|
|
||||||
container.innerHTML = `<div class="alert alert-danger">加载失败: ${err.message}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderList(container, plans) {
|
|
||||||
container.innerHTML = `
|
|
||||||
${plans.length === 0 ? `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-clipboard-list"></i>
|
|
||||||
<p>暂无计划</p>
|
|
||||||
</div>
|
|
||||||
` : `
|
|
||||||
<div class="card">
|
|
||||||
<div class="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>计划标题</th>
|
|
||||||
<th>关联投诉</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>规划者</th>
|
|
||||||
<th>创建时间</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${plans.map(p => `
|
|
||||||
<tr>
|
|
||||||
<td>${p.id}</td>
|
|
||||||
<td><strong>${Utils.escapeHtml(p.title)}</strong></td>
|
|
||||||
<td><a href="#" onclick="App.navigate('complaint',${p.complaint_id});return false" style="color:var(--primary)">#${p.complaint_id} ${Utils.escapeHtml(p.complaint_title)}</a></td>
|
|
||||||
<td>${Utils.statusBadge(p.status)}</td>
|
|
||||||
<td>${p.planner_name || '-'}</td>
|
|
||||||
<td class="date">${Utils.formatDate(p.created_at)}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="App.navigate('complaint',${p.complaint_id})"><i class="fas fa-eye"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('')}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
const ReviewsPage = {
|
|
||||||
async render() {
|
|
||||||
return '<div class="loading"><i class="fas fa-spinner"></i><p>加载中...</p></div>';
|
|
||||||
},
|
|
||||||
|
|
||||||
async mount() {
|
|
||||||
const container = document.getElementById('page-content');
|
|
||||||
document.getElementById('page-title').textContent = '审查管理';
|
|
||||||
document.getElementById('page-actions').innerHTML = '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reviews = await API.get('/reviews');
|
|
||||||
this.renderList(container, reviews);
|
|
||||||
} catch (err) {
|
|
||||||
container.innerHTML = `<div class="alert alert-danger">加载失败: ${err.message}</div>`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderList(container, reviews) {
|
|
||||||
container.innerHTML = `
|
|
||||||
${reviews.length === 0 ? `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-search"></i>
|
|
||||||
<p>暂无审查记录</p>
|
|
||||||
</div>
|
|
||||||
` : `
|
|
||||||
<div class="card">
|
|
||||||
<div class="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>关联投诉</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>审查者</th>
|
|
||||||
<th>审查意见</th>
|
|
||||||
<th>创建时间</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${reviews.map(r => `
|
|
||||||
<tr>
|
|
||||||
<td>${r.id}</td>
|
|
||||||
<td><a href="#" onclick="App.navigate('complaint',${r.complaint_id});return false" style="color:var(--primary)">#${r.complaint_id} ${Utils.escapeHtml(r.complaint_title)}</a></td>
|
|
||||||
<td>${Utils.statusBadge(r.status)}</td>
|
|
||||||
<td>${r.reviewer_name || '-'}</td>
|
|
||||||
<td class="text-sm text-muted" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${r.review_notes ? Utils.escapeHtml(r.review_notes.slice(0, 50)) : '-'}</td>
|
|
||||||
<td class="date">${Utils.formatDate(r.created_at)}</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="App.navigate('complaint',${r.complaint_id})"><i class="fas fa-eye"></i></button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('')}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user