Files
MC_Report/public/js/pages/notifications.js
canglan c5ceba7cf0 fix: data-action buttons, sources enabled, email notify, ticket detail single-page
- app.js: mount page objects via inline script (CSP blocks eval → data-action buttons dead)
- app.js: add tickets list entry to sidebar nav (admin could not find claimed tickets)
- sources: GET /sources?manage=1 returns enabled field for admin view
- mailer: sendNotifyEmail consumes email-type notification_configs (webhook_url = recipients)
- tickets: trigger notify on created/claimed/transferred/updated/status-change
- notifications: email type shows recipient field, validates email list
- ticket-detail: single-page chat flow (player/staff bubbles) + processing log timeline
- css: chat bubble styles
2026-08-22 20:00:12 +08:00

126 lines
8.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const NotificationsPage = {
async render() { return '<div class="loading"><i class="fas fa-spinner"></i></div>'; },
async mount() {
document.getElementById('page-title').textContent = '通知配置';
document.getElementById('page-actions').innerHTML = '<button class="btn btn-p btn-sm" data-action="NotificationsPage:showCreate"><i class="fas fa-plus"></i> 添加通知</button>';
await this.load();
},
async load() {
const ct = document.getElementById('page-content');
try {
const cfgs = await API.get('/notifications');
const typeLabel = { webhook:'Discord', email:'邮件通知', generic:'通用Webhook' };
ct.innerHTML = cfgs.length ? cfgs.map(c => `
<div class="card"><div class="card-h"><span><i class="fas fa-${c.type==='email'?'envelope':c.type==='generic'?'plug':'link'}"></i> ${U.esc(c.name)}</span><span>${c.active?`<span class="badge bg-resolved">启用</span>`:`<span class="badge bg-rejected">禁用</span>`}</span></div><div class="card-b">
<div class="detail-row"><span class="lbl">类型</span><span class="val">${typeLabel[c.type]||c.type}</span></div>
${c.type!=='email' && c.webhook_url?`<div class="detail-row"><span class="lbl">地址</span><span class="val ts" style="word-break:break-all">${U.esc(c.webhook_url)}</span></div>`:''}
<div class="detail-row"><span class="lbl">事件</span><span class="val">${EventsLabel(c.events)}</span></div>
<div class="mt-2 flex g2">
<button class="btn btn-o btn-sm" onclick="NotificationsPage.showEdit(${c.id},'${U.escJs(c.name)}','${c.type}','${U.escJs(c.webhook_url||'')}','${c.events}',${c.active})"><i class="fas fa-edit"></i> 编辑</button>
${c.type!=='email'?`<button class="btn btn-i btn-sm" onclick="NotificationsPage.test(${c.id})"><i class="fas fa-paper-plane"></i> 测试</button>`:''}
<button class="btn btn-d btn-sm" onclick="NotificationsPage.del(${c.id})"><i class="fas fa-trash"></i></button>
</div>
</div></div>
`).join('') : '<div class="empty"><i class="fas fa-bell-slash"></i><p>暂无通知配置</p></div>';
} catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
},
showCreate() {
U.modal('添加通知', `
<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" onchange="NotificationsPage.toggleFields()">
<option value="webhook">Discord Webhook</option>
<option value="email">邮件通知</option>
<option value="generic">通用Webhook(企微/QQ/飞书)</option>
</select></div>
<div class="form-group" id="nc-url-group"><label id="nc-url-label">Webhook URL</label><input id="nc-url" placeholder="https://..."></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>
`);
bindEventToggles();
document.getElementById('nc-form').onsubmit = async e => { e.preventDefault();
const type = document.getElementById('nc-type').value;
try { await API.post('/notifications',{name:document.getElementById('nc-name').value, type, webhook_url:document.getElementById('nc-url').value, events:document.getElementById('nc-events').value}); App.closeModal(); this.load(); } catch(ex){alert(ex.message);}
};
this.toggleFields();
},
toggleFields() {
const type = document.getElementById('nc-type')?.value || document.getElementById('ne-type')?.value;
const urlGroup = document.querySelector('#nc-url-group') || document.querySelector('#ne-url-group');
const urlLabel = document.querySelector('#nc-url-label') || document.querySelector('#ne-url-label');
if (!urlGroup) return;
if (type === 'email') {
urlGroup.style.display = '';
urlLabel.textContent = '收件人邮箱 (逗号分隔)';
const inp = urlGroup.querySelector('input');
if (inp) { inp.placeholder = 'admin@example.com, staff@example.com'; inp.type = 'email'; }
} else {
urlGroup.style.display = '';
urlLabel.textContent = 'Webhook URL';
const inp = urlGroup.querySelector('input');
if (inp) { inp.placeholder = 'https://...'; inp.type = 'text'; }
}
},
showEdit(id, name, type, url, events, active) {
U.modal('编辑通知', `
<form id="ne-form"><div class="form-group"><label>名称</label><input id="ne-name" value="${name}"></div>
<div class="form-group"><label>类型</label><select id="ne-type" onchange="NotificationsPage.toggleFields()">
<option value="webhook" ${type==='webhook'?'selected':''}>Discord Webhook</option>
<option value="email" ${type==='email'?'selected':''}>邮件通知</option>
<option value="generic" ${type==='generic'?'selected':''}>通用Webhook</option>
</select></div>
<div class="form-group" id="ne-url-group" style="${type==='email'?'display:none':''}"><label id="ne-url-label">Webhook URL</label><input id="ne-url" value="${url}"></div>
<div class="form-group"><label>触发事件</label>
<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':''}>启用</option><option value="0" ${!active?'selected':''}>禁用</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">保存</button></div></form>
`);
bindEventToggles();
this.toggleFields();
document.getElementById('ne-form').onsubmit = async e => { e.preventDefault();
const newType = document.getElementById('ne-type').value;
try { await API.put(`/notifications/${id}`,{name:document.getElementById('ne-name').value, webhook_url:document.getElementById('ne-url')?.value, events:document.getElementById('ne-events').value, active:document.getElementById('ne-active').value==='1', type:newType}); App.closeModal(); this.load(); } catch(ex){alert(ex.message);}
};
},
async test(id) { try { alert((await API.post(`/notifications/${id}/test`)).message); } 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" ${String(current||'').split(',').map(s=>s.trim()).includes(e)?'checked':''}> ${EventsLabelSingle(e)}</label>`).join(''); }
function EventsLabel(v) { return v==='all'?'全部事件':v.split(',').map(s=>s.trim()).map(EventsLabelSingle).join(', '); }
function EventsLabelSingle(e) { return {all:'全部',ticket_created:'工单创建',ticket_claimed:'工单认领',ticket_transferred:'工单转交',ticket_updated:'工单更新'}[e]||e; }
function bindEventToggles() {
document.querySelectorAll('.ev-check').forEach(cb=>{ cb.onchange=()=>{ const all=document.querySelectorAll('.ev-check'); const v=Array.from(all).filter(c=>c.checked).map(c=>c.value).join(','); const hid=cb.closest('form').querySelector('input[type="hidden"]'); if(hid)hid.value=v; }; });
}