474 lines
18 KiB
JavaScript
474 lines
18 KiB
JavaScript
/**
|
||
* TH1 Dashboard - Bug Tracker Module (BUG跟踪)
|
||
*
|
||
* Data source: DOC/bugs.json (project-level, version controlled)
|
||
* Server API:
|
||
* POST /api/bugs/create - Create new bug
|
||
* POST /api/bugs/update - Update bug fields
|
||
* POST /api/bugs/delete - Delete bug by id
|
||
*/
|
||
|
||
let bugsList = [];
|
||
let bugsLoaded = false;
|
||
let bugsNextId = 1;
|
||
let bugsLongTermMode = false;
|
||
const BUG_DEFAULT_STATUS_FILTER = 'open';
|
||
|
||
// ========== Status / Priority Definitions ==========
|
||
|
||
const BUG_STATUSES = {
|
||
open: { label: '待修复', color: '#dc2626', bg: '#fef2f2' },
|
||
fixing: { label: '修复中', color: '#d97706', bg: '#fffbeb' },
|
||
fixed: { label: '已修复', color: '#2563eb', bg: '#eff6ff' },
|
||
verified: { label: '已验证', color: '#16a34a', bg: '#f0fdf4' },
|
||
wontfix: { label: '不修复', color: '#6b7280', bg: '#f9fafb' },
|
||
};
|
||
|
||
const BUG_PRIORITIES = {
|
||
critical: { label: 'P0 致命', color: '#dc2626', sort: 0 },
|
||
high: { label: 'P1 严重', color: '#ea580c', sort: 1 },
|
||
medium: { label: 'P2 一般', color: '#d97706', sort: 2 },
|
||
low: { label: 'P3 轻微', color: '#6b7280', sort: 3 },
|
||
};
|
||
|
||
const BUG_MODULES = [
|
||
'战斗系统', '技能系统', 'AI系统', 'UI界面', '地图系统',
|
||
'科技树', '存档系统', '多语言', '美术资源', '配置数据',
|
||
'网络/联机', '音频', '性能', '其他'
|
||
];
|
||
|
||
// ========== Data Loading ==========
|
||
|
||
async function bugsLoadAll() {
|
||
if (bugsLoaded) return;
|
||
try {
|
||
const r = await fetch('/api/bugs/list?t=' + Date.now());
|
||
if (!r.ok) throw new Error(r.status);
|
||
const data = await r.json();
|
||
bugsList = data.bugs || [];
|
||
bugsNextId = data.nextId || (bugsList.length > 0 ? Math.max(...bugsList.map(b => b.id)) + 1 : 1);
|
||
} catch (e) {
|
||
console.warn('bugsLoad fail:', e);
|
||
bugsList = [];
|
||
}
|
||
bugsLoaded = true;
|
||
bugsRenderList();
|
||
}
|
||
|
||
// ========== List Rendering ==========
|
||
|
||
function bugsRenderList() {
|
||
const container = document.getElementById('bugs-list');
|
||
const countEl = document.getElementById('bugs-count');
|
||
if (!container) return;
|
||
|
||
// Get filter values
|
||
const searchQ = (document.getElementById('bugs-search')?.value || '').trim().toLowerCase();
|
||
const statusF = document.getElementById('bugs-status-filter')?.value || '';
|
||
const priorityF = document.getElementById('bugs-priority-filter')?.value || '';
|
||
const moduleF = document.getElementById('bugs-module-filter')?.value || '';
|
||
|
||
let filtered = [...bugsList];
|
||
|
||
// Apply filters
|
||
if (searchQ) {
|
||
filtered = filtered.filter(b =>
|
||
String(b.id).includes(searchQ) ||
|
||
(b.title || '').toLowerCase().includes(searchQ) ||
|
||
(b.description || '').toLowerCase().includes(searchQ)
|
||
);
|
||
}
|
||
if (statusF) filtered = filtered.filter(b => b.status === statusF);
|
||
if (priorityF) filtered = filtered.filter(b => b.priority === priorityF);
|
||
if (moduleF) filtered = filtered.filter(b => b.module === moduleF);
|
||
filtered = filtered.filter(b => !!b.longTerm === bugsLongTermMode);
|
||
|
||
// Sort: priority (asc) then id (desc)
|
||
filtered.sort((a, b) => {
|
||
const pa = BUG_PRIORITIES[a.priority]?.sort ?? 9;
|
||
const pb = BUG_PRIORITIES[b.priority]?.sort ?? 9;
|
||
if (pa !== pb) return pa - pb;
|
||
return b.id - a.id;
|
||
});
|
||
|
||
// Update count
|
||
if (countEl) {
|
||
const total = bugsList.length;
|
||
const openCount = bugsList.filter(b => b.status === 'open' || b.status === 'fixing').length;
|
||
const longTermCount = bugsList.filter(b => !!b.longTerm).length;
|
||
countEl.textContent = `共 ${total} 个 BUG,${openCount} 个待处理,${longTermCount} 个长期 BUG`;
|
||
}
|
||
|
||
const modeBtn = document.getElementById('bugs-longterm-toggle');
|
||
if (modeBtn) {
|
||
modeBtn.textContent = bugsLongTermMode ? '查看非长期 BUG' : '查看长期 BUG';
|
||
modeBtn.title = bugsLongTermMode ? '切换到非长期 BUG 列表' : '切换到长期 BUG 列表';
|
||
modeBtn.classList.toggle('bug-btn-primary', bugsLongTermMode);
|
||
}
|
||
|
||
if (filtered.length === 0) {
|
||
const emptyText = bugsLongTermMode ? '暂无匹配的长期 BUG 记录' : '暂无匹配的非长期 BUG 记录';
|
||
container.innerHTML = `<div class="loading-inline">${emptyText}</div>`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = filtered.map(b => {
|
||
const st = BUG_STATUSES[b.status] || BUG_STATUSES.open;
|
||
const pr = BUG_PRIORITIES[b.priority] || BUG_PRIORITIES.medium;
|
||
const dateStr = b.createdAt ? new Date(b.createdAt).toLocaleDateString('zh-CN') : '';
|
||
const longTermBtnLabel = b.longTerm ? '转出长期 bug' : '转入长期 bug';
|
||
const longTermBtnTitle = b.longTerm ? '取消长期 BUG 标记' : '标记为长期 BUG';
|
||
const longTermBtn = `<button class="bug-btn bug-btn-sm bug-longterm-action" data-bug-id="${b.id}" title="${longTermBtnTitle}" style="margin-right:4px">${longTermBtnLabel}</button>`;
|
||
|
||
// 快速修复按钮 - 只在待修复/修复中状态时显示
|
||
const showFixBtn = (b.status === 'open' || b.status === 'fixing');
|
||
const fixBtn = showFixBtn ? `<button class="bug-btn bug-btn-sm bug-btn-primary" onclick="bugsQuickFix(${b.id}, event)" title="将状态设为已修复" style="margin-right:4px">✓ 修复</button>` : '';
|
||
|
||
return `<div class="bug-card" onclick="bugsShowDetail(${b.id})">
|
||
<div class="bug-card-left">
|
||
<span class="bug-card-id">#${String(b.id).padStart(3, '0')}</span>
|
||
<span class="bug-card-pri" style="color:${pr.color}">${pr.label}</span>
|
||
<span class="bug-card-title">${bugsEsc(b.title)}</span>
|
||
${b.longTerm ? '<span class="bug-card-module" style="background:#fef3c7;color:#92400e">长期 BUG</span>' : ''}
|
||
${b.module ? `<span class="bug-card-module">${bugsEsc(b.module)}</span>` : ''}
|
||
</div>
|
||
<div class="bug-card-right">
|
||
${longTermBtn}
|
||
${fixBtn}
|
||
<span class="bug-card-status" style="color:${st.color};background:${st.bg}">${st.label}</span>
|
||
<span class="bug-card-date">${dateStr}</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
container.querySelectorAll('.bug-longterm-action').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
bugsToggleLongTerm(Number(btn.dataset.bugId), e);
|
||
});
|
||
});
|
||
}
|
||
|
||
function bugsEsc(s) {
|
||
if (!s) return '';
|
||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||
}
|
||
|
||
// ========== Detail View ==========
|
||
|
||
function bugsShowDetail(id) {
|
||
const bug = bugsList.find(b => b.id === id);
|
||
if (!bug) return;
|
||
|
||
const st = BUG_STATUSES[bug.status] || BUG_STATUSES.open;
|
||
const pr = BUG_PRIORITIES[bug.priority] || BUG_PRIORITIES.medium;
|
||
const dateStr = bug.createdAt ? new Date(bug.createdAt).toLocaleString('zh-CN') : '';
|
||
const updStr = bug.updatedAt ? new Date(bug.updatedAt).toLocaleString('zh-CN') : '';
|
||
|
||
// Status options
|
||
const statusOpts = Object.entries(BUG_STATUSES).map(([k, v]) =>
|
||
`<option value="${k}" ${bug.status === k ? 'selected' : ''}>${v.label}</option>`
|
||
).join('');
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'bug-detail-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="bug-detail-modal">
|
||
<div class="bug-detail-header">
|
||
<div>
|
||
<div class="bug-detail-id">#${String(bug.id).padStart(3, '0')}</div>
|
||
<div class="bug-detail-title">${bugsEsc(bug.title)}</div>
|
||
<div class="bug-detail-meta">
|
||
<span class="bug-card-pri" style="color:${pr.color}">${pr.label}</span>
|
||
${bug.longTerm ? '<span class="bug-card-module" style="background:#fef3c7;color:#92400e">长期 BUG</span>' : ''}
|
||
${bug.module ? `<span class="bug-card-module">${bugsEsc(bug.module)}</span>` : ''}
|
||
<span>创建: ${dateStr}</span>
|
||
${updStr ? `<span>更新: ${updStr}</span>` : ''}
|
||
</div>
|
||
</div>
|
||
<button class="bug-detail-close" onclick="this.closest('.bug-detail-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="bug-detail-body">
|
||
${bug.description ? `<div class="bug-detail-desc">${bugsEsc(bug.description).replace(/\n/g, '<br>')}</div>` : '<div class="bug-detail-desc" style="color:#94a3b8">无详细描述</div>'}
|
||
</div>
|
||
<div class="bug-detail-footer">
|
||
<div class="bug-detail-status-change">
|
||
<label>状态:</label>
|
||
<select id="bug-status-select" class="filter-select" onchange="bugsUpdateStatus(${bug.id}, this.value)">${statusOpts}</select>
|
||
</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<button class="bug-btn" id="bug-detail-longterm-btn" type="button">
|
||
${bug.longTerm ? '转出长期 bug' : '转入长期 bug'}
|
||
</button>
|
||
<button class="bug-btn bug-btn-danger" onclick="bugsDelete(${bug.id})">删除</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(overlay);
|
||
|
||
document.getElementById('bug-detail-longterm-btn')?.addEventListener('click', async (e) => {
|
||
await bugsToggleLongTerm(bug.id, e);
|
||
overlay.remove();
|
||
});
|
||
|
||
// ESC to close
|
||
const handler = (e) => {
|
||
if (e.key === 'Escape') {
|
||
overlay.remove();
|
||
document.removeEventListener('keydown', handler);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handler);
|
||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||
}
|
||
|
||
// ========== Create Bug ==========
|
||
|
||
function bugsShowAddModal() {
|
||
const moduleOpts = BUG_MODULES.map(m => `<option value="${m}">${m}</option>`).join('');
|
||
const priOpts = Object.entries(BUG_PRIORITIES).map(([k, v]) =>
|
||
`<option value="${k}" ${k === 'medium' ? 'selected' : ''}>${v.label}</option>`
|
||
).join('');
|
||
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'bug-detail-overlay';
|
||
overlay.id = 'bug-add-overlay';
|
||
overlay.innerHTML = `
|
||
<div class="bug-add-modal">
|
||
<div class="bug-detail-header">
|
||
<div class="bug-detail-title">添加新 BUG</div>
|
||
<button class="bug-detail-close" onclick="this.closest('.bug-detail-overlay').remove()">×</button>
|
||
</div>
|
||
<div class="bug-add-body">
|
||
<div class="bug-form-group">
|
||
<label class="bug-form-label">标题 *</label>
|
||
<input type="text" id="bug-add-title" class="bug-form-input" placeholder="简要描述BUG现象">
|
||
</div>
|
||
<div class="bug-form-row">
|
||
<div class="bug-form-group" style="flex:1">
|
||
<label class="bug-form-label">优先级</label>
|
||
<select id="bug-add-priority" class="bug-form-select">${priOpts}</select>
|
||
</div>
|
||
<div class="bug-form-group" style="flex:1">
|
||
<label class="bug-form-label">模块</label>
|
||
<select id="bug-add-module" class="bug-form-select">
|
||
<option value="">未分类</option>
|
||
${moduleOpts}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="bug-form-group">
|
||
<label class="bug-form-label">详细描述</label>
|
||
<textarea id="bug-add-desc" class="bug-form-textarea" rows="6" placeholder="复现步骤、预期行为、实际行为..."></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="bug-detail-footer">
|
||
<button class="bug-btn" onclick="document.getElementById('bug-add-overlay').remove()">取消</button>
|
||
<button class="bug-btn bug-btn-primary" onclick="bugsSubmitAdd()">提交 BUG</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(overlay);
|
||
|
||
// Focus title input
|
||
setTimeout(() => document.getElementById('bug-add-title')?.focus(), 100);
|
||
|
||
// ESC to close
|
||
const handler = (e) => {
|
||
if (e.key === 'Escape') {
|
||
overlay.remove();
|
||
document.removeEventListener('keydown', handler);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handler);
|
||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||
}
|
||
|
||
async function bugsSubmitAdd() {
|
||
const title = document.getElementById('bug-add-title').value.trim();
|
||
if (!title) { alert('请输入BUG标题'); return; }
|
||
|
||
const priority = document.getElementById('bug-add-priority').value;
|
||
const module = document.getElementById('bug-add-module').value;
|
||
const description = document.getElementById('bug-add-desc').value.trim();
|
||
|
||
const btn = document.querySelector('#bug-add-overlay .bug-btn-primary');
|
||
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
|
||
|
||
try {
|
||
const r = await fetch('/api/bugs/create', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ title, priority, module, description })
|
||
});
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
document.getElementById('bug-add-overlay')?.remove();
|
||
bugsLoaded = false;
|
||
await bugsLoadAll();
|
||
bugsShowToast(`BUG #${String(result.id).padStart(3, '0')} 已创建`, 'success');
|
||
} else {
|
||
alert('创建失败: ' + (result.error || '未知错误'));
|
||
}
|
||
} catch (e) {
|
||
alert('创建失败: ' + e.message);
|
||
} finally {
|
||
if (btn) { btn.disabled = false; btn.textContent = '提交 BUG'; }
|
||
}
|
||
}
|
||
|
||
// ========== Quick Fix ==========
|
||
|
||
async function bugsQuickFix(id, event) {
|
||
if (event) {
|
||
event.stopPropagation();
|
||
}
|
||
try {
|
||
await bugsUpdateStatus(id, 'fixed');
|
||
} catch (e) {
|
||
console.error('Quick fix failed:', e);
|
||
}
|
||
}
|
||
|
||
// ========== Long-term Bug ==========
|
||
|
||
function bugsToggleLongTermMode() {
|
||
bugsLongTermMode = !bugsLongTermMode;
|
||
bugsRenderList();
|
||
}
|
||
|
||
async function bugsToggleLongTerm(id, event) {
|
||
if (event) {
|
||
event.stopPropagation();
|
||
}
|
||
|
||
const bug = bugsList.find(b => b.id === id);
|
||
if (!bug) return;
|
||
|
||
const nextLongTerm = !bug.longTerm;
|
||
try {
|
||
const r = await fetch('/api/bugs/update', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, longTerm: nextLongTerm })
|
||
});
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
bug.longTerm = nextLongTerm;
|
||
bug.updatedAt = Date.now();
|
||
bugsRenderList();
|
||
bugsShowToast(nextLongTerm ? '已转入长期 BUG' : '已转出长期 BUG', 'success');
|
||
}
|
||
} catch (e) {
|
||
alert('更新失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ========== Update Status ==========
|
||
|
||
async function bugsUpdateStatus(id, newStatus) {
|
||
try {
|
||
const r = await fetch('/api/bugs/update', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, status: newStatus })
|
||
});
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
// Update local data
|
||
const bug = bugsList.find(b => b.id === id);
|
||
if (bug) {
|
||
bug.status = newStatus;
|
||
bug.updatedAt = Date.now();
|
||
}
|
||
bugsRenderList();
|
||
bugsShowToast('状态已更新', 'success');
|
||
}
|
||
} catch (e) {
|
||
alert('更新失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ========== Delete ==========
|
||
|
||
async function bugsDelete(id) {
|
||
if (!confirm(`确定要删除 BUG #${String(id).padStart(3, '0')} 吗?`)) return;
|
||
|
||
try {
|
||
const r = await fetch('/api/bugs/delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id })
|
||
});
|
||
const result = await r.json();
|
||
if (result.success) {
|
||
document.querySelector('.bug-detail-overlay')?.remove();
|
||
bugsLoaded = false;
|
||
await bugsLoadAll();
|
||
bugsShowToast('BUG 已删除', 'success');
|
||
}
|
||
} catch (e) {
|
||
alert('删除失败: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ========== Toast ==========
|
||
|
||
function bugsShowToast(msg, type) {
|
||
const toast = document.getElementById('toast');
|
||
if (!toast) return;
|
||
toast.textContent = msg;
|
||
toast.className = 'toast ' + (type || 'success');
|
||
toast.style.display = 'block';
|
||
setTimeout(() => { toast.style.display = 'none'; }, 3000);
|
||
}
|
||
|
||
// ========== Filter Binding ==========
|
||
|
||
function bugsBindFilters() {
|
||
const statusFilter = document.getElementById('bugs-status-filter');
|
||
if (statusFilter && !statusFilter.value) {
|
||
statusFilter.value = BUG_DEFAULT_STATUS_FILTER;
|
||
}
|
||
|
||
const ids = ['bugs-search', 'bugs-status-filter', 'bugs-priority-filter', 'bugs-module-filter'];
|
||
ids.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.addEventListener(el.tagName === 'SELECT' ? 'change' : 'input', () => bugsRenderList());
|
||
});
|
||
|
||
const longTermToggle = document.getElementById('bugs-longterm-toggle');
|
||
if (longTermToggle && !longTermToggle.dataset.bound) {
|
||
longTermToggle.dataset.bound = '1';
|
||
longTermToggle.addEventListener('click', bugsToggleLongTermMode);
|
||
}
|
||
|
||
// Populate module filter dropdown
|
||
const moduleFilter = document.getElementById('bugs-module-filter');
|
||
if (moduleFilter && moduleFilter.options.length <= 1) {
|
||
BUG_MODULES.forEach(m => {
|
||
const opt = document.createElement('option');
|
||
opt.value = m;
|
||
opt.textContent = m;
|
||
moduleFilter.appendChild(opt);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ========== Lazy Init ==========
|
||
|
||
(function () {
|
||
const observer = new MutationObserver(() => {
|
||
const panel = document.getElementById('panel-bugs');
|
||
if (panel && panel.classList.contains('active') && !bugsLoaded) {
|
||
bugsLoadAll();
|
||
bugsBindFilters();
|
||
}
|
||
});
|
||
const panel = document.getElementById('panel-bugs');
|
||
if (panel) {
|
||
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
|
||
}
|
||
})();
|