feat: admin complaint (owner only) + result appeal (one per ticket, owner only)

This commit is contained in:
2026-07-12 15:19:09 +08:00
parent 6657c05dec
commit 10b6ff94ed
5 changed files with 72 additions and 9 deletions

View File

@@ -46,6 +46,10 @@ const TicketCreate = {
</datalist>
</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>申诉理由 *</label><input name="reason" required placeholder="如:误封、证据不足被封、违规行为已纠正等"></div>
<div class="form-group"><label>详细说明 *</label><textarea name="description" rows="5" placeholder="请详细说明情况,包括被封时间、可能原因、为什么应解封等..." required></textarea></div>

View File

@@ -34,6 +34,10 @@ 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">原工单</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>`:''}
@@ -63,6 +67,12 @@ const TicketDetail = {
</div></div>
`:''}
${!isStaff && t.user_id === Auth.user().id && ['resolved','rejected','closed'].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>
</div></div>`:''}
${t.transfers?.length?`
<div class="card"><div class="card-h">转交历史</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>
@@ -104,5 +114,28 @@ const TicketDetail = {
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>申诉理由 *</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" 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 {
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('申诉已提交,请等待服主处理');
this.mount();
} catch(ex) { alert(ex.message); }
};
}
};

View File

@@ -1,6 +1,6 @@
const U = {
STATUS: { pending:'待处理', processing:'处理中', awaiting_info:'待补充', resolved:'已解决', rejected:'已驳回', closed:'已关闭' },
TYPE: { report:'举报', suggestion:'建议', appeal:'申诉' },
TYPE: { report:'举报', suggestion:'建议', appeal:'申诉', result_appeal:'结果申诉' },
ROLE: { owner:'服主', admin:'管理员', player:'玩家' },
PRIORITY: { low:'低', medium:'中', high:'高', urgent:'紧急' },