Files
MC_Report/public/js/pages/ticket-detail.js

142 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const TicketDetail = {
id: null,
async render(id) { this.id = id; return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
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" 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>`; }
},
renderDetail(ct, t) {
const isStaff = Auth.isAdmin();
const isOwner = Auth.isOwner();
const isRep = t.type === 'report';
const isAppeal = t.type === 'appeal' || t.type === 'result_appeal';
const canClaim = isStaff && !t.assigned_to && t.status === 'pending';
const canTransfer = isStaff && t.assigned_to === Auth.user().id && t.status === 'processing';
const isMyTicket = t.assigned_to === Auth.user().id;
ct.innerHTML = `
<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">æ<><C3A6>交è€?/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>`:''}
${t.description?`<div class="detail-row"><span class="lbl">${isRep?'详情':isAppeal?'详细说明':'建议内容'}</span><span class="val" style="white-space:pre-wrap">${U.esc(t.description)}</span></div>`:''}
<div class="detail-row"><span class="lbl">æ<><C3A6>交时间</span><span class="val">${U.date(t.created_at)}</span></div>
<div class="detail-row"><span class="lbl">æ›´æ–°æ—¶é—´</span><span class="val">${U.date(t.updated_at)}</span></div>
</div></div>
${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">原工å<C2A5>?/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">
${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?'输入åžå¤<C3A5>内容...':'补充说明...'}" 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">处ç<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>
${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" data-action="TicketDetail:claimTicket:${t.id}"><i class="fas fa-hand-paper"></i> 接å<C2A5>•认领</button>`:''}
${isMyTicket?`<button class="btn btn-p btn-sm w-full" data-action="TicketDetail:editNote:${t.id}"><i class="fas fa-pen"></i> æ´æ°å¤„ç<E2809E>†å¤‡æ³¨</button>`:''}
${canTransfer?`<button class="btn btn-w btn-sm w-full" data-action="TicketDetail:transferTicket:${t.id}"><i class="fas fa-exchange-alt"></i> 转å<C2AC>•</button>`:''}
${isOwner||isMyTicket?`<button class="btn btn-o btn-sm w-full" data-action="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">对处ç<E2809E>†ç»“果有å¼è®®ï¼Ÿå<C5B8>¯ä»¥æ<C2A5><C3A6>交一次申诉,由æœ<C3A6>主å¤<C3A5>æ ¸ã€?/p>
<button class="btn btn-w btn-sm w-full" data-action="TicketDetail:appealResult:${t.id}"><i class="fas fa-gavel"></i> 对结果æ<C593><C3A6>出申è¯?/button>
</div></div>`:''}
${t.transfers?.length?`
<div class="card"><div class="card-h">转交历å<E280A0>²</div><div class="card-b">
<div class="timeline">${t.transfers.map(tf=>`<div class="timeline-item"><div class="th"><span class="tn">${U.esc(tf.from_game_name||tf.from_username)}</span><i class="fas fa-arrow-right ts tm"></i><span class="tn">${U.esc(tf.to_game_name||tf.to_username)}</span><span>${U.date(tf.created_at)}</span></div>${tf.reason?`<div class="tc">${U.esc(tf.reason)}</div>`:''}</div>`).join('')}</div>
</div></div>`:''}
</div></div>`;
document.getElementById('reply-form')?.addEventListener('submit', async e => {
e.preventDefault(); const c = document.getElementById('reply-content').value.trim(); if(!c)return;
try { await API.post(`/tickets/${id}/response`, {content:c}); this.mount(); } catch(ex){alert(ex.message);}
});
},
async claimTicket(id) {
U.modal('接å<C2A5>•认领', `<form id="claim-form"><div class="form-group"><label>åˆ<C3A5>å§åžå¤<C3A5> (玩家å<C2B6>¯è§<C3A8>)</label><textarea id="claim-reply" rows="3" placeholder="接å<C2A5>•æ—¶å<C2B6>é€<C3A9>给玩家的åžå¤<C3A5>,å¦ï¼šå·²æ”¶åˆ°ä¸¾æŠ¥ï¼Œæ­£åœ¨æ ¸æŸ¥ä¸?.."></textarea></div><div class="form-group"><label>处ç<E2809E>†å¤‡æ³¨ (内部)</label><textarea id="claim-note" rows="2" placeholder="内部处ç<E2809E>†å¤‡æ³¨ï¼Œçީ家ä¸<C3A4>å<EFBFBD>¯è§<C3A8>"></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> 确认接å<C2A5>•</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);}
};
},
async editNote(id) {
const t = await API.get(`/tickets/${id}`);
U.modal(´æ°å¤„ç<E2809E>†å¤‡æ³¨', `<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">ä¿<C3A4>å­˜</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);}
};
},
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('转å<C2AC>•', `<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" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-p">ä¿<C3A4>å­˜</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);}
};
},
appealResult(parentId) {
U.modal('ç»æžœç³è¯', `<form id="ar-form">
<div class="form-group"><label>申诉ç<E280B0>†ç”± *</label><input id="ar-reason" required placeholder="为什么对该处ç<E2809E>†ç»“果有å¼è®®ï¼?></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" data-action="App:closeModal">å<>消</button><button type="submit" class="btn btn-w"><i class="fas fa-gavel"></i> æ<><C3A6>交申诉</button></div>
</form>`);
document.getElementById('ar-form').onsubmit = async e => { e.preventDefault();
try {
await API.post('/tickets', {
type: 'result_appeal',
title: `结果申诉: #${parentId}`,
reason: document.getElementById('ar-reason').value,
description: document.getElementById('ar-desc').value,
parent_ticket_id: parentId,
});
App.closeModal();
alert('ç³è¯å·²æ<EFBFBD><EFBFBD>交,请ç­å¾æœ<EFBFBD>主å¤ç<EFBFBD>?);
this.mount();
} catch(ex) { alert(ex.message); }
};
}
};