- logged-in users: identity dropdown shown when >=1 identity exists, game name input hidden (identity carries the submitter); required removed from hidden field - no identity: fall back to showing the game name input - anonymous: unchanged, only the input field - submit: identity selection still overrides reporter name/uid
245 lines
11 KiB
JavaScript
245 lines
11 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 TicketCreate = {
|
||
files: [],
|
||
|
||
render(container, { type, prefill, backLink, hideTabs }) {
|
||
const user = Auth.user();
|
||
const isReport = type === 'report';
|
||
const isSuggestion = type === 'suggestion';
|
||
const isAppeal = type === 'appeal';
|
||
const typeLabel = isReport ? '举报' : isAppeal ? '申诉' : '建议';
|
||
this.files = [];
|
||
this.identities = [];
|
||
|
||
// 登录用户: 拉取多来源身份(网易端+皮肤站可同时绑定)
|
||
if (user) {
|
||
API.get('/auth/identities').then(list => {
|
||
this.identities = list || [];
|
||
const sel = container.querySelector('#identity-select');
|
||
const nameGroup = container.querySelector('#reporter-name-group');
|
||
if (sel) {
|
||
if (this.identities.length > 0) {
|
||
// 有身份: 显示身份选择, 隐藏游戏名输入框(身份即代表提交者)
|
||
sel.innerHTML = this.identities.map((it, i) => `<option value="${i}">${U.esc(it.source)} — ${U.esc(it.game_name)}</option>`).join('');
|
||
sel.closest('.form-group').classList.remove('hidden');
|
||
if (nameGroup) {
|
||
nameGroup.classList.add('hidden');
|
||
const inp = nameGroup.querySelector('input[name="reporter_game_name"]');
|
||
if (inp) inp.required = false; // 隐藏字段不再参与原生必填校验
|
||
}
|
||
} else {
|
||
// 无身份: 保留游戏名输入框兜底
|
||
sel.closest('.form-group').classList.add('hidden');
|
||
if (nameGroup) nameGroup.classList.remove('hidden');
|
||
}
|
||
}
|
||
}).catch(() => {});
|
||
}
|
||
|
||
// 子服务器列表(server_groups: 分组/子服)
|
||
this.servers = [];
|
||
API.get('/polls/server-list').then(list => {
|
||
this.servers = list || [];
|
||
const sel = container.querySelector('#server-select');
|
||
if (sel) {
|
||
sel.innerHTML = '<option value="">不指定</option>' + this.servers.map(g =>
|
||
`<option value="${U.escJs(g.group_name)}/${U.escJs(g.server_name)}">${U.esc(g.group_name)} / ${U.esc(g.server_name)}</option>`).join('');
|
||
}
|
||
}).catch(() => {});
|
||
|
||
container.innerHTML = `
|
||
${hideTabs ? '' : `<div class="tab-nav" id="type-tabs">
|
||
<button class="${isReport?'active':''}" data-type="report">举报</button>
|
||
<button class="${isSuggestion?'active':''}" data-type="suggestion">建议</button>
|
||
<button class="${isAppeal?'active':''}" data-type="appeal">申诉</button>
|
||
</div>`}
|
||
<form id="ticket-form" enctype="multipart/form-data">
|
||
<input type="hidden" name="type" value="${type}">
|
||
|
||
<div class="card">
|
||
<div class="card-h">提交${typeLabel}</div>
|
||
<div class="card-b">
|
||
<div class="form-group">
|
||
<label>${typeLabel}标题 *</label>
|
||
<input name="title" required placeholder="${isReport ? '如:玩家破坏建筑' : isAppeal ? '如:申请解除封禁' : '如:增加领地保护功能'}">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>子服务器</label>
|
||
<select name="server" id="server-select"><option value="">不指定</option></select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>优先级</label>
|
||
<select name="priority">
|
||
<option value="low">低</option><option value="medium" selected>中</option><option value="high">高</option><option value="urgent">紧急</option>
|
||
</select>
|
||
</div>
|
||
|
||
${user ? `<div class="form-group hidden" id="identity-group">
|
||
<label>使用身份 *</label>
|
||
<select id="identity-select"></select>
|
||
</div>` : ''}
|
||
|
||
<div class="form-group" id="reporter-name-group">
|
||
<label>您的游戏名称 *</label>
|
||
<input name="reporter_game_name" required placeholder="Minecraft ID" value="${U.esc(prefill?.game_name || user?.game_name || '')}" ${prefill?.game_name ? 'readonly' : ''}>
|
||
</div>
|
||
|
||
${isReport ? `
|
||
<div class="grid-2">
|
||
<div class="form-group"><label>对方游戏名称 (至少一项)</label><input name="target_game_name" placeholder="被举报者Minecraft ID"></div>
|
||
<div class="form-group"><label>对方游戏UID (至少一项)</label><input name="target_game_uid" placeholder="被举报者UID"></div>
|
||
</div>
|
||
<div class="form-group"><label>举报原因 *</label>
|
||
<input name="reason" list="reason-list" required placeholder="选择或自行输入举报原因">
|
||
<datalist id="reason-list">
|
||
<option value="破坏建筑"><option value="盗窃物品"><option value="外挂作弊">
|
||
<option value="语言骚扰"><option value="恶意PVP"><option value="利用漏洞">
|
||
<option value="其他违规"><option value="诈骗"><option value="恶意辱骂">
|
||
</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>
|
||
` : `
|
||
<div class="form-group"><label>建议内容 *</label><textarea name="description" rows="5" placeholder="请详细描述您的建议..." required></textarea></div>
|
||
`}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<div class="card-h">上传文件 (图片/视频,每个最大20MB)</div>
|
||
<div class="card-b">
|
||
<div class="file-upload" id="file-drop">
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
<p>点击选择文件或拖拽到此处</p>
|
||
<p class="help">支持: JPG/PNG/GIF/WebP/MP4/WebM 每个最大50MB</p>
|
||
</div>
|
||
<input type="file" id="file-input" multiple accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm" style="display:none">
|
||
<div class="file-list" id="file-list"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:8px;justify-content:flex-end">
|
||
${backLink ? `<button type="button" class="btn btn-o" onclick="${backLink}">返回</button>` : ''}
|
||
<button type="submit" class="btn btn-p btn-lg"><i class="fas fa-paper-plane"></i> 提交</button>
|
||
</div>
|
||
</form>
|
||
`;
|
||
|
||
this.bindFileUpload(container);
|
||
this.bindSubmit(container, type);
|
||
|
||
container.querySelectorAll('#type-tabs button').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
TicketCreate.render(container, { type: btn.dataset.type, prefill, backLink });
|
||
});
|
||
});
|
||
},
|
||
|
||
bindFileUpload(container) {
|
||
const drop = container.querySelector('#file-drop');
|
||
const input = container.querySelector('#file-input');
|
||
const list = container.querySelector('#file-list');
|
||
|
||
drop.onclick = () => input.click();
|
||
input.onchange = () => this.addFiles(input.files, list);
|
||
drop.ondragover = e => { e.preventDefault(); drop.style.borderColor = 'var(--p)'; };
|
||
drop.ondragleave = () => { drop.style.borderColor = 'var(--g300)'; };
|
||
drop.ondrop = e => {
|
||
e.preventDefault();
|
||
drop.style.borderColor = 'var(--g300)';
|
||
this.addFiles(e.dataTransfer.files, list);
|
||
};
|
||
},
|
||
|
||
addFiles(fileList, listEl) {
|
||
const allowed = ['image/jpeg','image/png','image/gif','image/webp','video/mp4','video/webm'];
|
||
for (const f of fileList) {
|
||
if (!allowed.includes(f.type)) { alert(`不支持的文件类型: ${f.name}`); continue; }
|
||
if (f.size > 50 * 1024 * 1024) { alert(`文件过大: ${f.name}`); continue; }
|
||
if (this.files.length >= 5) { alert('最多5个文件'); break; }
|
||
this.files.push(f);
|
||
}
|
||
this.refreshFileList(listEl);
|
||
},
|
||
|
||
refreshFileList(listEl) {
|
||
listEl.innerHTML = this.files.map((f, i) => `
|
||
<div class="file-item">
|
||
<i class="fas fa-${f.type.startsWith('video') ? 'video' : 'image'}"></i>
|
||
${U.esc(f.name)} (${(f.size / 1024 / 1024).toFixed(2)}MB)
|
||
<button type="button" onclick="TicketCreate.removeFile(${i})"><i class="fas fa-times"></i></button>
|
||
</div>`).join('');
|
||
},
|
||
|
||
removeFile(i) { this.files.splice(i, 1); this.refreshFileList(document.getElementById('file-list')); },
|
||
|
||
async bindSubmit(container, type) {
|
||
const form = container.querySelector('#ticket-form');
|
||
form.addEventListener('submit', async e => {
|
||
e.preventDefault();
|
||
const fd = new FormData(form);
|
||
|
||
// 身份选择: 选中多来源身份时覆盖游戏名/UID
|
||
const sel = container.querySelector('#identity-select');
|
||
if (sel && sel.value !== '') {
|
||
const it = this.identities[parseInt(sel.value)];
|
||
if (it) {
|
||
fd.set('reporter_game_name', it.game_name);
|
||
fd.set('reporter_game_uid', it.game_uid || '');
|
||
}
|
||
}
|
||
|
||
if (type === 'report') {
|
||
const tgn = fd.get('target_game_name');
|
||
const tgu = fd.get('target_game_uid');
|
||
if (!tgn && !tgu) { return alert('举报需至少填写对方游戏名或UID之一'); }
|
||
}
|
||
|
||
for (const f of this.files) fd.append('files', f);
|
||
|
||
const btn = form.querySelector('[type="submit"]');
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 提交中...';
|
||
try {
|
||
const res = await API.req('POST', '/tickets', fd);
|
||
if (res.tracking_token) {
|
||
document.cookie = `tracking_token=${res.tracking_token};path=/;max-age=${365*24*3600};SameSite=Lax`;
|
||
}
|
||
alert('提交成功!工单编号: #' + res.id);
|
||
if (Auth.logged()) App.navigate('tickets');
|
||
else App.navigate('home');
|
||
} catch (ex) {
|
||
alert(ex.message);
|
||
btn.disabled = false;
|
||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> 提交';
|
||
}
|
||
});
|
||
}
|
||
};
|