fix: restore from regex corruption, JS-bind modal close, data-action delegation

This commit is contained in:
2026-07-15 03:53:16 +08:00
parent 80f8342c0b
commit 021351d898
20 changed files with 140 additions and 152 deletions

View File

@@ -22,13 +22,13 @@ const App = {
const el = document.getElementById('sidebar-nav');
if (el) el.insertAdjacentHTML('beforeend', '<div style="color:red;font-size:10px;padding:8px">Error: ' + String(msg).substring(0,80) + '</div>');
};
document.getElementById('modal-close').onclick = () => this.closeModal();
document.getElementById('modal-close-btn').onclick = () => this.closeModal();
document.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]');
if (!action) return;
const [obj, method, ...args] = action.dataset.action.split(':');
const target = window[obj];
if (target && target[method]) target[method](...args);
const btn = e.target.closest('[data-action]');
if (!btn) return;
const parts = btn.dataset.action.split(':');
const target = window[parts[0]];
if (target && target[parts[1]]) target[parts[1]](...parts.slice(2));
});
this.route();
},
@@ -68,7 +68,6 @@ const App = {
if (page === 'install') { this.showPublic('install'); return; }
if (page === 'home') { this.showPublic('home', param); return; }
if (page === 'login' || page === 'register') { this.showPublic(page); return; }
if (page === 'forgot' || page === 'reset') { this.showPublic(page); return; }
if (!Auth.logged()) { location.hash = '#/login'; return; }
this.showLayout();
@@ -104,7 +103,7 @@ const App = {
const pub = document.getElementById('public-page');
pub.classList.remove('hidden');
this.updateTopAuth();
const comp = page === 'login' ? LoginPage : page === 'register' ? RegisterPage : page === 'install' ? InstallPage : page === 'forgot' ? ForgotPage : page === 'reset' ? ResetPage : HomePage;
const comp = page === 'login' ? LoginPage : page === 'register' ? RegisterPage : page === 'install' ? InstallPage : HomePage;
comp.render(param).then(html => { pub.innerHTML = html; if (comp.mount) comp.mount(param); }).catch(() => {
pub.innerHTML = '<div class="pub-card"><div class="logo"><i class="fas fa-exclamation-triangle" style="color:var(--d)"></i><h2>加载失败</h2><p>请刷新页面重试</p></div></div>';
});
@@ -173,8 +172,6 @@ window.PollsPage = PollsPage;
window.FeaturesPage = FeaturesPage;
window.LoginPage = LoginPage;
window.RegisterPage = RegisterPage;
window.ForgotPage = ForgotPage;
window.ResetPage = ResetPage;
window.HomePage = HomePage;
window.InstallPage = InstallPage;
window.U = U;

View File

@@ -3,7 +3,7 @@ const Dashboard = {
async mount() {
const ct = document.getElementById('page-content');
document.getElementById('page-title').textContent = '控ĺ<EFBFBD>¶ĺŹ?;
document.getElementById('page-title').textContent = '控制台';
document.getElementById('page-actions').innerHTML = `
<button class="btn btn-p btn-sm" onclick="App.navigate('tickets','create?type=report')"><i class="fas fa-flag"></i> 举报</button>
<button class="btn btn-s btn-sm" onclick="App.navigate('tickets','create?type=suggestion')"><i class="fas fa-lightbulb"></i> 建议</button>
@@ -25,24 +25,24 @@ const Dashboard = {
ct.innerHTML = `
<div class="stats-grid">
<div class="stat-card"><div class="si" style="background:#eef2ff;color:var(--p)"><i class="fas fa-ticket-alt"></i></div><div><div class="sv">${stats.total}</div><div class="sl">工单总数</div></div></div>
<div class="stat-card"><div class="si" style="background:#fef3c7;color:#d97706"><i class="fas fa-clock"></i></div><div><div class="sv">${byStatus.pending || 0}</div><div class="sl">待处ç<EFBFBD>?/div></div></div>
<div class="stat-card"><div class="si" style="background:#ede9fe;color:#7c3aed"><i class="fas fa-cogs"></i></div><div><div class="sv">${byStatus.processing || 0}</div><div class="sl">处ç<EFBFBD>†ä¸?/div></div></div>
<div class="stat-card"><div class="si" style="background:#d1fae5;color:var(--s)"><i class="fas fa-check-circle"></i></div><div><div class="sv">${byStatus.resolved || 0}</div><div class="sl">已解�/div></div></div>
<div class="stat-card"><div class="si" style="background:#fef3c7;color:#d97706"><i class="fas fa-clock"></i></div><div><div class="sv">${byStatus.pending || 0}</div><div class="sl">待处理</div></div></div>
<div class="stat-card"><div class="si" style="background:#ede9fe;color:#7c3aed"><i class="fas fa-cogs"></i></div><div><div class="sv">${byStatus.processing || 0}</div><div class="sl">处理中</div></div></div>
<div class="stat-card"><div class="si" style="background:#d1fae5;color:var(--s)"><i class="fas fa-check-circle"></i></div><div><div class="sv">${byStatus.resolved || 0}</div><div class="sl">已解决</div></div></div>
</div>
${Auth.isAdmin() && exports ? `
<div class="grid-2">
<div class="card"><div class="card-h">类型分布</div><div class="card-b">
${(exports.byType||[]).map(r => `<div class="detail-row"><span class="lbl">${U.TYPE[r.type]||r.type}</span><span class="val fb">${r.c} ćť?/span></div>`).join('')}
${(exports.byType||[]).map(r => `<div class="detail-row"><span class="lbl">${U.TYPE[r.type]||r.type}</span><span class="val fb">${r.c} 条</span></div>`).join('')}
</div></div>
<div class="card"><div class="card-h">状ć€<EFBFBD>ĺ<EFBFBD>†ĺ¸?/div><div class="card-b">
${(exports.byStatus||[]).map(r => `<div class="detail-row"><span class="lbl">${U.STATUS[r.status]||r.status}</span><span class="val fb">${r.c} ćť?/span></div>`).join('')}
<div class="card"><div class="card-h">状态分布</div><div class="card-b">
${(exports.byStatus||[]).map(r => `<div class="detail-row"><span class="lbl">${U.STATUS[r.status]||r.status}</span><span class="val fb">${r.c} 条</span></div>`).join('')}
</div></div>
</div>
${exports.monthly?.length ? `
<div class="card"><div class="card-h">月度趋势</div><div class="card-b table-wrap"><table><thead><tr><th>月份</th><th>数量</th></tr></thead><tbody>${exports.monthly.map(m=>`<tr><td>${m.m}</td><td>${m.c}</td></tr>`).join('')}</tbody></table></div></div>` : ''}
` : ''}
<p class="tm ts tc">ćŹ<EFBFBD>示: 可在"工单ĺ<E280A2>—表"中查çśĺŚç®ˇç<EFBFBD>ććśĺ·ĄĺŤ?/p>
<p class="tm ts tc">提示: 可在"工单列表"中查看和管理所有工单</p>
`;
}
};

View File

@@ -11,8 +11,8 @@ const ExportPage = {
<div class="card"><div class="card-h"><i class="fas fa-file-csv"></i> 导出工单</div><div class="card-b">
<form id="exp-tickets">
<div class="grid-2"><div class="form-group"><label>类型</label><select id="et-type"><option value="">全部</option>${Object.entries(U.TYPE).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select></div>
<div class="form-group"><label><EFBFBD><EFBFBD>?/label><select id="et-status"><option value=""><EFBFBD><EFBFBD></option>${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select></div></div>
<div class="grid-2"><div class="form-group"><label><EFBFBD>憪𧢲𠯫<EFBFBD>?/label><input type="date" id="et-start"></div><div class="form-group"><label>蝏𤘪<EFBFBD><EFBFBD><EFBFBD></label><input type="date" id="et-end"></div></div>
<div class="form-group"><label>状态</label><select id="et-status"><option value="">全部</option>${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select></div></div>
<div class="grid-2"><div class="form-group"><label>开始日期</label><input type="date" id="et-start"></div><div class="form-group"><label>结束日期</label><input type="date" id="et-end"></div></div>
<div class="form-group"><label>格式</label><select id="et-fmt"><option value="csv">CSV (Excel)</option><option value="json">JSON</option></select></div>
<button type="submit" class="btn btn-p btn-sm"><i class="fas fa-download"></i> 导出</button>
</form>
@@ -28,14 +28,14 @@ const ExportPage = {
<div class="card"><div class="card-h"><i class="fas fa-history"></i> 导出操作日志</div><div class="card-b">
<form id="exp-logs">
<div class="grid-2"><div class="form-group"><label><EFBFBD>憪𧢲𠯫<EFBFBD>?/label><input type="date" id="el-start"></div><div class="form-group"><label>蝏𤘪<EFBFBD><EFBFBD><EFBFBD></label><input type="date" id="el-end"></div></div>
<div class="grid-2"><div class="form-group"><label>开始日期</label><input type="date" id="el-start"></div><div class="form-group"><label>结束日期</label><input type="date" id="el-end"></div></div>
<div class="form-group"><label>格式</label><select id="el-fmt"><option value="csv">CSV (Excel)</option><option value="json">JSON</option></select></div>
<button type="submit" class="btn btn-i btn-sm"><i class="fas fa-download"></i> 导出</button>
</form>
</div></div>
<div class="card"><div class="card-h"><i class="fas fa-chart-bar"></i> 统计报表</div><div class="card-b">
<p class="tm mb-4"><EFBFBD><EFBFBD>蝟餌<EFBFBD><EFBFBD><EFBFBD>蝏蠘恣<EFBFBD><EFBFBD><EFBFBD>踵㺭<EFBFBD>?/p>
<p class="tm mb-4">查看系统整体统计和趋势数据</p>
<button class="btn btn-p btn-sm" onclick="ExportPage.viewStats()"><i class="fas fa-chart-simple"></i> 查看统计</button>
</div></div>
</div>
@@ -77,11 +77,11 @@ const ExportPage = {
<div class="detail-section"><h4>概览</h4>
<div class="detail-row"><span class="lbl">工单总数</span><span class="val fb">${d.total}</span></div>
<div class="detail-row"><span class="lbl">今日新增</span><span class="val fb">${d.today}</span></div>
<div class="detail-row"><span class="lbl"><EFBFBD><EFBFBD><EFBFBD>?/span><span class="val fb">${d.usersTotal}</span></div>
<div class="detail-row"><span class="lbl">用户数</span><span class="val fb">${d.usersTotal}</span></div>
</div>
<div class="detail-section"><h4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>撣?/h4>${(d.byStatus||[]).map(r=>`<div class="detail-row"><span class="lbl">${U.STATUS[r.status]||r.status}</span><span class="val fb">${r.c}</span></div>`).join('')}</div>
<div class="detail-section"><h4>状态分布</h4>${(d.byStatus||[]).map(r=>`<div class="detail-row"><span class="lbl">${U.STATUS[r.status]||r.status}</span><span class="val fb">${r.c}</span></div>`).join('')}</div>
<div class="detail-section"><h4>类型分布</h4>${(d.byType||[]).map(r=>`<div class="detail-row"><span class="lbl">${U.TYPE[r.type]||r.type}</span><span class="val fb">${r.c}</span></div>`).join('')}</div>
${d.monthly?.length?`<div class="detail-section"><h4><EFBFBD><EFBFBD>漲頞见飵</h4>${d.monthly.map(m=>`<div class="detail-row"><span class="lbl">${m.m}</span><span class="val fb">${m.c} <EFBFBD>?/span></div>`).join('')}</div>`:''}
${d.monthly?.length?`<div class="detail-section"><h4>月度趋势</h4>${d.monthly.map(m=>`<div class="detail-row"><span class="lbl">${m.m}</span><span class="val fb">${m.c} 条</span></div>`).join('')}</div>`:''}
`, '600px');
} catch (e) { alert(e.message); }
}

View File

@@ -20,7 +20,7 @@ const FeaturesPage = {
ct.innerHTML = `<div style="display:flex;gap:10px;align-items:center;margin-bottom:16px;flex-wrap:wrap">
<select id="feat-group" onchange="FeaturesPage.setFilter()"><option value="">全部分组</option></select>
<select id="feat-server" onchange="FeaturesPage.setFilter()"><option value="">全部子服</option></select>
<select id="feat-status" onchange="FeaturesPage.setFilter()"><option value="all">全部状�/option><option value="pending">待定</option><option value="planned">计划�/option><option value="done">已完�/option></select>
<select id="feat-status" onchange="FeaturesPage.setFilter()"><option value="all">全部状态</option><option value="pending">待定</option><option value="planned">计划中</option><option value="done">已完成</option></select>
<span style="flex:1"></span>
<button class="btn btn-o btn-sm ${this.sort==='time'?'btn-p':''}" id="sort-time" onclick="FeaturesPage.setSort('time')">时间</button>
<button class="btn btn-o btn-sm ${this.sort==='votes'?'btn-p':''}" id="sort-votes" onclick="FeaturesPage.setSort('votes')">热度</button>
@@ -74,7 +74,7 @@ const FeaturesPage = {
renderItem(i) {
const sc = i.status==='done'?'var(--s)':i.status==='planned'?'var(--p)':'var(--g5)';
const statusLabel = i.status==='done'?'已完æˆ?:i.status==='planned'?'计åˆä¸?:'待定';
const statusLabel = i.status==='done'?'已完成':i.status==='planned'?'计划中':'待定';
return `<div class="card" id="feat-${i.id}">
<div class="card-h">
<span><span style="color:${sc};font-size:18px;margin-right:8px">${i.status==='done'?'\u2713':i.status==='planned'?'\u25C9':'\u25CB'}</span> ${U.esc(i.title)}</span>
@@ -153,9 +153,9 @@ const FeaturesPage = {
U.modal('添加', `
<form id="feat-form">
<div class="grid-2"><div class="form-group"><label>标题 *</label><input id="ft-title" required></div>
<div class="form-group"><label>分组/å­<C3A5>æœ<C3A6></label><select id="ft-gs"><option value="::">æ—?/option>${opts}</select></div></div>
<div class="form-group"><label>分组/子服</label><select id="ft-gs"><option value="::">无</option>${opts}</select></div></div>
<div class="form-group"><label>描述</label><textarea id="ft-desc" rows="3"></textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">添加</button></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">添加</button></div>
</form>
`);
document.getElementById('feat-form').onsubmit = async e => { e.preventDefault();
@@ -171,10 +171,10 @@ const FeaturesPage = {
U.modal('编辑', `
<form id="fe-edit">
<div class="form-group"><label>标题</label><input id="fe-title" value="${title}"></div>
<div class="grid-2"><div class="form-group"><label>状�/label><select id="fe-status"><option value="pending" ${status==='pending'?'selected':''}>待定</option><option value="planned" ${status==='planned'?'selected':''}>计划�/option><option value="done" ${status==='done'?'selected':''}>已完�/option></select></div>
<div class="form-group"><label>分组/å­<C3A5>æœ<C3A6></label><select id="fe-gs"><option value="::">æ—?/option>${opts}</select></div></div>
<div class="grid-2"><div class="form-group"><label>状态</label><select id="fe-status"><option value="pending" ${status==='pending'?'selected':''}>待定</option><option value="planned" ${status==='planned'?'selected':''}>计划中</option><option value="done" ${status==='done'?'selected':''}>已完成</option></select></div>
<div class="form-group"><label>分组/子服</label><select id="fe-gs"><option value="::">无</option>${opts}</select></div></div>
<div class="form-group"><label>描述</label><textarea id="fe-desc" rows="3">${desc}</textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="button" class="btn btn-d" onclick="FeaturesPage.del(${id})"><i class="fas fa-trash"></i></button><button type="submit" class="btn btn-p">ä¿<EFBFBD>å­˜</button></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="button" class="btn btn-d" onclick="FeaturesPage.del(${id})"><i class="fas fa-trash"></i></button><button type="submit" class="btn btn-p">保存</button></div>
</form>
`);
document.getElementById('fe-edit').onsubmit = async e => { e.preventDefault();
@@ -184,5 +184,5 @@ const FeaturesPage = {
};
},
async del(id) { if (!await U.confirm('确认删除�)) return; try { await API.req('DELETE', '/features/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} }
async del(id) { if (!await U.confirm('确认删除?')) return; try { await API.req('DELETE', '/features/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} }
};

View File

@@ -39,19 +39,19 @@ const HomePage = {
this.searchTracking = async () => {
const token = document.getElementById('track-token').value.trim();
if (!token) return alert('霂瑁<EFBFBD><EFBFBD>亥蕭頦芣<EFBFBD>霂?);
if (!token) return alert('请输入追踪标识');
const ct = document.getElementById('track-result');
ct.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i></div>';
try {
const tickets = await API.get(`/tickets/tracking?token=${token}`);
ct.innerHTML = tickets.length ? `
<div class="table-wrap"><table><thead><tr>
<th>#</th><th>蝐餃<EFBFBD></th><th><EFBFBD><EFBFBD><EFBFBD></th><th><EFBFBD><EFBFBD>?/th><th><EFBFBD>園𡢿</th><th><EFBFBD><EFBFBD></th>
<th>#</th><th>类型</th><th>标题</th><th>状态</th><th>时间</th><th>操作</th>
</tr></thead><tbody>${tickets.map(tick => `
<tr><td>${tick.id}</td><td>${U.badge(tick.type,'type')}</td><td>${U.esc(tick.title)}</td>
<td>${U.badge(tick.status,'status')}</td><td>${U.date(tick.created_at)}</td>
<td><button class="btn btn-o btn-sm" onclick="App.navigate('home','tracking');HomePage.showTrackDetail(${tick.id},'${U.escJs(token)}')">查看</button></td></tr>
`).join('')}</tbody></table></div>` : '<div class="empty"><i class="fas fa-inbox"></i><p><EFBFBD>芣𪄳<EFBFBD>啣極<EFBFBD>?/p></div>';
`).join('')}</tbody></table></div>` : '<div class="empty"><i class="fas fa-inbox"></i><p>未找到工单</p></div>';
} catch (ex) { ct.innerHTML = `<div class="alert alert-e">${ex.message}</div>`; }
};
},
@@ -62,20 +62,20 @@ const HomePage = {
U.loading(ct);
API.get(`/tickets/tracking?token=${token}`).then(tickets => {
const t = tickets.find(x => x.id === id);
if (!t) { ct.innerHTML = '<p><EFBFBD>芣𪄳<EFBFBD>?/p>'; return; }
if (!t) { ct.innerHTML = '<p>未找到</p>'; return; }
ct.innerHTML = `
<div class="detail-section"><h4>基本信息</h4>
<div class="detail-row"><span class="lbl">标题</span><span class="val fb">${U.esc(t.title)}</span></div>
<div class="detail-row"><span class="lbl">类型</span><span class="val">${U.badge(t.type,'type')}</span></div>
<div class="detail-row"><span class="lbl"><EFBFBD><EFBFBD>?/span><span class="val">${U.badge(t.status,'status')}</span></div>
<div class="detail-row"><span class="lbl"><EFBFBD>𣂷漱<EFBFBD>?/span><span class="val">${U.esc(t.reporter_game_name)} (UID: ${U.esc(t.reporter_game_uid)})</span></div>
<div class="detail-row"><span class="lbl">状态</span><span class="val">${U.badge(t.status,'status')}</span></div>
<div class="detail-row"><span class="lbl">提交者</span><span class="val">${U.esc(t.reporter_game_name)} (UID: ${U.esc(t.reporter_game_uid)})</span></div>
${t.type==='report'?`
<div class="detail-row"><span class="lbl">被举报人</span><span class="val">${U.esc(t.target_game_name||'?')} (UID: ${U.esc(t.target_game_uid||'?')})</span></div>
<div class="detail-row"><span class="lbl">原因</span><span class="val">${U.esc(t.reason)}</span></div>`:''}
${t.description?`<div class="detail-row"><span class="lbl">详情</span><span class="val">${U.esc(t.description)}</span></div>`:''}
<div class="detail-row"><span class="lbl">时间</span><span class="val">${U.date(t.created_at)}</span></div>
</div>
<p class="tm ts"><EFBFBD><EFBFBD><EFBFBD>𧼮<EFBFBD>嚗諹窈<a href="#/register" style="color:var(--p)">瘜典<EFBFBD>韐血噡</a><3E>𤾸<EFBFBD><F0A4BEB8>娍迨撌亙<E6928C><E4BA99>?/p>`;
<p class="tm ts">如需回复,请<a href="#/register" style="color:var(--p)">注册账号</a>后关联此工单。</p>`;
}).catch(e => { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; });
}
};

View File

@@ -20,15 +20,15 @@ const InstallPage = {
renderDBStep(ct) {
ct.innerHTML = `
<div class="card"><div class="card-h"><i class="fas fa-database"></i> 绗?姝ワ細鏁版嵁搴撻厤缃?/div><div class="card-b">
<div class="card"><div class="card-h"><i class="fas fa-database"></i> 第1步数据库配置</div><div class="card-b">
<form id="install-db">
<div class="grid-2"><div class="form-group"><label>MySQL 主机</label><input id="db-host" value="localhost"></div>
<div class="form-group"><label>端口</label><input id="db-port" value="3306" type="number"></div></div>
<div class="grid-2"><div class="form-group"><label>鐢ㄦ埛鍚?/label><input id="db-user" value="root" required></div>
<div class="grid-2"><div class="form-group"><label>用户名</label><input id="db-user" value="root" required></div>
<div class="form-group"><label>密码</label><input type="password" id="db-pass"></div></div>
<div class="form-group"><label>鏁版嵁搴撳悕</label><input id="db-name" value="mc_report" required><span class="help">濡備笉瀛樺湪灏嗚嚜鍔ㄥ垱寤?/span></div>
<div class="form-group"><label>数据库名</label><input id="db-name" value="mc_report" required><span class="help">如不存在将自动创建</span></div>
<div id="db-err" class="alert alert-e hidden"></div>
<button type="submit" class="btn btn-p"><i class="fas fa-check"></i> 娴嬭瘯杩炴帴骞剁户缁?/button>
<button type="submit" class="btn btn-p"><i class="fas fa-check"></i> 测试连接并继续</button>
</form>
</div></div>
`;
@@ -36,7 +36,7 @@ const InstallPage = {
e.preventDefault();
const btn = e.target.querySelector('button');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 娴嬭瘯涓?..';
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 测试中...';
try {
await API.post('/install/check-db', {
host: document.getElementById('db-host').value,
@@ -57,24 +57,24 @@ const InstallPage = {
document.getElementById('db-err').textContent = ex.message;
document.getElementById('db-err').classList.remove('hidden');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-check"></i> 娴嬭瘯杩炴帴骞剁户缁?;
btn.innerHTML = '<i class="fas fa-check"></i> 测试连接并继续';
}
};
},
renderAdminStep(ct) {
ct.innerHTML = `
<div class="card"><div class="card-h"><i class="fas fa-user-crown"></i> 绗?姝ワ細鍒涘缓鏈嶄富璐﹀彿</div><div class="card-b">
<div class="card"><div class="card-h"><i class="fas fa-user-crown"></i> 第2步创建服主账号</div><div class="card-b">
<form id="install-admin">
<div class="grid-2"><div class="form-group"><label>站点名称</label><input id="ia-sitename" value="MC举报系统"></div>
<div class="form-group"><label>站点地址</label><input id="ia-siteurl" value="${location.origin}"></div></div>
<div class="grid-2"><div class="form-group"><label>鏈嶄富鐢ㄦ埛鍚?*</label><input id="ia-username" required placeholder="鐧诲綍鐢?></div>
<div class="grid-2"><div class="form-group"><label>服主用户名 *</label><input id="ia-username" required placeholder="登录用"></div>
<div class="form-group"><label>邮箱 *</label><input type="email" id="ia-email" required></div></div>
<div class="grid-2"><div class="form-group"><label>密码 *</label><input type="password" id="ia-password" required minlength="6"></div>
<div class="form-group"><label>游戏名称</label><input id="ia-game" placeholder="Minecraft ID"></div></div>
<div class="form-group"><label>游戏UID</label><input id="ia-uid" placeholder="游戏唯一ID"></div>
<div id="ia-err" class="alert alert-e hidden"></div>
<div id="ia-warn" class="alert alert-i"><strong>娉ㄦ剰锛?/strong>瀹夎<EFBFBD>瀹屾垚鍚庨渶瑕侀噸鍚<EFBFBD>湇鍔″櫒鎵嶈兘鍔犺浇涓氬姟璺<EFBFBD>敱銆傝<EFBFBD>璁板ソ璐﹀彿瀵嗙爜銆?/div>
<div id="ia-warn" class="alert alert-i"><strong>注意:</strong>安装完成后需要重启服务器才能加载业务路由。请记好账号密码。</div>
<button type="submit" class="btn btn-p btn-lg w-full"><i class="fas fa-check"></i> 完成安装</button>
</form>
</div></div>
@@ -83,7 +83,7 @@ const InstallPage = {
e.preventDefault();
const btn = e.target.querySelector('button');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <EFBFBD>?..';
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 安装中...';
try {
await API.post('/install/complete', {
...this.dbConfig,
@@ -97,7 +97,7 @@ const InstallPage = {
admin_game_name: document.getElementById('ia-game').value,
admin_game_uid: document.getElementById('ia-uid').value,
});
document.getElementById('install-admin').innerHTML = '<div class="alert alert-s"><h3><i class="fas fa-check-circle"></i> 瀹夎<EFBFBD>鎴愬姛锛?/h3><p>閰嶇疆宸蹭繚瀛樸€傝<E282AC>閲嶅惎鏈嶅姟鍣ㄥ悗鍒锋柊椤甸潰銆?/p></div>';
document.getElementById('install-admin').innerHTML = '<div class="alert alert-s"><h3><i class="fas fa-check-circle"></i> 安装成功!</h3><p>配置已保存。请重启服务器后刷新页面。</p></div>';
} catch (ex) {
document.getElementById('ia-err').textContent = ex.message;
document.getElementById('ia-err').classList.remove('hidden');

View File

@@ -4,7 +4,7 @@ const LoginPage = {
<div class="pub-card">
<div class="logo"><i class="fas fa-shield-halved"></i><h2>欢迎回来</h2><p>登录您的账号</p></div>
<form id="login-form">
<div class="form-group"><input id="lu" placeholder="用户<EFBFBD>? required autofocus></div>
<div class="form-group"><input id="lu" placeholder="用户名" required autofocus></div>
<div class="form-group"><input type="password" id="lp" placeholder="密码" required></div>
<div id="le" class="alert alert-e hidden"></div>
<button type="submit" class="btn btn-p btn-lg w-full"><i class="fas fa-sign-in-alt"></i> 登录</button>
@@ -12,8 +12,6 @@ const LoginPage = {
<div style="margin-top:18px;text-align:center;font-size:13px">
<a href="#/register" style="color:var(--p);text-decoration:none;font-weight:600">还没有账号?立即注册</a>
<span style="color:var(--g4);margin:0 10px">|</span>
<a href="#/forgot" style="color:var(--g5);text-decoration:none">忘记密码</a>
<span style="color:var(--g4);margin:0 10px">|</span>
<a href="#/home" style="color:var(--g5);text-decoration:none">匿名提交</a>
</div>
</div>`;
@@ -24,7 +22,7 @@ const LoginPage = {
e.preventDefault();
const btn = e.target.querySelector('button');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 登录<EFBFBD>?..';
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 登录中...';
try {
await Auth.login(document.getElementById('lu').value.trim(), document.getElementById('lp').value.trim());
App.afterLogin();

View File

@@ -28,14 +28,14 @@ const NotificationsPage = {
showCreate() {
U.modal('添加Webhook', `
<form id="nc-form"><div class="form-group"><label>å<EFBFBD><EFBFBD>ç§° *</label><input id="nc-name" placeholder="å¦? Discord通知" required></div>
<form id="nc-form"><div class="form-group"><label>名称 *</label><input id="nc-name" placeholder="如: Discord通知" required></div>
<div class="form-group"><label>类型 *</label><select id="nc-type"><option value="webhook">Webhook</option></select></div>
<div class="form-group"><label>Webhook URL *</label><input id="nc-url" placeholder="https://discord.com/api/webhooks/..." required></div>
<div class="form-group"><label>触发事件</label>
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:4px">${EventOptions('all')}</div>
<input type="hidden" id="nc-events" value="all">
</div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">创建</button></div></form>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">创建</button></div></form>
`);
bindEventToggles();
document.getElementById('nc-form').onsubmit = async e => { e.preventDefault();
@@ -51,8 +51,8 @@ const NotificationsPage = {
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:4px">${EventOptions(events)}</div>
<input type="hidden" id="ne-events" value="${events}">
</div>
<div class="form-group"><label>状æ€?/label><select id="ne-active"><option value="1" ${active?'selected':''}>å<EFBFBD>¯ç”¨</option><option value="0" ${!active?'selected':''}>ç¦<EFBFBD>用</option></select></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">ä¿<EFBFBD>å­˜</button></div></form>
<div class="form-group"><label>状态</label><select id="ne-active"><option value="1" ${active?'selected':''}>启用</option><option value="0" ${!active?'selected':''}>禁用</option></select></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">保存</button></div></form>
`);
bindEventToggles();
document.getElementById('ne-form').onsubmit = async e => { e.preventDefault();
@@ -61,7 +61,7 @@ const NotificationsPage = {
},
async test(id) { try { alert((await API.post(`/notifications/${id}/test`)).message); } catch(ex){alert(ex.message);} },
async del(id) { if(await U.confirm('确认删除此通知é…<EFBFBD>ç½®ï¼?)){ try { await API.req('DELETE',`/notifications/${id}`); this.load(); } catch(ex){alert(ex.message);} } }
async del(id) { if(await U.confirm('确认删除此通知配置?')){ try { await API.req('DELETE',`/notifications/${id}`); this.load(); } catch(ex){alert(ex.message);} } }
};
function EventOptions(current) { return ['all','ticket_created','ticket_claimed','ticket_transferred','ticket_updated'].map(e=>`<label style="cursor:pointer;font-size:12px"><input type="checkbox" value="${e}" class="ev-check" ${current.split(',').map(s=>s.trim()).includes(e)?'checked':''}> ${EventsLabelSingle(e)}</label>`).join(''); }

View File

@@ -32,16 +32,16 @@ const PollsPage = {
const opts = p.options || [];
const timed = p.start_time || p.end_time;
return `<div class="card" id="poll-${p.id}">
<div class="card-h"><span>${U.esc(p.server_name)} �${U.esc(p.title)}</span>
<div class="card-h"><span>${U.esc(p.server_name)} ${U.esc(p.title)}</span>
<span style="display:flex;gap:8px;align-items:center">
${timed ? `<span class="ts tm">${p.start_time?'� '+U.date(p.start_time):''} ${p.end_time?'� '+U.date(p.end_time):''}</span>`:''}
<span class="ts tm">${p.total||0} �/span>
${timed ? `<span class="ts tm">${p.start_time?'起: '+U.date(p.start_time):''} ${p.end_time?'至: '+U.date(p.end_time):''}</span>`:''}
<span class="ts tm">${p.total||0} 票</span>
${Auth.isOwner() ? `<button class="btn btn-o btn-sm" onclick="PollsPage.showEdit(${p.id})"><i class="fas fa-edit"></i></button>` : ''}
</span>
</div>
<div class="card-b">
${p.description?`<p class="tm mb-4">${U.esc(p.description)}</p>`:''}
${p.voted ? `<div class="alert alert-s"><i class="fas fa-check-circle"></i> 已投�/div>` : ''}
${p.voted ? `<div class="alert alert-s"><i class="fas fa-check-circle"></i> 已投票</div>` : ''}
<div style="display:flex;flex-direction:column;gap:8px">
${opts.map((o,i) => {
const v = p.voted ? 0 : (p.votes?.[i] || 0);
@@ -51,7 +51,7 @@ const PollsPage = {
${!p.voted ? `<div style="position:absolute;top:0;left:0;bottom:0;width:${pct}%;background:${my?'var(--pl)':'var(--g2)'};transition:width .3s;z-index:0"></div>` : ''}
<div style="position:relative;z-index:1;display:flex;justify-content:space-between;align-items:center;padding:10px 14px;font-size:13px">
<span>${my?'<i class="fas fa-check" style="color:var(--p)"></i> ':''}${U.esc(o)}</span>
${!p.voted ? `<span class="fb">${v}�${pct}%</span>` : ''}
${!p.voted ? `<span class="fb">${v}${pct}%</span>` : ''}
</div>
</div>`;
}).join('')}
@@ -79,17 +79,17 @@ const PollsPage = {
<div class="grid-2"><div class="form-group"><label>分组/子服 *</label><select id="pl-gs"><option value="">请选择</option>${opts}</select></div>
<div class="form-group"><label>标题 *</label><input id="pl-title" required></div></div>
<div class="form-group"><label>描述</label><textarea id="pl-desc" rows="2"></textarea></div>
<div class="grid-2"><div class="form-group"><label>开始时é—?å<>¯é€?</label><input type="datetime-local" id="pl-start"></div>
<div class="form-group"><label>结æ<EFBFBD>Ÿæ—¶é—´<>¯é€?</label><input type="datetime-local" id="pl-end"></div></div>
<div class="form-group"><label>选项 (æ¯<C3A6>行一个,至尸? *</label><textarea id="pl-opts" rows="4" placeholder="选项A&#10;选项B&#10;选项C" required></textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">创建</button></div>
<div class="grid-2"><div class="form-group"><label>开始时间(可选)</label><input type="datetime-local" id="pl-start"></div>
<div class="form-group"><label>结束时间(可选)</label><input type="datetime-local" id="pl-end"></div></div>
<div class="form-group"><label>选项 (每行一个至少2个) *</label><textarea id="pl-opts" rows="4" placeholder="选项A&#10;选项B&#10;选项C" required></textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">创建</button></div>
</form>
`);
document.getElementById('poll-form').onsubmit = async e => {
e.preventDefault();
const [group_name, server_name] = document.getElementById('pl-gs').value.split('::');
const opts = document.getElementById('pl-opts').value.split('\n').map(s=>s.trim()).filter(s=>s);
if (opts.length < 2) return alert('至少需�个选项');
if (opts.length < 2) return alert('至少需要2个选项');
try {
await API.post('/polls', { title: document.getElementById('pl-title').value, description: document.getElementById('pl-desc').value, options: opts, group_name, server_name, start_time: document.getElementById('pl-start').value||null, end_time: document.getElementById('pl-end').value||null });
App.closeModal(); this.load();
@@ -107,7 +107,7 @@ const PollsPage = {
<button class="btn btn-d btn-sm" onclick="PollsPage.delGroup(${g.id})"><i class="fas fa-trash"></i></button>
</div>`).join('');
U.modal('管理分组', `
<form id="gm-form"><div class="grid-2"><div class="form-group"><label>分组å<EFBFBD>?/label><input id="gm-group" required></div><div class="form-group"><label>å­<EFBFBD>æœ<EFBFBD>å<EFBFBD>?/label><input id="gm-server" required></div></div>
<form id="gm-form"><div class="grid-2"><div class="form-group"><label>分组名</label><input id="gm-group" required></div><div class="form-group"><label>子服名</label><input id="gm-server" required></div></div>
<button type="submit" class="btn btn-p btn-sm mb-4"><i class="fas fa-plus"></i> 添加</button></form>
<div>${list||'<p class="tm">暂无</p>'}</div>
`);
@@ -124,10 +124,10 @@ const PollsPage = {
document.getElementById('modal-title').textContent = '编辑投票';
document.getElementById('modal-body').innerHTML = `
<form id="pe-form">
<div class="form-group"><label>状æ€?/label><select id="pe-active"><option value="1" ${p.active?'selected':''}>å<EFBFBD>¯ç”¨</option><option value="0" ${!p.active?'selected':''}>关闭</option></select></div>
<div class="form-group"><label>状态</label><select id="pe-active"><option value="1" ${p.active?'selected':''}>启用</option><option value="0" ${!p.active?'selected':''}>关闭</option></select></div>
<div class="form-group"><label>标题</label><input id="pe-title" value="${U.esc(p.title)}"></div>
<div class="form-group"><label>选项 (æ¯<C3A6>行一ä¸?</label><textarea id="pe-opts" rows="4">${U.esc((JSON.parse(p.options||'[]')).join('\n'))}</textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="button" class="btn btn-d" onclick="PollsPage.deletePoll(${id})"><i class="fas fa-trash"></i></button><button type="submit" class="btn btn-p">ä¿<EFBFBD>å­˜</button></div>
<div class="form-group"><label>选项 (每行一个)</label><textarea id="pe-opts" rows="4">${U.esc((JSON.parse(p.options||'[]')).join('\n'))}</textarea></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="button" class="btn btn-d" onclick="PollsPage.deletePoll(${id})"><i class="fas fa-trash"></i></button><button type="submit" class="btn btn-p">保存</button></div>
</form>
`;
document.getElementById('modal-overlay').classList.remove('hidden');
@@ -138,5 +138,5 @@ const PollsPage = {
};
},
async deletePoll(id) { if (!await U.confirm('确认删除�)) return; try { await API.req('DELETE', '/polls/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} }
async deletePoll(id) { if (!await U.confirm('确认删除?')) return; try { await API.req('DELETE', '/polls/'+id); App.closeModal(); this.load(); } catch(ex){alert(ex.message);} }
};

View File

@@ -8,29 +8,29 @@ const RegisterPage = {
<div class="logo"><i class="fas fa-user-plus"></i><h2>创建账号</h2><p>加入MC举报系统</p></div>
<form id="reg-form">
<div class="grid-2">
<div class="form-group"><label>ç«™ç¹ç”¨æˆ·å<EFBFBD>?*</label><input id="ru" required placeholder="登录ç”?></div>
<div class="form-group"><label>站点用户名 *</label><input id="ru" required placeholder="登录用"></div>
<div class="form-group"><label>邮箱 *</label><input type="email" id="re" required placeholder="接收通知"></div>
</div>
<div class="grid-2">
<div class="form-group"><label>游戏名称 *</label><input id="rgn" required placeholder="Minecraft ID"></div>
<div class="form-group"><label>游戏UID *</label><input id="rgu" required placeholder="游戏唯一ID"></div>
</div>
<div class="form-group"><label>密ç <EFBFBD> *</label><input type="password" id="rp" required placeholder="至å°6ä½?></div>
<div class="form-group"><label>密码 *</label><input type="password" id="rp" required placeholder="至少6位"></div>
${this.captcha ? `
<div class="form-group"><label>验è¯<EFBFBD>ç ?/label>
<div class="form-group"><label>验证码</label>
<div style="display:flex;align-items:center;gap:10px">
<div id="captcha-img" style="flex-shrink:0;border:1px solid var(--g200);border-radius:var(--r);overflow:hidden;cursor:pointer" onclick="RegisterPage.refreshCaptcha()" title="点击刷新">${this.captcha.svg}</div>
<input type="text" id="captcha-a" placeholder="输入图片字符" required style="max-width:100px">
<input type="hidden" id="captcha-id" value="${this.captcha.id}">
</div>
<div class="help">ç¹å‡»å¾ç‰‡åˆ·æ°éªŒè¯<EFBFBD>ç ?/div>
<div class="help">点击图片刷新验证码</div>
</div>` : ''}
<div id="regerr" class="alert alert-e hidden"></div>
<div id="regok" class="alert alert-s hidden"></div>
<button type="submit" class="btn btn-p btn-lg w-full"><i class="fas fa-user-plus"></i> 注册</button>
</form>
<div style="margin-top:14px;text-align:center;font-size:13px">
<a href="#/login" style="color:var(--p);text-decoration:none;font-weight:600">已有账å<EFBFBD>·ï¼Ÿç™»å½?/a>
<a href="#/login" style="color:var(--p);text-decoration:none;font-weight:600">已有账号?登录</a>
<span style="color:var(--g4);margin:0 10px">|</span>
<a href="#/home" style="color:var(--g5);text-decoration:none">匿名提交</a>
</div>
@@ -55,7 +55,7 @@ const RegisterPage = {
err.classList.add('hidden');
ok.classList.add('hidden');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 注册�..';
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 注册中...';
try {
await Auth.register({

View File

@@ -13,19 +13,19 @@ const SettingsPage = {
<div class="grid-2"><div class="form-group"><label>站点名称</label><input id="s-name" value="${U.esc(settings.site_name?.value||'')}"></div>
<div class="form-group"><label>站点地址</label><input id="s-url" value="${U.esc(settings.site_url?.value||'')}"></div></div>
<div class="grid-2"><div class="form-group"><label>页尾版权</label><input id="s-copyright" value="${U.esc(settings.copyright?.value||'')}" placeholder="&copy; 2024 MC举报系统"></div>
<div class="form-group"><label>ICP备案å<EFBFBD>?/label><input id="s-icp" value="${U.esc(settings.icp?.value||'')}" placeholder="沪ICP备XXXXXXXXå<EFBFBD>?></div></div>
<div class="form-group"><label>ICP备案号</label><input id="s-icp" value="${U.esc(settings.icp?.value||'')}" placeholder="沪ICP备XXXXXXXX号"></div></div>
<button type="submit" class="btn btn-p btn-sm"><i class="fas fa-save"></i> 保存站点设置</button>
</form>
</div></div>
<div class="card"><div class="card-h">SMTP 邮件设置</div><div class="card-b">
<form id="smtp-form">
<div class="grid-2"><div class="form-group"><label>SMTPæœ<EFBFBD>务å™?/label><input id="s-host" value="${U.esc(settings.smtp_host?.value||'')}"></div>
<div class="grid-2"><div class="form-group"><label>SMTP服务器</label><input id="s-host" value="${U.esc(settings.smtp_host?.value||'')}"></div>
<div class="form-group"><label>端口</label><input id="s-port" value="${U.esc(settings.smtp_port?.value||'587')}"></div></div>
<div class="grid-2"><div class="form-group"><label>用户å<EFBFBD>?/label><input id="s-user" value="${U.esc(settings.smtp_user?.value||'')}"></div>
<div class="grid-2"><div class="form-group"><label>用户名</label><input id="s-user" value="${U.esc(settings.smtp_user?.value||'')}"></div>
<div class="form-group"><label>密码</label><input type="password" id="s-pass" value="${U.esc(settings.smtp_pass?.value||'')}"></div></div>
<div class="grid-2"><div class="form-group"><label>发件地址</label><input id="s-from" value="${U.esc(settings.smtp_from?.value||'')}"></div>
<div class="form-group"><label>SSL</label><select id="s-secure"><option value="0" ${settings.smtp_secure?.value!=='1'?'selected':''}>å<EFBFBD>?587)</option><option value="1" ${settings.smtp_secure?.value==='1'?'selected':''}>æ˜?465)</option></select></div></div>
<div class="form-group"><label>SSL</label><select id="s-secure"><option value="0" ${settings.smtp_secure?.value!=='1'?'selected':''}>否(587)</option><option value="1" ${settings.smtp_secure?.value==='1'?'selected':''}>是(465)</option></select></div></div>
<button type="submit" class="btn btn-p btn-sm"><i class="fas fa-save"></i> 保存SMTP设置</button>
</form>
</div></div>
@@ -38,9 +38,9 @@ const SettingsPage = {
site_name: {value: document.getElementById('s-name').value, label:'站点名称'},
site_url: {value: document.getElementById('s-url').value, label:'站点地址'},
copyright: {value: document.getElementById('s-copyright').value, label:'页尾版权'},
icp: {value: document.getElementById('s-icp').value, label:'ICP备案å<EFBFBD>?},
icp: {value: document.getElementById('s-icp').value, label:'ICP备案号'},
});
alert('å·²ä¿<EFBFBD>å­?);
alert('已保存');
} catch (ex) { alert(ex.message); }
};
@@ -48,14 +48,14 @@ const SettingsPage = {
e.preventDefault();
try {
await API.put('/settings/settings', {
smtp_host: {value: document.getElementById('s-host').value, label:'SMTPæœ<EFBFBD>务å™?},
smtp_host: {value: document.getElementById('s-host').value, label:'SMTP服务器'},
smtp_port: {value: document.getElementById('s-port').value, label:'SMTP端口'},
smtp_secure: {value: document.getElementById('s-secure').value, label:'SSL'},
smtp_user: {value: document.getElementById('s-user').value, label:'SMTPç¨æˆ·å<EFBFBD>?},
smtp_user: {value: document.getElementById('s-user').value, label:'SMTP用户名'},
smtp_pass: {value: document.getElementById('s-pass').value, label:'SMTP密码'},
smtp_from: {value: document.getElementById('s-from').value, label:'发件地址'},
});
alert('å·²ä¿<EFBFBD>å­?);
alert('已保存');
} catch (ex) { alert(ex.message); }
};
} catch (e) {

View File

@@ -29,7 +29,7 @@ const TemplatesPage = {
<div class="form-group"><label>邮件主题</label><input id="et-subject" value="${U.esc(t.subject).replace(/"/g,'&quot;')}" required></div>
<div class="form-group"><label>邮件正文 (HTML)</label><textarea id="et-body" rows="14" required>${U.esc(t.body).replace(/"/g,'&quot;')}</textarea></div>
<div class="alert alert-i ts">可用变量: ${t.description?.match(/\{\{[^}]+\}\}/g)?.join(', ') || '{{site_name}}'}</div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">取消</button><button type="submit" class="btn btn-p">保存</button></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">保存</button></div>
</form>
`, '800px');
document.getElementById('et-form').onsubmit = async e => {

View File

@@ -34,38 +34,38 @@ const TicketCreate = {
${isReport ? `
<div class="grid-2">
<div class="form-group"><label>å¯¹æ¹æ¸¸æˆ<EFBFBD>å<EFBFBD><EFBFBD>ç§° (至少一é¡?</label><input name="target_game_name" placeholder="被举报者Minecraft ID"></div>
<div class="form-group"><label>å¯¹æ¹æ¸¸æˆ<EFBFBD>UID (至少一é¡?</label><input name="target_game_uid" placeholder="被举报者UID"></div>
<div class="form-group"><label>对方游戏名称 (至少一项)</label><input name="target_game_name" placeholder="被举报者Minecraft ID"></div>
<div class="form-group"><label>对方游戏UID (至少一项)</label><input name="target_game_uid" placeholder="被举报者UID"></div>
</div>
<div class="form-group"><label>举报原因 *</label>
<input name="reason" list="reason-list" required placeholder="选择或自行输入举报原�>
<input name="reason" list="reason-list" required placeholder="选择或自行输入举报原因">
<datalist id="reason-list">
<option value="破坏建筑"><option value="盗窃物品"><option value="外挂作弊">
<option value="语言骚扰"><option value="恶意PVP"><option value="利用漏洞">
<option value="其他违规"><option value="诈骗"><option value="恶意辱骂">
</datalist>
</div>
<div class="form-group"><label>详细æ<EFBFBD><EFBFBD>è¿°</label><textarea name="description" rows="4" placeholder="请详细æ<EFBFBD><EFBFBD>è¿°äºä»¶ç»<EFBFBD>è¿?.."></textarea></div>
<div class="form-group"><label>详细描述</label><textarea name="description" rows="4" placeholder="请详细描述事件经过..."></textarea></div>
<div class="form-group" style="display:flex;align-items:center;gap:8px;margin-top:12px">
<input type="checkbox" name="is_admin_complaint" value="1" id="cb-admin" style="width:auto">
<label for="cb-admin" style="margin:0;font-weight:500;text-transform:none;letter-spacing:0;color:var(--d)"><i class="fas fa-exclamation-triangle"></i> 投诉管理员(仅服主可见)</label>
</div>
` : isAppeal ? `
<div class="form-group"><label>申诉ç<EFBFBD>†ç”± *</label><input name="reason" required placeholder="å¦ï¼šè¯¯å°<EFBFBD>ã€<EFBFBD>è¯<EFBFBD>æ<EFBFBD>®ä¸<EFBFBD>足被å°<EFBFBD>ã€<EFBFBD>è¿<EFBFBD>规行为已纠正ç­?></div>
<div class="form-group"><label>详细说明 *</label><textarea name="description" rows="5" placeholder="请详细说明情况,包æ¬è¢«å°<EFBFBD>æ—¶é—´ã€<EFBFBD>å<EFBFBD>¯èƒ½åŽŸå ã€<EFBFBD>为什么应解å°<EFBFBD>ç­?.." required></textarea></div>
<div class="form-group"><label>申诉理由 *</label><input name="reason" required placeholder="如:误封、证据不足被封、违规行为已纠正等"></div>
<div class="form-group"><label>详细说明 *</label><textarea name="description" rows="5" placeholder="请详细说明情况,包括被封时间、可能原因、为什么应解封等..." required></textarea></div>
` : `
<div class="form-group"><label>建议内容 *</label><textarea name="description" rows="5" placeholder="请详细æ<EFBFBD><EFBFBD>è¿°æ¨çš„建è®?.." required></textarea></div>
<div class="form-group"><label>建议内容 *</label><textarea name="description" rows="5" placeholder="请详细描述您的建议..." required></textarea></div>
`}
</div>
</div>
<div class="card">
<div class="card-h">上传文件 (图片/视é¢ï¼Œæ¯<C3A6>个最å¤?0MB)</div>
<div class="card-h">上传文件 (图片/视频每个最大20MB)</div>
<div class="card-b">
<div class="file-upload" id="file-drop">
<i class="fas fa-cloud-upload-alt"></i>
<p>点击选择文件或拖拽到此处</p>
<p class="help">支æŒ<EFBFBD>: JPG/PNG/GIF/WebP/MP4/WebM &nbsp; æ¯<EFBFBD>个最å¤?0MB</p>
<p class="help">支持: JPG/PNG/GIF/WebP/MP4/WebM &nbsp; 每个最大50MB</p>
</div>
<input type="file" id="file-input" multiple accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm" style="display:none">
<div class="file-list" id="file-list"></div>
@@ -104,7 +104,7 @@ const TicketCreate = {
for (const f of fileList) {
if (!allowed.includes(f.type)) { alert(`不支持的文件类型: ${f.name}`); continue; }
if (f.size > 50 * 1024 * 1024) { alert(`文件过大: ${f.name}`); continue; }
if (this.files.length >= 5) { alert('最�个文�); break; }
if (this.files.length >= 5) { alert('最多5个文件'); break; }
this.files.push(f);
}
this.refreshFileList(listEl);
@@ -137,13 +137,13 @@ const TicketCreate = {
const btn = form.querySelector('[type="submit"]');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> æ<EFBFBD><EFBFBD>交ä¸?..';
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 提交中...';
try {
const res = await API.req('POST', '/tickets', fd);
if (res.tracking_token) {
document.cookie = `tracking_token=${res.tracking_token};path=/;max-age=${365*24*3600};SameSite=Lax`;
}
alert('æ<EFBFBD><EFBFBD>交æˆ<EFBFBD>功ï¼<EFBFBD>å·¥å<EFBFBD>•ç¼å<EFBFBD>? #' + res.id);
alert('提交成功!工单编号: #' + res.id);
if (Auth.logged()) App.navigate('tickets');
else App.navigate('home');
} catch (ex) {

View File

@@ -6,7 +6,7 @@ const TicketDetail = {
async mount() {
const id = this.id;
document.getElementById('page-title').textContent = `工单 #${id}`;
document.getElementById('page-actions').innerHTML = `<button class="btn btn-o btn-sm" data-action="App:navigate:tickets"><i class="fas fa-arrow-left"></i> 返回列表</button>`;
document.getElementById('page-actions').innerHTML = `<button class="btn btn-o btn-sm" onclick="App.navigate('tickets')"><i class="fas fa-arrow-left"></i> 返回列表</button>`;
try { this.renderDetail(document.getElementById('page-content'), await API.get(`/tickets/${id}`)); } catch (e) { document.getElementById('page-content').innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
},
@@ -23,7 +23,7 @@ const TicketDetail = {
<div class="detail-grid"><div>
<div class="card"><div class="card-h"><span>基本信息</span><div>${U.badge(t.status,'status')} ${U.badge(t.type,'type')} ${U.badge(t.priority,'priority')}</div></div><div class="card-b">
<div class="detail-row"><span class="lbl">标题</span><span class="val fb">${U.esc(t.title)}</span></div>
<div class="detail-row"><span class="lbl">æ<EFBFBD><EFBFBD>交è€?/span><span class="val">${U.esc(t.reporter_game_name)} (UID: ${U.esc(t.reporter_game_uid)})</span></div>
<div class="detail-row"><span class="lbl">提交者</span><span class="val">${U.esc(t.reporter_game_name)} (UID: ${U.esc(t.reporter_game_uid)})</span></div>
${isRep?`<div class="detail-row"><span class="lbl">被举报人</span><span class="val">${U.esc(t.target_game_name||'?')} (UID: ${U.esc(t.target_game_uid||'?')})</span></div>
<div class="detail-row"><span class="lbl">原因</span><span class="val">${U.esc(t.reason)}</span></div>`:''}
${isAppeal?`<div class="detail-row"><span class="lbl">申诉理由</span><span class="val">${U.esc(t.reason)}</span></div>`:''}
@@ -35,42 +35,42 @@ const TicketDetail = {
${t.claim_note?`<div class="card"><div class="card-h">处理备注</div><div class="card-b" style="white-space:pre-wrap">${U.esc(t.claim_note)}</div></div>`:''}
${t.parent_ticket?`<div class="card"><div class="card-h">关联工单</div><div class="card-b">
<div class="detail-row"><span class="lbl">原工å<EFBFBD>?/span><span class="val"><a href="#" onclick="App.navigate('ticket',${t.parent_ticket.id});return false" style="color:var(--p)">#${t.parent_ticket.id} ${U.esc(t.parent_ticket.title)}</a> â€?${U.badge(t.parent_ticket.status,'status')}</span></div>
<div class="detail-row"><span class="lbl">原工单</span><span class="val"><a href="#" onclick="App.navigate('ticket',${t.parent_ticket.id});return false" style="color:var(--p)">#${t.parent_ticket.id} ${U.esc(t.parent_ticket.title)}</a> ${U.badge(t.parent_ticket.status,'status')}</span></div>
</div></div>`:''}
${t.attachments?.length?`<div class="card"><div class="card-h">附件 (${t.attachments.length})</div><div class="card-b" style="display:flex;flex-wrap:wrap;gap:10px">
${t.attachments.map(a=>a.mime_type.startsWith('image')?`<a href="/api/uploads/${a.stored_name}" target="_blank"><img src="/api/uploads/${a.stored_name}" style="max-width:150px;max-height:120px;border-radius:6px;border:1px solid var(--g200)" title="${U.esc(a.original_name)}"></a>`:`<a href="/api/uploads/${a.stored_name}" target="_blank" class="btn btn-o btn-sm"><i class="fas fa-video"></i> ${U.esc(a.original_name)}</a>`).join('')}
</div></div>`:''}
<div class="card"><div class="card-h">沟通记�/div><div class="card-b">
<div class="card"><div class="card-h">沟通记录</div><div class="card-b">
${t.responses?.length?`<div class="timeline">${t.responses.map(r=>`<div class="timeline-item ${r.is_staff?'staff':''}"><div class="th"><span class="tn">${U.esc(r.username||'系统')}</span>${r.role?`<span class="badge bg-${r.role}">${U.ROLE[r.role]}</span>`:''}<span>${U.date(r.created_at)}</span></div><div class="tc">${U.esc(r.content)}</div></div>`).join('')}</div>`:'<p class="tm">暂无记录</p>'}
</div></div>
<div class="card"><div class="card-h">${isStaff?'回复 (公开)':'补充信息'}</div><div class="card-b">
<form id="reply-form"><div class="form-group"><textarea id="reply-content" rows="3" placeholder="${isStaff?'输入åžå¤<EFBFBD>内容...':'补充说明...'}" required></textarea></div><button type="submit" class="btn btn-p btn-sm"><i class="fas fa-reply"></i> å<EFBFBD>é€?/button></form>
<form id="reply-form"><div class="form-group"><textarea id="reply-content" rows="3" placeholder="${isStaff?'输入回复内容...':'补充说明...'}" required></textarea></div><button type="submit" class="btn btn-p btn-sm"><i class="fas fa-reply"></i> 发送</button></form>
</div></div>
</div>
<div>
<div class="card"><div class="card-h">处理信息</div><div class="card-b">
<div class="detail-row"><span class="lbl">提交账号</span><span class="val">${t.submitter?`${U.esc(t.submitter)} (${U.esc(t.user_game_name||'')})`:'匿名'}</span></div>
<div class="detail-row"><span class="lbl">负责�/span><span class="val">${t.assignee_name?`<span class="badge bg-admin">${U.esc(t.assignee_name)}</span>`:'<span class="tm">待认�/span>'}</span></div>
<div class="detail-row"><span class="lbl">负责人</span><span class="val">${t.assignee_name?`<span class="badge bg-admin">${U.esc(t.assignee_name)}</span>`:'<span class="tm">待认领</span>'}</span></div>
${t.claimed_at?`<div class="detail-row"><span class="lbl">认领时间</span><span class="val">${U.date(t.claimed_at)}</span></div>`:''}
</div></div>
${isStaff?`
<div class="card"><div class="card-h">管理操作</div><div class="card-b" style="display:flex;flex-direction:column;gap:6px">
${canClaim?`<button class="btn btn-s btn-sm w-full" data-action="TicketDetail:claimTicket:${t.id}"><i class="fas fa-hand-paper"></i> 接å<EFBFBD>•认领</button>`:''}
${isMyTicket?`<button class="btn btn-p btn-sm w-full" data-action="TicketDetail:editNote:${t.id}"><i class="fas fa-pen"></i> æ´æ°å¤„ç<EFBFBD>†å¤‡æ³¨</button>`:''}
${canTransfer?`<button class="btn btn-w btn-sm w-full" data-action="TicketDetail:transferTicket:${t.id}"><i class="fas fa-exchange-alt"></i> 转å<EFBFBD></button>`:''}
${isOwner||isMyTicket?`<button class="btn btn-o btn-sm w-full" data-action="TicketDetail:changeStatus:${t.id}"><i class="fas fa-tag"></i> å<EFBFBD>˜æ´çжæ€?/button>`:''}
${canClaim?`<button class="btn btn-s btn-sm w-full" onclick="TicketDetail.claimTicket(${t.id})"><i class="fas fa-hand-paper"></i> 接单认领</button>`:''}
${isMyTicket?`<button class="btn btn-p btn-sm w-full" onclick="TicketDetail.editNote(${t.id})"><i class="fas fa-pen"></i> 更新处理备注</button>`:''}
${canTransfer?`<button class="btn btn-w btn-sm w-full" onclick="TicketDetail.transferTicket(${t.id})"><i class="fas fa-exchange-alt"></i> 转单</button>`:''}
${isOwner||isMyTicket?`<button class="btn btn-o btn-sm w-full" onclick="TicketDetail.changeStatus(${t.id})"><i class="fas fa-tag"></i> 变更状态</button>`:''}
</div></div>
`:''}
${!isStaff && t.user_id === Auth.user().id && ['resolved','rejected'].includes(t.status) && t.type !== 'result_appeal' ? `
<div class="card"><div class="card-h">结果申诉</div><div class="card-b">
<p class="tm mb-4">对处ç<EFBFBD>†ç»“果有å¼è®®ï¼Ÿå<EFBFBD>¯ä»¥æ<EFBFBD><EFBFBD>交一次申诉,由æœ<EFBFBD>主å¤<EFBFBD>æ ¸ã€?/p>
<button class="btn btn-w btn-sm w-full" data-action="TicketDetail:appealResult:${t.id}"><i class="fas fa-gavel"></i> 对结果æ<EFBFBD><EFBFBD>出申è¯?/button>
<p class="tm mb-4">对处理结果有异议?可以提交一次申诉,由服主复核。</p>
<button class="btn btn-w btn-sm w-full" onclick="TicketDetail.appealResult(${t.id})"><i class="fas fa-gavel"></i> 对结果提出申诉</button>
</div></div>`:''}
${t.transfers?.length?`
@@ -86,7 +86,7 @@ const TicketDetail = {
},
async claimTicket(id) {
U.modal('接å<EFBFBD>•认领', `<form id="claim-form"><div class="form-group"><label>åˆ<EFBFBD>å§åžå¤<EFBFBD> (玩家å<C2B6>¯è§<C3A8>)</label><textarea id="claim-reply" rows="3" placeholder="接å<EFBFBD>•æ—¶å<EFBFBD>é€<EFBFBD>给玩家的åžå¤<EFBFBD>,å¦ï¼šå·²æ”¶åˆ°ä¸¾æŠ¥ï¼Œæ­£åœ¨æ ¸æŸ¥ä¸?.."></textarea></div><div class="form-group"><label>处ç<EFBFBD>†å¤‡æ³¨ (内部)</label><textarea id="claim-note" rows="2" placeholder="内部处ç<EFBFBD>†å¤‡æ³¨ï¼Œçީ家ä¸<EFBFBD>å<EFBFBD>¯è§<EFBFBD>"></textarea></div><div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-s"><i class="fas fa-hand-paper"></i> 确认接å<EFBFBD></button></div></form>`);
U.modal('接单认领', `<form id="claim-form"><div class="form-group"><label>初始回复 (玩家可见)</label><textarea id="claim-reply" rows="3" placeholder="接单时发送给玩家的回复,如:已收到举报,正在核查中..."></textarea></div><div class="form-group"><label>处理备注 (内部)</label><textarea id="claim-note" rows="2" placeholder="内部处理备注,玩家不可见"></textarea></div><div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-s"><i class="fas fa-hand-paper"></i> 确认接单</button></div></form>`);
document.getElementById('claim-form').onsubmit = async e => { e.preventDefault();
try { await API.post(`/tickets/${id}/claim`, {claim_note: document.getElementById('claim-note').value, initial_reply: document.getElementById('claim-reply').value}); App.closeModal(); this.mount(); } catch(ex){alert(ex.message);}
};
@@ -94,7 +94,7 @@ const TicketDetail = {
async editNote(id) {
const t = await API.get(`/tickets/${id}`);
U.modal('æ´æ°å¤„ç<EFBFBD>†å¤‡æ³¨', `<form id="note-form"><div class="form-group"><textarea id="upd-note" rows="4">${U.esc(t.claim_note||'')}</textarea></div><div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">ä¿<EFBFBD>å­˜</button></div></form>`);
U.modal('更新处理备注', `<form id="note-form"><div class="form-group"><textarea id="upd-note" rows="4">${U.esc(t.claim_note||'')}</textarea></div><div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">保存</button></div></form>`);
document.getElementById('note-form').onsubmit = async e => { e.preventDefault();
try { await API.put(`/tickets/${id}`, {claim_note: document.getElementById('upd-note').value}); App.closeModal(); this.mount(); } catch(ex){alert(ex.message);}
};
@@ -103,14 +103,14 @@ const TicketDetail = {
async transferTicket(id) {
const users = await API.get('/users');
const staff = users.filter(u => (u.role==='owner'||u.role==='admin') && u.id !== Auth.user().id);
U.modal('转å<EFBFBD>', `<form id="tf-form"><div class="form-group"><label>转交ç»?/label><select id="tf-to">${staff.map(u=>`<option value="${u.id}">${U.esc(u.game_name||u.username)} (${U.esc(u.username)})</option>`).join('')}</select></div><div class="form-group"><label>转交原因</label><textarea id="tf-reason" rows="2" placeholder="说明转交原因..."></textarea></div><div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-w"><i class="fas fa-exchange-alt"></i> 确认转交</button></div></form>`);
U.modal('转单', `<form id="tf-form"><div class="form-group"><label>转交给</label><select id="tf-to">${staff.map(u=>`<option value="${u.id}">${U.esc(u.game_name||u.username)} (${U.esc(u.username)})</option>`).join('')}</select></div><div class="form-group"><label>转交原因</label><textarea id="tf-reason" rows="2" placeholder="说明转交原因..."></textarea></div><div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-w"><i class="fas fa-exchange-alt"></i> 确认转交</button></div></form>`);
document.getElementById('tf-form').onsubmit = async e => { e.preventDefault();
try { await API.post(`/tickets/${id}/transfer`, {to_user_id: parseInt(document.getElementById('tf-to').value), reason: document.getElementById('tf-reason').value}); App.closeModal(); this.mount(); } catch(ex){alert(ex.message);}
};
},
async changeStatus(id) {
U.modal('å<EFBFBD>˜æ´çжæ€?, `<form id="cs-form"><div class="form-group"><label>新状æ€?/label><select id="cs-status">${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select></div><div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">ä¿<EFBFBD>å­˜</button></div></form>`);
U.modal('变更状态', `<form id="cs-form"><div class="form-group"><label>新状态</label><select id="cs-status">${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select></div><div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">保存</button></div></form>`);
document.getElementById('cs-form').onsubmit = async e => { e.preventDefault();
try { await API.put(`/tickets/${id}`, {status:document.getElementById('cs-status').value}); App.closeModal(); this.mount(); } catch(ex){alert(ex.message);}
};
@@ -118,10 +118,10 @@ const TicketDetail = {
appealResult(parentId) {
U.modal('结果申诉', `<form id="ar-form">
<div class="form-group"><label>申诉ç<EFBFBD>†ç”± *</label><input id="ar-reason" required placeholder="为什么对该处ç<EFBFBD>†ç»“果有å¼è®®ï¼?></div>
<div class="form-group"><label>详细说明 *</label><textarea id="ar-desc" rows="4" required placeholder="请详细说明情�.."></textarea></div>
<div class="form-group"><label>申诉理由 *</label><input id="ar-reason" required placeholder="为什么对该处理结果有异议?"></div>
<div class="form-group"><label>详细说明 *</label><textarea id="ar-desc" rows="4" required placeholder="请详细说明情况..."></textarea></div>
<div class="alert alert-i ts">申诉后将生成新工单,仅服主可见及处理</div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-w"><i class="fas fa-gavel"></i> æ<EFBFBD><EFBFBD>交申诉</button></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-w"><i class="fas fa-gavel"></i> 提交申诉</button></div>
</form>`);
document.getElementById('ar-form').onsubmit = async e => { e.preventDefault();
try {
@@ -133,7 +133,7 @@ const TicketDetail = {
parent_ticket_id: parentId,
});
App.closeModal();
alert('ç³è¯å·²æ<EFBFBD><EFBFBD>交,请ç­å¾æœ<EFBFBD>主å¤ç<EFBFBD>?);
alert('申诉已提交,请等待服主处理');
this.mount();
} catch(ex) { alert(ex.message); }
};

View File

@@ -20,14 +20,14 @@ const TicketsPage = {
ct.innerHTML = `
<div class="tab-nav" id="ticket-tabs">
<button data-tab="all" class="active">全部</button>
<button data-tab="pending">待认�/button>
<button data-tab="claimed">已接å<EFBFBD>?/button>
<button data-tab="pending">待认领</button>
<button data-tab="claimed">已接单</button>
${Auth.user().role === 'player' ? '<button data-tab="mine">我的工单</button>' : ''}
</div>
<div class="filter-bar">
<select id="ft-type" onchange="TicketsPage.applyFilters()"><option value="">全部类型</option>${Object.entries(U.TYPE).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>
<select id="ft-status" onchange="TicketsPage.applyFilters()"><option value="">全部状�/option>${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>
<select id="ft-priority" onchange="TicketsPage.applyFilters()"><option value="">全部优先�/option>${Object.entries(U.PRIORITY).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>
<select id="ft-status" onchange="TicketsPage.applyFilters()"><option value="">全部状态</option>${Object.entries(U.STATUS).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>
<select id="ft-priority" onchange="TicketsPage.applyFilters()"><option value="">全部优先级</option>${Object.entries(U.PRIORITY).map(([k,v])=>`<option value="${k}">${v}</option>`).join('')}</select>
<input id="ft-search" placeholder="搜索..." onkeyup="if(event.key==='Enter')TicketsPage.applyFilters()">
<button class="btn btn-o btn-sm" onclick="TicketsPage.applyFilters()"><i class="fas fa-search"></i></button>
<button class="btn btn-o btn-sm" onclick="TicketsPage.resetFilters()"><i class="fas fa-undo"></i></button>
@@ -74,7 +74,7 @@ const TicketsPage = {
ct.innerHTML = tickets.length === 0
? `<div class="empty"><i class="fas fa-inbox"></i><p>暂无工单</p></div>`
: `<div class="card table-wrap"><table><thead><tr>
<th>#</th><th>类型</th><th>标题</th><th>æ<EFBFBD><EFBFBD>交è€?/th><th>对象</th><th>状æ€?/th><th>优先çº?/th><th>è´Ÿè´£äº?/th><th>æ—¶é—´</th><th>æ“<EFBFBD>作</th>
<th>#</th><th>类型</th><th>标题</th><th>提交者</th><th>对象</th><th>状态</th><th>优先级</th><th>负责人</th><th>时间</th><th>操作</th>
</tr></thead><tbody>${tickets.map(t => `
<tr>
<td>${t.id}</td>
@@ -84,7 +84,7 @@ const TicketsPage = {
<td>${t.target_game_name?`${U.esc(t.target_game_name)}${t.target_game_uid?`<br><span class="tm ts">UID:${U.esc(t.target_game_uid)}</span>`:''}`:'-'}</td>
<td>${U.badge(t.status,'status')}</td>
<td>${U.badge(t.priority,'priority')}</td>
<td>${t.assignee_name?`<span class="badge bg-admin">${U.esc(t.assignee_name)}</span>`:`<span class="tm">待认�/span>`}</td>
<td>${t.assignee_name?`<span class="badge bg-admin">${U.esc(t.assignee_name)}</span>`:`<span class="tm">待认领</span>`}</td>
<td class="ts">${U.date(t.updated_at)}</td>
<td><button class="btn btn-o btn-sm" onclick="App.navigate('ticket',${t.id})"><i class="fas fa-eye"></i></button></td>
</tr>`).join('')}</tbody></table></div>`;

View File

@@ -19,20 +19,20 @@ const TrackingPage = {
async search() {
const token = document.getElementById('track-token').value.trim();
if (!token) return alert('请输入追踪标�);
if (!token) return alert('请输入追踪标识');
const ct = document.getElementById('track-result');
U.loading(ct);
try {
const tickets = await API.get(`/tickets/tracking?token=${token}`);
ct.innerHTML = tickets.length ? `
<div class="table-wrap"><table><thead><tr><th>#</th><th>类型</th><th>标题</th><th>状�/th><th>时间</th></tr></thead><tbody>${tickets.map(t => `
<div class="table-wrap"><table><thead><tr><th>#</th><th>类型</th><th>标题</th><th>状态</th><th>时间</th></tr></thead><tbody>${tickets.map(t => `
<tr>
<td>${t.id}</td><td>${U.badge(t.type,'type')}</td>
<td><strong>${U.esc(t.title)}</strong></td>
<td>${U.badge(t.status,'status')}</td><td class="ts">${U.date(t.created_at)}</td>
</tr>`).join('')}</tbody></table></div>
<p class="tm ts mt-4">如需获取更详细的跟踪和回复通知,请<a href="#/register" style="color:var(--p)">注册账号</a></p>
` : '<div class="empty"><i class="fas fa-inbox"></i><p>未æ¾åˆ°å·¥å<EFBFBD>?/p></div>';
` : '<div class="empty"><i class="fas fa-inbox"></i><p>未找到工单</p></div>';
} catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
}
};

View File

@@ -4,7 +4,7 @@ const UnclaimedPage = {
async render() { return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
async mount() {
document.getElementById('page-title').textContent = '待处ç<EFBFBD>†å·¥å<EFBFBD>?;
document.getElementById('page-title').textContent = '待处理工单';
document.getElementById('page-actions').innerHTML = '';
const ct = document.getElementById('page-content');
@@ -35,7 +35,7 @@ const UnclaimedPage = {
ct.innerHTML = tickets.length === 0
? `<div class="empty"><i class="fas fa-check-circle"></i><p>所有工单已处理完毕</p></div>`
: `<div class="card"><div class="card-h"><span>${tickets.length} 个待处理工单</span><span class="ts tm">按状态优先级排序</span></div><div class="table-wrap"><table><thead><tr>
<th>#</th><th>类型</th><th>标题</th><th>æ<EFBFBD><EFBFBD>交è€?/th><th>对象/详情</th><th>状æ€?/th><th>优先çº?/th><th>æ—¶é—´</th><th>æ“<EFBFBD>作</th>
<th>#</th><th>类型</th><th>标题</th><th>提交者</th><th>对象/详情</th><th>状态</th><th>优先级</th><th>时间</th><th>操作</th>
</tr></thead><tbody>${tickets.map(t => `
<tr>
<td>${t.id}</td><td>${U.badge(t.type,'type')}</td>
@@ -55,7 +55,7 @@ const UnclaimedPage = {
},
async quickClaim(id) {
U.modal('接å<EFBFBD>认é¢', `<form id="qc-form"><div class="form-group"><label>åˆ<EFBFBD>å§åžå¤<EFBFBD> (玩家å<C2B6>¯è§<C3A8>)</label><textarea id="qc-reply" rows="3" placeholder="接å<EFBFBD>•æ—¶å<EFBFBD>é€<EFBFBD>给玩家的åžå¤?.."></textarea></div><div class="form-group"><label>处ç<EFBFBD>†å¤‡æ³¨ (内部)</label><textarea id="qc-note" rows="2" placeholder="内部备注,玩家ä¸<EFBFBD>å<EFBFBD>¯è§<EFBFBD>"></textarea></div><div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-s"><i class="fas fa-hand-paper"></i> 确认接å<EFBFBD></button></div></form>`);
U.modal('接单认领', `<form id="qc-form"><div class="form-group"><label>初始回复 (玩家可见)</label><textarea id="qc-reply" rows="3" placeholder="接单时发送给玩家的回复..."></textarea></div><div class="form-group"><label>处理备注 (内部)</label><textarea id="qc-note" rows="2" placeholder="内部备注,玩家不可见"></textarea></div><div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-s"><i class="fas fa-hand-paper"></i> 确认接单</button></div></form>`);
document.getElementById('qc-form').onsubmit = async e => { e.preventDefault();
try { await API.post(`/tickets/${id}/claim`, {claim_note: document.getElementById('qc-note').value, initial_reply: document.getElementById('qc-reply').value}); App.closeModal(); this.load(); } catch(ex){alert(ex.message);}
};

View File

@@ -13,7 +13,7 @@ const UsersPage = {
const users = await API.get('/users');
ct.innerHTML = `
<div class="card table-wrap"><table><thead><tr>
<th>#</th><th>用户å<EFBFBD>?/th><th>邮箱</th><th>游æˆ<EFBFBD>å<EFBFBD>?/th><th>游æˆ<EFBFBD>UID</th><th>角色</th><th>状æ€?/th><th>æ—¶é—´</th><th>æ“<EFBFBD>作</th>
<th>#</th><th>用户名</th><th>邮箱</th><th>游戏名</th><th>游戏UID</th><th>角色</th><th>状态</th><th>时间</th><th>操作</th>
</tr></thead><tbody>${users.map(u => `
<tr>
<td>${u.id}</td><td>${U.esc(u.username)}</td><td>${U.esc(u.email)}</td>
@@ -30,10 +30,10 @@ const UsersPage = {
createUser() {
U.modal('添加用户', `
<form id="cu-form">
<div class="grid-2"><div class="form-group"><label>用户å<EFBFBD>?*</label><input id="cu-u" required></div><div class="form-group"><label>密ç <EFBFBD> *</label><input type="password" id="cu-p" required minlength="6"></div></div>
<div class="grid-2"><div class="form-group"><label>邮箱 *</label><input type="email" id="cu-e" required></div><div class="form-group"><label>角色 *</label><select id="cu-r"><option value="owner">æœ<EFBFBD>主</option><option value="admin">管ç<EFBFBD>†å?/option><option value="player">玩家</option></select></div></div>
<div class="grid-2"><div class="form-group"><label>游æˆ<EFBFBD>å<EFBFBD>?*</label><input id="cu-gn" required></div><div class="form-group"><label>游æˆ<EFBFBD>UID *</label><input id="cu-gu" required></div></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">创建</button></div>
<div class="grid-2"><div class="form-group"><label>用户名 *</label><input id="cu-u" required></div><div class="form-group"><label>密码 *</label><input type="password" id="cu-p" required minlength="6"></div></div>
<div class="grid-2"><div class="form-group"><label>邮箱 *</label><input type="email" id="cu-e" required></div><div class="form-group"><label>角色 *</label><select id="cu-r"><option value="owner">服主</option><option value="admin">管理员</option><option value="player">玩家</option></select></div></div>
<div class="grid-2"><div class="form-group"><label>游戏名 *</label><input id="cu-gn" required></div><div class="form-group"><label>游戏UID *</label><input id="cu-gu" required></div></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">创建</button></div>
</form>
`);
document.getElementById('cu-form').onsubmit = async e => {
@@ -56,11 +56,11 @@ const UsersPage = {
editUser(id, username, email, gameName, gameUid, role, active) {
U.modal('编辑用户', `
<form id="eu-form">
<div class="form-group"><label>用户å<EFBFBD>?/label><input value="${username}" disabled style="background:var(--g100)"></div>
<div class="form-group"><label>用户名</label><input value="${username}" disabled style="background:var(--g100)"></div>
<div class="grid-2"><div class="form-group"><label>邮箱</label><input type="email" id="eu-e" value="${email}"></div><div class="form-group"><label>角色</label><select id="eu-r">${['owner','admin','player'].map(r=>`<option value="${r}" ${r===role?'selected':''}>${U.ROLE[r]}</option>`).join('')}</select></div></div>
<div class="grid-2"><div class="form-group"><label>游æˆ<EFBFBD>å<EFBFBD>?/label><input id="eu-gn" value="${gameName}"></div><div class="form-group"><label>游æˆ<EFBFBD>UID</label><input id="eu-gu" value="${gameUid}"></div></div>
<div class="grid-2"><div class="form-group"><label>新密ç ?留空ä¸<C3A4>å<EFBFBD>˜)</label><input type="password" id="eu-p"></div><div class="form-group"><label>状æ€?/label><select id="eu-a"><option value="1" ${active?'selected':''}>å<EFBFBD>¯ç”¨</option><option value="0" ${!active?'selected':''}>ç¦<EFBFBD>用</option></select></div></div>
<div class="modal-f"><button type="button" class="btn btn-o" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">ä¿<EFBFBD>å­˜</button></div>
<div class="grid-2"><div class="form-group"><label>游戏名</label><input id="eu-gn" value="${gameName}"></div><div class="form-group"><label>游戏UID</label><input id="eu-gu" value="${gameUid}"></div></div>
<div class="grid-2"><div class="form-group"><label>新密码(留空不变)</label><input type="password" id="eu-p"></div><div class="form-group"><label>状态</label><select id="eu-a"><option value="1" ${active?'selected':''}>启用</option><option value="0" ${!active?'selected':''}>禁用</option></select></div></div>
<div class="modal-f"><button type="button" class="btn btn-o" onclick="App.closeModal()">取消</button><button type="submit" class="btn btn-p">保存</button></div>
</form>
`);
document.getElementById('eu-form').onsubmit = async e => {

View File

@@ -60,11 +60,5 @@ const U = {
document.querySelector('.btn-confirm-cancel').onclick = () => { App.closeModal(); resolve(false); };
document.querySelector('.btn-confirm-ok').onclick = () => { App.closeModal(); resolve(true); };
});
},
bindClicks(container, handlers) {
for (const [selector, fn] of Object.entries(handlers)) {
container.querySelectorAll(selector).forEach(el => el.addEventListener('click', fn));
}
}
};