feat: MC Report System - MySQL + Express + Vanilla JS SPA

This commit is contained in:
2026-07-12 01:23:19 +08:00
commit 247a4e851d
52 changed files with 5710 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
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> 提交';
}
});
}
};