TH1/Tools/Dashboard/js/suggestions.js
2026-05-26 20:03:45 +08:00

328 lines
13 KiB
JavaScript

/**
* TH1 Dashboard - Suggestion Tracker Module
*
* Data source: DOC/suggestions.json (project-level, version controlled)
* Server API:
* GET /api/suggestions/list
* POST /api/suggestions/create
* POST /api/suggestions/update
* POST /api/suggestions/delete
*/
let suggestionsList = [];
let suggestionsLoaded = false;
const SUGGESTION_STATUSES = {
open: { label: '未处理', color: '#d97706', bg: '#fffbeb' },
processed: { label: '已处理', color: '#16a34a', bg: '#f0fdf4' },
};
const SUGGESTION_MODULES = [
'战斗系统', '技能系统', 'AI系统', 'UI界面', '地图系统',
'科技树', '存档系统', '多语言', '美术资源', '配置数据',
'网络/联机', '音频', '性能', '其他'
];
async function suggestionsLoadAll() {
if (suggestionsLoaded) return;
try {
const r = await fetch('/api/suggestions/list?t=' + Date.now());
if (!r.ok) throw new Error(r.status);
const data = await r.json();
suggestionsList = data.suggestions || [];
} catch (e) {
console.warn('suggestionsLoad fail:', e);
suggestionsList = [];
}
suggestionsLoaded = true;
suggestionsRenderList();
}
function suggestionsRenderList() {
const container = document.getElementById('suggestions-list');
const countEl = document.getElementById('suggestions-count');
if (!container) return;
const searchQ = (document.getElementById('suggestions-search')?.value || '').trim().toLowerCase();
const statusF = document.getElementById('suggestions-status-filter')?.value || '';
const moduleF = document.getElementById('suggestions-module-filter')?.value || '';
let filtered = [...suggestionsList];
if (searchQ) {
filtered = filtered.filter(s =>
String(s.id).includes(searchQ) ||
(s.title || '').toLowerCase().includes(searchQ) ||
(s.description || '').toLowerCase().includes(searchQ)
);
}
if (statusF) filtered = filtered.filter(s => s.status === statusF);
if (moduleF) filtered = filtered.filter(s => s.module === moduleF);
filtered.sort((a, b) => b.id - a.id);
if (countEl) {
const total = suggestionsList.length;
const openCount = suggestionsList.filter(s => s.status !== 'processed').length;
countEl.textContent = `${total} 条建议,${openCount} 条未处理`;
}
if (filtered.length === 0) {
container.innerHTML = '<div class="loading-inline">暂无匹配的建议记录</div>';
return;
}
container.innerHTML = filtered.map(s => {
const st = SUGGESTION_STATUSES[s.status] || SUGGESTION_STATUSES.open;
const dateStr = s.createdAt ? new Date(s.createdAt).toLocaleDateString('zh-CN') : '';
const showDoneBtn = s.status !== 'processed';
const doneBtn = showDoneBtn ? `<button class="bug-btn bug-btn-sm bug-btn-primary" onclick="suggestionsQuickProcess(${s.id}, event)" title="标记为已处理" style="margin-right:4px">处理</button>` : '';
return `<div class="bug-card" onclick="suggestionsShowDetail(${s.id})">
<div class="bug-card-left">
<span class="bug-card-id">#${String(s.id).padStart(3, '0')}</span>
<span class="bug-card-title">${suggestionsEsc(s.title)}</span>
${s.module ? `<span class="bug-card-module">${suggestionsEsc(s.module)}</span>` : ''}
</div>
<div class="bug-card-right">
${doneBtn}
<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('');
}
function suggestionsEsc(s) {
if (!s) return '';
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function suggestionsShowDetail(id) {
const suggestion = suggestionsList.find(s => s.id === id);
if (!suggestion) return;
const st = SUGGESTION_STATUSES[suggestion.status] || SUGGESTION_STATUSES.open;
const dateStr = suggestion.createdAt ? new Date(suggestion.createdAt).toLocaleString('zh-CN') : '';
const updStr = suggestion.updatedAt ? new Date(suggestion.updatedAt).toLocaleString('zh-CN') : '';
const statusOpts = Object.entries(SUGGESTION_STATUSES).map(([k, v]) =>
`<option value="${k}" ${suggestion.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(suggestion.id).padStart(3, '0')}</div>
<div class="bug-detail-title">${suggestionsEsc(suggestion.title)}</div>
<div class="bug-detail-meta">
<span class="bug-card-status" style="color:${st.color};background:${st.bg}">${st.label}</span>
${suggestion.module ? `<span class="bug-card-module">${suggestionsEsc(suggestion.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">
${suggestion.description ? `<div class="bug-detail-desc">${suggestionsEsc(suggestion.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="suggestion-status-select" class="filter-select" onchange="suggestionsUpdateStatus(${suggestion.id}, this.value)">${statusOpts}</select>
</div>
<div>
<button class="bug-btn bug-btn-danger" onclick="suggestionsDelete(${suggestion.id})">删除</button>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
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(); });
}
function suggestionsShowAddModal() {
const moduleOpts = SUGGESTION_MODULES.map(m => `<option value="${m}">${m}</option>`).join('');
const overlay = document.createElement('div');
overlay.className = 'bug-detail-overlay';
overlay.id = 'suggestion-add-overlay';
overlay.innerHTML = `
<div class="bug-add-modal">
<div class="bug-detail-header">
<div class="bug-detail-title">新增建议</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="suggestion-add-title" class="bug-form-input" placeholder="简要描述建议内容">
</div>
<div class="bug-form-group">
<label class="bug-form-label">模块</label>
<select id="suggestion-add-module" class="bug-form-select">
<option value="">未分类</option>
${moduleOpts}
</select>
</div>
<div class="bug-form-group">
<label class="bug-form-label">详细描述</label>
<textarea id="suggestion-add-desc" class="bug-form-textarea" rows="6" placeholder="记录建议背景、期望调整、玩家反馈来源等..."></textarea>
</div>
</div>
<div class="bug-detail-footer">
<button class="bug-btn" onclick="document.getElementById('suggestion-add-overlay').remove()">取消</button>
<button class="bug-btn bug-btn-primary" onclick="suggestionsSubmitAdd()">提交建议</button>
</div>
</div>
`;
document.body.appendChild(overlay);
setTimeout(() => document.getElementById('suggestion-add-title')?.focus(), 100);
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 suggestionsSubmitAdd() {
const title = document.getElementById('suggestion-add-title').value.trim();
if (!title) { alert('请输入建议标题'); return; }
const module = document.getElementById('suggestion-add-module').value;
const description = document.getElementById('suggestion-add-desc').value.trim();
const btn = document.querySelector('#suggestion-add-overlay .bug-btn-primary');
if (btn) { btn.disabled = true; btn.textContent = '提交中...'; }
try {
const r = await fetch('/api/suggestions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, module, description })
});
const result = await r.json();
if (result.success) {
document.getElementById('suggestion-add-overlay')?.remove();
suggestionsLoaded = false;
await suggestionsLoadAll();
suggestionsShowToast(`建议 #${String(result.id).padStart(3, '0')} 已创建`, 'success');
} else {
alert('创建失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('创建失败: ' + e.message);
} finally {
if (btn) { btn.disabled = false; btn.textContent = '提交建议'; }
}
}
async function suggestionsQuickProcess(id, event) {
if (event) event.stopPropagation();
await suggestionsUpdateStatus(id, 'processed');
}
async function suggestionsUpdateStatus(id, newStatus) {
try {
const r = await fetch('/api/suggestions/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status: newStatus })
});
const result = await r.json();
if (result.success) {
const suggestion = suggestionsList.find(s => s.id === id);
if (suggestion) {
suggestion.status = newStatus;
suggestion.updatedAt = Date.now();
}
suggestionsRenderList();
suggestionsShowToast('状态已更新', 'success');
} else {
alert('更新失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('更新失败: ' + e.message);
}
}
async function suggestionsDelete(id) {
if (!confirm(`确定要删除建议 #${String(id).padStart(3, '0')} 吗?`)) return;
try {
const r = await fetch('/api/suggestions/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();
suggestionsLoaded = false;
await suggestionsLoadAll();
suggestionsShowToast('建议已删除', 'success');
} else {
alert('删除失败: ' + (result.error || '未知错误'));
}
} catch (e) {
alert('删除失败: ' + e.message);
}
}
function suggestionsShowToast(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);
}
function suggestionsBindFilters() {
const ids = ['suggestions-search', 'suggestions-status-filter', 'suggestions-module-filter'];
ids.forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener(el.tagName === 'SELECT' ? 'change' : 'input', () => suggestionsRenderList());
});
const moduleFilter = document.getElementById('suggestions-module-filter');
if (moduleFilter && moduleFilter.options.length <= 1) {
SUGGESTION_MODULES.forEach(m => {
const opt = document.createElement('option');
opt.value = m;
opt.textContent = m;
moduleFilter.appendChild(opt);
});
}
}
(function () {
const observer = new MutationObserver(() => {
const panel = document.getElementById('panel-suggestions');
if (panel && panel.classList.contains('active') && !suggestionsLoaded) {
suggestionsLoadAll();
suggestionsBindFilters();
}
});
const panel = document.getElementById('panel-suggestions');
if (panel) {
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
}
})();