- 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
105 lines
5.2 KiB
JavaScript
105 lines
5.2 KiB
JavaScript
/*
|
||
* 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 SourcesPage = {
|
||
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" onclick="SourcesPage.showAdd()"><i class="fas fa-plus"></i> 添加来源</button>';
|
||
await this.load();
|
||
},
|
||
|
||
async load() {
|
||
const ct = document.getElementById('page-content');
|
||
try {
|
||
// 管理视图: 全量含 enabled(可显示/切换启用停用)
|
||
const rows = await API.get('/sources?manage=1');
|
||
ct.innerHTML = rows.length === 0
|
||
? '<div class="empty"><i class="fas fa-tag"></i><p>暂无来源</p></div>'
|
||
: `<div class="card table-wrap"><table><thead><tr><th>code</th><th>名称</th><th>排序</th><th>状态</th><th>操作</th></tr></thead><tbody>${rows.map(s => `
|
||
<tr>
|
||
<td><code>${U.esc(s.code)}</code></td>
|
||
<td>${U.esc(s.label)}</td>
|
||
<td>${s.sort_order}</td>
|
||
<td>${s.enabled ? '<span class="badge bg-resolved">启用</span>' : '<span class="badge bg-rejected">停用</span>'}</td>
|
||
<td>
|
||
<button class="btn btn-o btn-sm" onclick="SourcesPage.showEdit('${U.escJs(s.code)}','${U.escJs(s.label)}',${s.enabled},${s.sort_order})"><i class="fas fa-edit"></i> 编辑</button>
|
||
<button class="btn btn-d btn-sm" onclick="SourcesPage.del('${U.escJs(s.code)}')"><i class="fas fa-trash"></i></button>
|
||
</td>
|
||
</tr>`).join('')}</tbody></table></div>
|
||
<p class="help" style="margin-top:8px">来源用于标识玩家身份归属(如: 网易端/皮肤站/正版/离线)。已被身份数据使用的来源不能删除, 只能停用。</p>`;
|
||
} catch (e) { ct.innerHTML = `<div class="alert alert-e">${e.message}</div>`; }
|
||
},
|
||
|
||
showAdd() {
|
||
U.modal('添加来源', `
|
||
<form id="src-form">
|
||
<div class="grid-2">
|
||
<div class="form-group"><label>code *</label><input id="src-code" required placeholder="如: official" pattern="[a-z0-9_]{1,20}"></div>
|
||
<div class="form-group"><label>名称 *</label><input id="src-label" required placeholder="如: 正版"></div>
|
||
</div>
|
||
<div class="form-group"><label>排序</label><input id="src-sort" type="number" value="0"></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>
|
||
`);
|
||
document.getElementById('src-form').onsubmit = async e => {
|
||
e.preventDefault();
|
||
try {
|
||
await API.post('/sources', {
|
||
code: document.getElementById('src-code').value,
|
||
label: document.getElementById('src-label').value,
|
||
sort_order: document.getElementById('src-sort').value,
|
||
});
|
||
App.closeModal(); this.load();
|
||
} catch(ex){alert(ex.message);}
|
||
};
|
||
},
|
||
|
||
showEdit(code, label, enabled, sortOrder) {
|
||
U.modal('编辑来源', `
|
||
<form id="src-edit-form">
|
||
<div class="form-group"><label>code</label><input value="${U.esc(code)}" disabled></div>
|
||
<div class="grid-2">
|
||
<div class="form-group"><label>名称 *</label><input id="se-label" required value="${U.esc(label)}"></div>
|
||
<div class="form-group"><label>排序</label><input id="se-sort" type="number" value="${sortOrder}"></div>
|
||
</div>
|
||
<div class="form-group"><label>状态</label><select id="se-enabled"><option value="1" ${enabled?'selected':''}>启用</option><option value="0" ${!enabled?'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>
|
||
`);
|
||
document.getElementById('src-edit-form').onsubmit = async e => {
|
||
e.preventDefault();
|
||
try {
|
||
await API.put(`/sources/${code}`, {
|
||
label: document.getElementById('se-label').value,
|
||
enabled: document.getElementById('se-enabled').value === '1',
|
||
sort_order: document.getElementById('se-sort').value,
|
||
});
|
||
App.closeModal(); this.load();
|
||
} catch(ex){alert(ex.message);}
|
||
};
|
||
},
|
||
|
||
async del(code) {
|
||
if (!await U.confirm(`确认删除来源 ${code}?`)) return;
|
||
try { await API.delete(`/sources/${code}`); this.load(); } catch(ex){alert(ex.message);}
|
||
}
|
||
};
|