feat: MC Report System - MySQL + Express + Vanilla JS SPA
This commit is contained in:
322
public/js/pages/complaint-detail.js
Normal file
322
public/js/pages/complaint-detail.js
Normal file
@@ -0,0 +1,322 @@
|
||||
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); }
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user