fix: data-action delegation + convert onclick to event listeners, sidebar+logout+modals

This commit is contained in:
2026-07-15 02:44:18 +08:00
parent 9e15a319f5
commit f0e891b17c
20 changed files with 147 additions and 133 deletions

View File

@@ -6,7 +6,7 @@ const TicketDetail = {
async mount() {
const id = this.id;
document.getElementById('page-title').textContent = `å·¥å<EFBFBD>• #${id}`;
document.getElementById('page-actions').innerHTML = `<button class="btn btn-o btn-sm" onclick="App.navigate('tickets')"><i class="fas fa-arrow-left"></i> 返回列表</button>`;
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>`;
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>基本信æ<C2A1>¯</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">提交者</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">æ<EFBFBD><EFBFBD>交è€?/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">申诉ç<E280B0>†ç”±</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">处ç<E2809E>†å¤‡æ³¨</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">å…³è<C2B3>”å·¥å<C2A5>•</div><div class="card-b">
<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 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></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?žå¤<C3A5> (公开)':'补充信æ<C2A1>¯'}</div><div class="card-b">
<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>
<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>
</div></div>
</div>
<div>
<div class="card"><div class="card-h">处ç<E2809E>†ä¿¡æ<C2A1>¯</div><div class="card-b">
<div class="detail-row"><span class="lbl">æ<><C3A6>交账å<C2A6>·</span><span class="val">${t.submitter?`${U.esc(t.submitter)} (${U.esc(t.user_game_name||'')})`:'匿å<C2BF><C3A5>'}</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">管ç<C2A1>†æ“<C3A6>作</div><div class="card-b" style="display:flex;flex-direction:column;gap:6px">
${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>`:''}
${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>`:''}
</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">对处理结果有异议?可以提交一次申诉,由服主复核。</p>
<button class="btn btn-w btn-sm w-full" onclick="TicketDetail.appealResult(${t.id})"><i class="fas fa-gavel"></i> 对结果提出申诉</button>
<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>
</div></div>`:''}
${t.transfers?.length?`
@@ -86,7 +86,7 @@ const TicketDetail = {
},
async claimTicket(id) {
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>`);
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>`);
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('更新处理备注', `<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>`);
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>`);
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('转单', `<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>`);
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>`);
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('变更状态', `<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>`);
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>`);
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>申诉理由 *</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="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="alert alert-i ts">申诉å<E280B0>Žå°†ç”Ÿæˆ<C3A6>æ°å·¥å<C2A5>•,仅æœ<C3A6>主å<C2BB>¯è§<C3A8>å<EFBFBD>Šå¤„ç<E2809E>†</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>
<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>
</form>`);
document.getElementById('ar-form').onsubmit = async e => { e.preventDefault();
try {
@@ -133,7 +133,7 @@ const TicketDetail = {
parent_ticket_id: parentId,
});
App.closeModal();
alert('申诉已提交,请等待服主处理');
alert('ç³è¯å·²æ<EFBFBD><EFBFBD>交,请ç­å¾æœ<EFBFBD>主å¤ç<EFBFBD>?);
this.mount();
} catch(ex) { alert(ex.message); }
};