TH1/Tools/Dashboard/js/bugs.js
2026-06-28 00:57:53 +08:00

474 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ========== 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()">&times;</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()">&times;</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'] });
}
})();