mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-07 05:45:46 +08:00
795 lines
28 KiB
HTML
795 lines
28 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>封禁管理</title>
|
|
<link rel="stylesheet" href="/lib/css/bootstrap.min.css">
|
|
<link rel="stylesheet" href="/lib/css/font-awesome.min.css">
|
|
<style>
|
|
:root {
|
|
--primary-color: #0056b3;
|
|
--bg-color: #f0f2f5;
|
|
--card-bg: #ffffff;
|
|
--text-color: #2c3e50;
|
|
--border-color: #d1d5db;
|
|
--secondary-color: #6c757d;
|
|
--danger-color: #d93025;
|
|
--success-color: #107c10;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
margin: 0;
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100vh;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.title {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
color: #d93025;
|
|
}
|
|
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #a80000;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background-color: #fff;
|
|
border: 1px solid #ccc;
|
|
color: #333;
|
|
}
|
|
|
|
.btn-secondary:hover {
|
|
background-color: #f0f0f0;
|
|
}
|
|
|
|
.container {
|
|
flex: 1;
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.toolbar {
|
|
padding: 15px 20px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: #fcfcfc;
|
|
}
|
|
|
|
.search-box {
|
|
position: relative;
|
|
width: 300px;
|
|
}
|
|
|
|
.search-input {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
padding-left: 30px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.search-icon {
|
|
position: absolute;
|
|
left: 10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #888;
|
|
}
|
|
|
|
.table-container {
|
|
flex: 1;
|
|
overflow: auto;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
th {
|
|
background: #f8f9fa;
|
|
padding: 12px 20px;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: #555;
|
|
position: sticky;
|
|
top: 0;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
td {
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
tr:hover {
|
|
background-color: #fff1f0;
|
|
}
|
|
|
|
.status-expired {
|
|
color: #999;
|
|
background: #eee;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.status-active {
|
|
color: #d93025;
|
|
background: #ffebe9;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 0.85em;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.steamid-text {
|
|
font-family: monospace;
|
|
color: #666;
|
|
}
|
|
|
|
.reason-text {
|
|
color: #333;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
z-index: 1000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.modal-overlay.show {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: white;
|
|
padding: 25px;
|
|
border-radius: 8px;
|
|
width: 500px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.modal-title {
|
|
margin-top: 0;
|
|
margin-bottom: 20px;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.form-input,
|
|
.form-select {
|
|
width: 100%;
|
|
padding: 8px;
|
|
border: 1px solid #ddd;
|
|
border-radius: 4px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
margin-top: 25px;
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 40px;
|
|
text-align: center;
|
|
color: #999;
|
|
}
|
|
|
|
.status-msg {
|
|
margin-right: 15px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.unsaved-badge {
|
|
display: inline-block;
|
|
background: #d93025;
|
|
color: white;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
font-size: 0.8rem;
|
|
margin-left: 10px;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% {
|
|
opacity: 1;
|
|
}
|
|
|
|
50% {
|
|
opacity: 0.6;
|
|
}
|
|
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
th.sortable {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
th.sortable:hover {
|
|
background-color: #eee;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<script>
|
|
(function() {
|
|
const required = 'bans.view';
|
|
const permsStr = localStorage.getItem('permissions');
|
|
const isSuper = localStorage.getItem('isSuperAdmin') === 'true';
|
|
|
|
if (isSuper) return;
|
|
|
|
if (!permsStr) {
|
|
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;color:red;font-size:1.5rem;">Access Denied: Not logged in.</div>';
|
|
throw new Error("Access Denied");
|
|
}
|
|
|
|
try {
|
|
const perms = JSON.parse(permsStr);
|
|
if (!perms.includes('all') && !perms.includes(required)) {
|
|
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;color:red;font-size:1.5rem;">Access Denied: Permission "' + required + '" required.</div>';
|
|
throw new Error("Access Denied");
|
|
}
|
|
} catch(e) {
|
|
if(e.message === "Access Denied") throw e;
|
|
console.error(e);
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<div class="header">
|
|
<div class="title">
|
|
<a href="/" class="btn btn-secondary btn-sm" style="margin-right: 10px;"><i class="fas fa-arrow-left"></i> 返回</a>
|
|
<i class="fas fa-ban"></i> 封禁管理
|
|
</div>
|
|
<div>
|
|
<span id="status-msg" class="status-msg"></span>
|
|
<button class="btn btn-secondary" onclick="loadBans()">
|
|
<i class="fas fa-sync"></i> 刷新
|
|
</button>
|
|
<button class="btn btn-primary" onclick="syncBans()" id="btnSync">
|
|
<i class="fas fa-file-export"></i> 同步到 Ban.cfg
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="toolbar">
|
|
<div class="search-box">
|
|
<span class="search-icon"><i class="fas fa-search"></i></span>
|
|
<input type="text" class="search-input" id="search-input" placeholder="搜索 SteamID 或 原因..."
|
|
onkeyup="filterBans()">
|
|
</div>
|
|
<button class="btn btn-primary" onclick="showAddBanModal()">
|
|
<i class="fas fa-plus"></i> 新增封禁
|
|
</button>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table id="ban-table">
|
|
<thead>
|
|
<tr>
|
|
<th width="100" class="sortable" onclick="sortBans('status')"><i class="fas fa-sort"></i> 状态</th>
|
|
<th width="180" class="sortable" onclick="sortBans('steamId')"><i class="fas fa-sort"></i> SteamID</th>
|
|
<th width="160" class="sortable" onclick="sortBans('timestamp')"><i class="fas fa-sort"></i> 到期时间</th>
|
|
<th width="120" class="sortable" onclick="sortBans('remaining')"><i class="fas fa-sort"></i> 剩余时间</th>
|
|
<th class="sortable" onclick="sortBans('reason')"><i class="fas fa-sort"></i> 封禁原因</th>
|
|
<th width="100" class="sortable" onclick="sortBans('operator')"><i class="fas fa-sort"></i> 操作人</th>
|
|
<th width="160" class="sortable" onclick="sortBans('unbanTime')"><i class="fas fa-sort"></i> 解封时间</th>
|
|
<th width="100" class="sortable" onclick="sortBans('unbanOperator')"><i class="fas fa-sort"></i> 解封人</th>
|
|
<th width="100">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ban-list">
|
|
<!-- Ban items here -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add/Edit Ban Modal -->
|
|
<div class="modal-overlay" id="ban-modal">
|
|
<div class="modal">
|
|
<h3 class="modal-title" id="modal-title">新增封禁</h3>
|
|
<input type="hidden" id="edit-index">
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">SteamID</label>
|
|
<input type="text" class="form-input" id="ban-steamid" placeholder="例如: 00029f57b77c...">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">封禁原因</label>
|
|
<input type="text" class="form-input" id="ban-reason" placeholder="例如: Teamkill">
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group" style="flex: 1;">
|
|
<label class="form-label">封禁时长</label>
|
|
<select class="form-select" id="ban-duration" onchange="updateCustomDate()">
|
|
<option value="3600">1 小时</option>
|
|
<option value="86400">1 天</option>
|
|
<option value="604800">1 周</option>
|
|
<option value="2592000">1 个月</option>
|
|
<option value="31536000">1 年</option>
|
|
<option value="0">永久</option>
|
|
<option value="custom">自定义时间</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group" id="custom-date-group" style="display: none;">
|
|
<label class="form-label">自定义到期时间</label>
|
|
<input type="datetime-local" class="form-input" id="ban-custom-date">
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeModal('ban-modal')"><i class="fas fa-times"></i> 取消</button>
|
|
<button class="btn btn-primary" onclick="confirmSaveBan()"><i class="fas fa-check"></i> 确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Permission Check Helper
|
|
window.checkPermission = function(key) {
|
|
const isSuper = localStorage.getItem('isSuperAdmin') === 'true';
|
|
if (isSuper) return true;
|
|
try {
|
|
const perms = JSON.parse(localStorage.getItem('permissions') || '[]');
|
|
return perms.includes('all') || perms.includes(key);
|
|
} catch { return false; }
|
|
};
|
|
|
|
(function() {
|
|
const required = 'bans.view';
|
|
if (!checkPermission(required)) {
|
|
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;color:red;font-size:1.5rem;">Access Denied: Permission "bans.view" required.</div>';
|
|
throw new Error("Access Denied");
|
|
}
|
|
})();
|
|
|
|
const CONFIG_FILENAME = 'Bans.cfg';
|
|
let bans = []; // { steamId, timestamp, reason }
|
|
let isEditing = false;
|
|
let hasUnsavedChanges = false;
|
|
let sortConfig = { key: 'timestamp', direction: 'desc' };
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadBans();
|
|
|
|
// Button Permissions
|
|
if (!checkPermission('bans.add')) {
|
|
const btn = document.querySelector('button[onclick="showAddBanModal()"]');
|
|
if (btn) btn.style.display = 'none';
|
|
}
|
|
if (!checkPermission('bans.edit')) {
|
|
const btn = document.getElementById('btnSync');
|
|
if (btn) btn.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
let currentPage = 1;
|
|
let currentSearch = '';
|
|
let currentSort = { field: 'id', desc: true };
|
|
|
|
// Load sync status on start
|
|
updateSyncStatus();
|
|
// Update sync status every minute
|
|
setInterval(updateSyncStatus, 60000);
|
|
|
|
async function updateSyncStatus() {
|
|
try {
|
|
const response = await fetch('/api/bans/sync-status');
|
|
if (!response.ok) return;
|
|
const data = await response.json();
|
|
|
|
const btn = document.getElementById('btnSync');
|
|
if (data.nextSync && data.nextSync !== '0001-01-01T00:00:00') {
|
|
const nextDate = new Date(data.nextSync);
|
|
const timeStr = nextDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
btn.innerHTML = `<i class="fas fa-file-export"></i> 同步到 Ban.cfg (下次: ${timeStr})`;
|
|
btn.title = `上次同步: ${new Date(data.lastSync).toLocaleString()}\n下次同步: ${new Date(data.nextSync).toLocaleString()}`;
|
|
} else {
|
|
// Initial state or just started
|
|
btn.innerHTML = `<i class="fas fa-file-export"></i> 同步到 Ban.cfg (即将同步)`;
|
|
}
|
|
} catch(e) {
|
|
console.error("Failed to fetch sync status", e);
|
|
}
|
|
}
|
|
|
|
async function loadBans() {
|
|
const status = document.getElementById('status-msg');
|
|
status.textContent = '正在加载...';
|
|
|
|
try {
|
|
// Fetch from new API
|
|
const response = await fetch('/api/bans?page=1&pageSize=500'); // Fetch up to 500 for now
|
|
if (!response.ok) throw new Error('Load failed');
|
|
|
|
const data = await response.json();
|
|
bans = data.list || [];
|
|
|
|
renderBans();
|
|
status.textContent = '加载成功';
|
|
setTimeout(() => status.textContent = '', 2000);
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
status.textContent = '加载失败: ' + err.message;
|
|
status.style.color = 'red';
|
|
}
|
|
}
|
|
|
|
// parseBans removed
|
|
// saveBans removed
|
|
|
|
function renderBans() {
|
|
const list = document.getElementById('ban-list');
|
|
list.innerHTML = '';
|
|
let displayedCount = 0;
|
|
const filter = document.getElementById('search-input').value.toLowerCase();
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
// Client-side sorting
|
|
bans.sort((a, b) => {
|
|
let valA, valB;
|
|
let key = sortConfig.key;
|
|
if (key === 'timestamp') key = 'expiryTimestamp';
|
|
|
|
if (key === 'status') {
|
|
// 0=active, 1=expired, 2=unbanned
|
|
valA = a.status !== undefined ? a.status : 0;
|
|
valB = b.status !== undefined ? b.status : 0;
|
|
} else {
|
|
valA = a[key];
|
|
valB = b[key];
|
|
}
|
|
|
|
if (typeof valA === 'string') valA = valA.toLowerCase();
|
|
if (typeof valB === 'string') valB = valB.toLowerCase();
|
|
|
|
if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1;
|
|
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
|
|
bans.forEach((ban) => {
|
|
if (filter && !ban.steamId.toLowerCase().includes(filter) && !ban.reason.toLowerCase().includes(filter)) {
|
|
return;
|
|
}
|
|
|
|
displayedCount++;
|
|
|
|
const isPermanent = ban.expiryTimestamp === 0;
|
|
// status: 0=Active, 1=Expired, 2=Unbanned
|
|
let status = ban.status !== undefined ? ban.status : 0;
|
|
if (status === 0 && !isPermanent && ban.expiryTimestamp < now) {
|
|
status = 1; // Expired
|
|
}
|
|
|
|
let statusHtml = '';
|
|
let remainingStr = '-';
|
|
|
|
if (status === 2) {
|
|
statusHtml = '<span class="status-expired" style="background-color: #d93025; color: #fff;">已解封</span>';
|
|
remainingStr = '已解封';
|
|
} else if (status === 1) {
|
|
statusHtml = '<span class="status-expired">已过期</span>';
|
|
remainingStr = '0';
|
|
} else {
|
|
statusHtml = '<span class="status-active">封禁中</span>';
|
|
if (isPermanent) {
|
|
statusHtml = '<span class="status-active" style="background-color: #333; color: #fff;">永久封禁</span>';
|
|
remainingStr = '∞';
|
|
} else {
|
|
const diff = ban.expiryTimestamp - now;
|
|
const days = Math.floor(diff / 86400);
|
|
const hours = Math.floor((diff % 86400) / 3600);
|
|
if (days > 0) remainingStr = `${days}天 ${hours}小时`;
|
|
else remainingStr = `${hours}小时 ${Math.floor((diff % 3600) / 60)}分`;
|
|
}
|
|
}
|
|
|
|
const dateStr = isPermanent ? '永久' : new Date(ban.expiryTimestamp * 1000).toLocaleString();
|
|
const unbanDateStr = ban.unbanTime ? new Date(ban.unbanTime).toLocaleString() : '-';
|
|
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${statusHtml}</td>
|
|
<td class="steamid-text">${ban.steamId}</td>
|
|
<td>${dateStr}</td>
|
|
<td>${remainingStr}</td>
|
|
<td class="reason-text">${ban.reason || '-'}</td>
|
|
<td>${ban.operator || '-'}</td>
|
|
<td>${unbanDateStr}</td>
|
|
<td>${ban.unbanOperator || '-'}</td>
|
|
<td>
|
|
${status === 0 && checkPermission('bans.delete') ? `<button class="btn btn-secondary" style="padding: 4px 8px; font-size: 0.8rem; color: #d93025;" onclick="deleteBan('${ban.steamId}')"><i class="fas fa-trash"></i> 解封</button>` : ''}
|
|
</td>
|
|
`;
|
|
list.appendChild(tr);
|
|
});
|
|
|
|
if (displayedCount === 0) {
|
|
list.innerHTML = '<tr><td colspan="6" class="empty-state">暂无数据</td></tr>';
|
|
}
|
|
}
|
|
|
|
function sortBans(key) {
|
|
if (sortConfig.key === key) {
|
|
sortConfig.direction = sortConfig.direction === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
sortConfig.key = key;
|
|
sortConfig.direction = 'desc';
|
|
}
|
|
renderBans();
|
|
}
|
|
|
|
function updateSaveButtonState() {
|
|
const btn = document.getElementById('save-btn');
|
|
if (hasUnsavedChanges) {
|
|
if (!document.getElementById('unsaved-badge')) {
|
|
const badge = document.createElement('span');
|
|
badge.id = 'unsaved-badge';
|
|
badge.className = 'unsaved-badge';
|
|
badge.textContent = '未保存';
|
|
btn.appendChild(badge);
|
|
}
|
|
} else {
|
|
const badge = document.getElementById('unsaved-badge');
|
|
if (badge) badge.remove();
|
|
}
|
|
}
|
|
|
|
function markUnsaved() {
|
|
hasUnsavedChanges = true;
|
|
updateSaveButtonState();
|
|
}
|
|
|
|
function filterBans() {
|
|
renderBans();
|
|
}
|
|
|
|
// Modal Functions
|
|
function showAddBanModal() {
|
|
isEditing = false;
|
|
document.getElementById('modal-title').textContent = '新增封禁';
|
|
document.getElementById('ban-steamid').value = '';
|
|
document.getElementById('ban-steamid').disabled = false;
|
|
document.getElementById('ban-reason').value = '';
|
|
document.getElementById('ban-duration').value = '604800'; // Default 1 week
|
|
updateCustomDate();
|
|
|
|
document.getElementById('ban-modal').classList.add('show');
|
|
}
|
|
|
|
function editBan(index) {
|
|
isEditing = true;
|
|
const ban = bans[index];
|
|
document.getElementById('modal-title').textContent = '编辑封禁';
|
|
document.getElementById('edit-index').value = index;
|
|
|
|
document.getElementById('ban-steamid').value = ban.steamId;
|
|
document.getElementById('ban-steamid').disabled = false; // Allow editing
|
|
document.getElementById('ban-reason').value = ban.reason;
|
|
|
|
// Try to match duration or set custom
|
|
const now = Math.floor(Date.now() / 1000);
|
|
|
|
if (ban.timestamp === 0) {
|
|
document.getElementById('ban-duration').value = '0';
|
|
updateCustomDate();
|
|
} else {
|
|
// Check if standard duration matches roughly (not perfect, but good for UI)
|
|
// Or just set to custom date
|
|
document.getElementById('ban-duration').value = 'custom';
|
|
updateCustomDate();
|
|
|
|
const date = new Date(ban.timestamp * 1000);
|
|
const iso = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 16);
|
|
document.getElementById('ban-custom-date').value = iso;
|
|
}
|
|
|
|
document.getElementById('ban-modal').classList.add('show');
|
|
}
|
|
|
|
function updateCustomDate() {
|
|
const duration = document.getElementById('ban-duration').value;
|
|
const customGroup = document.getElementById('custom-date-group');
|
|
|
|
if (duration === 'custom') {
|
|
customGroup.style.display = 'block';
|
|
} else {
|
|
customGroup.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function confirmSaveBan() {
|
|
const steamId = document.getElementById('ban-steamid').value.trim();
|
|
const reason = document.getElementById('ban-reason').value.trim();
|
|
const durationType = document.getElementById('ban-duration').value;
|
|
|
|
if (!steamId) return alert('请输入 SteamID');
|
|
|
|
let duration = 'perm';
|
|
if (durationType === 'custom') {
|
|
const dateStr = document.getElementById('ban-custom-date').value;
|
|
if (!dateStr) return alert('请选择到期时间');
|
|
const target = new Date(dateStr).getTime() / 1000;
|
|
const now = Date.now() / 1000;
|
|
const seconds = Math.floor(target - now);
|
|
if (seconds <= 0) return alert('到期时间必须在未来');
|
|
duration = seconds.toString();
|
|
} else {
|
|
let seconds = parseInt(durationType, 10);
|
|
if (seconds === 0) duration = 'perm';
|
|
else duration = seconds.toString();
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/players/ban', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
steamId: steamId,
|
|
reason: reason,
|
|
duration: duration
|
|
})
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to ban');
|
|
|
|
loadBans();
|
|
closeModal('ban-modal');
|
|
alert('封禁成功');
|
|
} catch(e) {
|
|
alert('封禁失败: ' + e.message);
|
|
}
|
|
}
|
|
|
|
async function deleteBan(steamId) {
|
|
if (confirm('确定要解封该玩家吗?')) {
|
|
try {
|
|
const response = await fetch('/api/bans/unban', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ steamId: steamId })
|
|
});
|
|
if (!response.ok) throw new Error('Unban failed');
|
|
|
|
loadBans();
|
|
alert('解封成功');
|
|
} catch(e) {
|
|
alert('解封失败: ' + e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function syncBans() {
|
|
const btn = event.target.closest('button');
|
|
const originalText = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 同步中...';
|
|
|
|
try {
|
|
const response = await fetch('/api/bans/sync', { method: 'POST' });
|
|
if (!response.ok) throw new Error('Sync failed');
|
|
|
|
const data = await response.json();
|
|
alert('同步成功');
|
|
updateSyncStatus(); // Refresh status immediately
|
|
} catch(e) {
|
|
alert('同步失败: ' + e.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalText;
|
|
}
|
|
}
|
|
|
|
// saveBans function removed as it is now handled by API directly
|
|
|
|
async function executeRconCommand(cmd) {
|
|
try {
|
|
console.log('Sending RCON:', cmd);
|
|
await fetch('/api/command', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command: cmd })
|
|
});
|
|
} catch (e) {
|
|
console.error('RCON Command failed:', e);
|
|
}
|
|
}
|
|
|
|
function closeModal(id) {
|
|
document.getElementById(id).classList.remove('show');
|
|
}
|
|
|
|
// Close modal on outside click
|
|
window.onclick = function (event) {
|
|
if (event.target.classList.contains('modal-overlay')) {
|
|
event.target.classList.remove('show');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |