Files
MC_Report/public/js/pages/complaint-detail.js

323 lines
16 KiB
JavaScript

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); }
});
}
};