Files
squad-rain-ops-mini/wwwroot/admin-manager.html

863 lines
29 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 {
/* Modern Palette */
--primary-color: #4361ee;
--secondary-color: #3f37c9;
--success-color: #4caf50;
--info-color: #00b4d8;
--warning-color: #f72585;
--danger-color: #f44336;
--bg-color: #f8f9fa;
--card-bg: #ffffff;
--text-primary: #2b2d42;
--text-secondary: #8d99ae;
--border-color: #edf2f4;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
margin: 0;
padding: 24px 40px;
display: flex;
flex-direction: column;
height: 100vh;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.title {
font-size: 1.5rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 12px;
color: var(--text-primary);
}
.btn {
padding: 8px 18px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
font-weight: 600;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
box-shadow: 0 2px 4px rgba(67, 97, 238, 0.3);
}
.btn-primary:hover {
background-color: var(--secondary-color);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(67, 97, 238, 0.4);
}
.btn-danger {
background-color: white;
color: var(--danger-color);
border: 1px solid var(--danger-color);
}
.btn-danger:hover {
background-color: #fff5f5;
}
.btn-secondary {
background-color: #fff;
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.btn-secondary:hover {
background-color: #f8f9fa;
color: var(--text-primary);
}
.container {
display: flex;
gap: 24px;
flex: 1;
overflow: hidden;
}
.panel {
background: var(--card-bg);
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
display: flex;
flex-direction: column;
border: 1px solid var(--border-color);
overflow: hidden;
}
.panel-left {
width: 350px;
flex-shrink: 0;
}
.panel-right {
flex: 1;
}
.panel-header {
padding: 15px;
border-bottom: 1px solid var(--border-color);
font-weight: 600;
background: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 0;
}
/* Group List */
.group-item {
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
}
.group-item:hover {
background-color: #f0f7ff;
}
.group-item.active {
background-color: #e6f0ff;
border-left: 4px solid var(--primary-color);
}
.group-name {
font-weight: 500;
}
.group-perms-count {
font-size: 0.8em;
color: #888;
background: #eee;
padding: 2px 6px;
border-radius: 10px;
}
/* Permissions Grid */
.perm-editor {
padding: 20px;
border-top: 1px solid var(--border-color);
background: #fafafa;
display: none;
/* Hidden by default */
}
.perm-editor.active {
display: block;
}
.perm-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 10px;
}
.perm-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
cursor: pointer;
}
/* Admin List */
.admin-group-section {
margin-bottom: 20px;
}
.admin-group-header {
padding: 10px 20px;
background-color: #f0f7ff;
color: var(--primary-color);
font-weight: 600;
font-size: 0.95rem;
border-bottom: 1px solid #e1dfdd;
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-row {
display: flex;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid #f0f0f0;
}
.admin-row:hover {
background-color: #fafafa;
}
.admin-info {
flex: 1;
}
.admin-steamid {
font-family: monospace;
color: #666;
font-size: 0.9rem;
}
.admin-name {
font-weight: 500;
margin-left: 10px;
}
.admin-actions {
display: flex;
gap: 5px;
}
.status-msg {
margin-right: 15px;
font-size: 0.9rem;
}
/* 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: 400px;
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-hint {
font-size: 0.8rem;
color: #888;
margin-top: 4px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 25px;
}
.empty-state {
padding: 40px;
text-align: center;
color: #999;
}
</style>
</head>
<body>
<div class="header">
<div class="title">
<span><i class="fas fa-user-shield"></i></span> 权限管理
</div>
<div>
<span id="status-msg" class="status-msg"></span>
<button class="btn btn-primary" id="btn-save-config" onclick="saveConfig()">
<i class="fas fa-save"></i> 保存配置
</button>
</div>
</div>
<div class="container">
<!-- Left Panel: Groups -->
<div class="panel panel-left">
<div class="panel-header">
<span>权限组</span>
<button class="btn btn-secondary" id="btn-add-group" style="padding: 4px 8px; font-size: 0.8rem;"
onclick="showAddGroupModal()">+ 新建组</button>
</div>
<div class="panel-content" id="group-list">
<!-- Group items here -->
</div>
<div class="perm-editor" id="perm-editor">
<div style="margin-bottom: 10px; font-weight: bold; display:flex; justify-content:space-between;">
<span id="current-group-name">Selected Group</span>
<button class="btn btn-danger" id="btn-delete-group" style="padding: 2px 6px; font-size: 0.7rem;"
onclick="deleteCurrentGroup()">删除</button>
</div>
<div class="perm-grid" id="perm-grid">
<!-- Checkboxes generated by JS -->
</div>
</div>
</div>
<!-- Right Panel: Admins -->
<div class="panel panel-right">
<div class="panel-header">
<span>管理员列表</span>
<button class="btn btn-primary" id="btn-add-admin" style="padding: 6px 12px; font-size: 0.85rem;"
onclick="showAddAdminModal()">+ 添加管理员</button>
</div>
<div class="panel-content" id="admin-list">
<!-- Admin groups here -->
</div>
</div>
</div>
<!-- Add Group Modal -->
<div class="modal-overlay" id="add-group-modal">
<div class="modal">
<h3 class="modal-title">新建权限组</h3>
<div class="form-group">
<label class="form-label">组名称</label>
<input type="text" class="form-input" id="new-group-name" placeholder="例如: SuperAdmin">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('add-group-modal')">取消</button>
<button class="btn btn-primary" onclick="confirmAddGroup()">创建</button>
</div>
</div>
</div>
<!-- Add Admin Modal -->
<div class="modal-overlay" id="add-admin-modal">
<div class="modal">
<h3 class="modal-title">添加管理员</h3>
<div class="form-group">
<label class="form-label">SteamID64</label>
<input type="text" class="form-input" id="new-admin-steamid" placeholder="例如: 76561198...">
<div class="form-hint"><a href="https://steamid.io/" target="_blank">查询 SteamID</a></div>
</div>
<div class="form-group">
<label class="form-label">备注</label>
<input type="text" class="form-input" id="new-admin-comment" placeholder="例如: 玩家A">
</div>
<div class="form-group">
<label class="form-label">分配组</label>
<select class="form-select" id="new-admin-group">
<!-- Options populated by JS -->
</select>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('add-admin-modal')">取消</button>
<button class="btn btn-primary" onclick="confirmAddAdmin()">添加</button>
</div>
</div>
</div>
<script>
// Permission Check
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 = 'admins.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 "admins.view" required.</div>';
throw new Error("Access Denied");
}
})();
// Constants
const PERMISSIONS = [
'changemap', 'pause', 'cheat', 'private', 'balance', 'chat',
'kick', 'ban', 'config', 'cameraman', 'immune', 'manageserver',
'reserve', 'debug', 'teamchange', 'forceteamchange', 'canseeadminchat'
];
const PERM_DESCRIPTIONS = {
'changemap': '更换/预设地图',
'pause': '暂停游戏',
'cheat': '使用作弊命令',
'private': '设置服务器密码',
'balance': '忽略服务器阵营平衡',
'chat': '管理员聊天/服务器公告',
'kick': '踢出玩家',
'ban': '封禁玩家',
'config': '更改服务器配置',
'cameraman': '摄影机 - 管理员视角',
'immune': '无法被 踢出/封禁',
'manageserver': '关闭服务器',
'reserve': '预留位',
'debug': '调试',
'teamchange': '忽略更换阵营时间限制',
'forceteamchange': '允许执行强制更换阵营命令',
'canseeadminchat': '查看 管理员聊天/友军击杀/管理员加入 消息'
};
const CONFIG_FILENAME = 'Admins.cfg';
// State
let groups = {}; // { name: [perms] }
let admins = []; // { steamId, group, comment }
let currentGroup = null;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initPermGrid();
loadConfig();
// Apply button permissions
if (!checkPermission('admins.add')) {
const btnAddGroup = document.getElementById('btn-add-group');
if (btnAddGroup) btnAddGroup.style.display = 'none';
const btnAddAdmin = document.getElementById('btn-add-admin');
if (btnAddAdmin) btnAddAdmin.style.display = 'none';
}
if (!checkPermission('admins.edit')) {
const btnSave = document.getElementById('btn-save-config');
if (btnSave) btnSave.style.display = 'none';
}
});
function initPermGrid() {
const grid = document.getElementById('perm-grid');
grid.innerHTML = '';
const canEdit = checkPermission('admins.edit');
PERMISSIONS.forEach(perm => {
const label = document.createElement('label');
label.className = 'perm-checkbox';
const desc = PERM_DESCRIPTIONS[perm] || '';
label.innerHTML = `
<input type="checkbox" value="${perm}" onchange="updateGroupPerms()" ${canEdit ? '' : 'disabled'}>
<div>
<div style="font-weight: 500;">${perm}</div>
<div style="font-size: 0.85em; color: #666;">${desc}</div>
</div>
`;
grid.appendChild(label);
});
}
async function loadConfig() {
const status = document.getElementById('status-msg');
status.textContent = '正在加载配置...';
try {
const response = await fetch(`/api/config/read?filename=${encodeURIComponent(CONFIG_FILENAME)}`);
if (!response.ok) {
// Try to load default/empty if not found
if (response.status === 404) {
console.log('File not found, starting fresh');
groups = {
'SuperAdmin': [...PERMISSIONS],
'Admin': ['changemap', 'balance', 'chat', 'kick', 'ban', 'cameraman', 'pause'],
'Moderator': ['changemap', 'chat', 'kick', 'ban']
};
admins = [];
renderAll();
status.textContent = '新建默认配置';
return;
}
throw new Error('Load failed');
}
const text = await response.text();
parseConfig(text);
// If no groups found (empty or invalid file), load defaults
if (Object.keys(groups).length === 0) {
console.log('No groups found, loading defaults');
groups = {
'SuperAdmin': ['changemap', 'cheat', 'private', 'balance', 'chat', 'kick', 'ban', 'config', 'cameraman', 'debug', 'pause'],
'Admin': ['changemap', 'balance', 'chat', 'kick', 'ban', 'cameraman', 'pause'],
'Moderator': ['changemap', 'chat', 'kick', 'ban'],
'Whitelist': ['reserve']
};
status.textContent = '配置无效,已加载默认值';
} else {
status.textContent = '配置已加载';
}
renderAll();
if (status.textContent === '配置已加载') {
setTimeout(() => status.textContent = '', 2000);
}
} catch (err) {
console.error(err);
status.textContent = '加载失败: ' + err.message;
status.style.color = 'var(--danger-color)';
}
}
function parseConfig(text) {
groups = {};
admins = [];
const lines = text.split('\n');
lines.forEach(line => {
line = line.trim();
if (!line || (line.startsWith('//') && !line.includes('Admin='))) return;
// Group=Name:perm1,perm2
const groupMatch = line.match(/^Group=(.*?):(.*?)(\/\/.*)?$/);
if (groupMatch) {
const name = groupMatch[1].trim();
const perms = groupMatch[2].split(',').map(p => p.trim()).filter(p => p);
groups[name] = perms;
return;
}
// Admin=SteamID:Group //Comment
// Flexible regex to handle comments
const adminMatch = line.match(/^Admin=(\d+):(.*?)(?:\s*\/\/(.*))?$/);
if (adminMatch) {
const steamId = adminMatch[1];
let groupPart = adminMatch[2].trim();
let comment = adminMatch[3] ? adminMatch[3].trim() : '';
// Handle case where comment starts without // immediately after group (unlikely but possible)
// But standard format is //
admins.push({ steamId, group: groupPart, comment });
}
});
}
async function saveConfig() {
const status = document.getElementById('status-msg');
status.textContent = '正在保存...';
status.style.color = '#666';
const content = generateConfig();
try {
const response = await fetch('/api/config/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: CONFIG_FILENAME,
content: content
})
});
if (!response.ok) throw new Error('Save failed');
status.textContent = '保存成功!';
status.style.color = 'var(--success-color)';
setTimeout(() => status.textContent = '', 3000);
} catch (err) {
status.textContent = '保存失败: ' + err.message;
status.style.color = 'var(--danger-color)';
}
}
function generateConfig() {
let lines = [];
// Groups
for (const [name, perms] of Object.entries(groups)) {
lines.push(`Group=${name}:${perms.join(',')}`);
}
lines.push('');
// Admins grouped by Group
const adminsByGroup = {};
// Ensure all defined groups exist as keys
for (const g in groups) adminsByGroup[g] = [];
// Also include groups that might be in admins but not in definitions (edge case)
admins.forEach(a => {
if (!adminsByGroup[a.group]) adminsByGroup[a.group] = [];
adminsByGroup[a.group].push(a);
});
for (const [groupName, list] of Object.entries(adminsByGroup)) {
if (list.length > 0) {
lines.push(`// ${groupName}`);
list.forEach(a => {
let line = `Admin=${a.steamId}:${a.group}`;
if (a.comment) line += ` // ${a.comment}`;
lines.push(line);
});
lines.push('');
}
}
return lines.join('\n');
}
// Render Functions
function renderAll() {
renderGroups();
renderAdmins();
}
function renderGroups() {
const list = document.getElementById('group-list');
list.innerHTML = '';
Object.keys(groups).forEach(name => {
const el = document.createElement('div');
el.className = `group-item ${currentGroup === name ? 'active' : ''}`;
el.onclick = () => selectGroup(name);
el.innerHTML = `
<span class="group-name">${name}</span>
<span class="group-perms-count">${groups[name].length} 权限</span>
`;
list.appendChild(el);
});
}
function selectGroup(name) {
currentGroup = name;
document.getElementById('current-group-name').textContent = name;
document.getElementById('perm-editor').classList.add('active');
// Delete permission check
const btnDelete = document.getElementById('btn-delete-group');
if (btnDelete) {
btnDelete.style.display = checkPermission('admins.delete') ? 'inline-block' : 'none';
}
// Update UI
document.querySelectorAll('.group-item').forEach(el => {
if (el.querySelector('.group-name').textContent === name) el.classList.add('active');
else el.classList.remove('active');
});
// Update Checkboxes
const perms = groups[name] || [];
document.querySelectorAll('#perm-grid input').forEach(input => {
input.checked = perms.includes(input.value);
});
}
function updateGroupPerms() {
if (!currentGroup) return;
const inputs = document.querySelectorAll('#perm-grid input:checked');
groups[currentGroup] = Array.from(inputs).map(i => i.value);
renderGroups(); // Update count
// Re-select to keep styling
document.querySelectorAll('.group-item').forEach(el => {
if (el.querySelector('.group-name').textContent === currentGroup) el.classList.add('active');
});
}
function renderAdmins() {
const list = document.getElementById('admin-list');
list.innerHTML = '';
const adminsByGroup = {};
// Use groups order if possible
const groupNames = Object.keys(groups).sort();
groupNames.forEach(g => adminsByGroup[g] = []);
// Add any other groups found in admins
admins.forEach(a => {
if (!adminsByGroup[a.group]) adminsByGroup[a.group] = [];
adminsByGroup[a.group].push(a);
});
// Render
Object.entries(adminsByGroup).forEach(([groupName, adminList]) => {
if (adminList.length === 0) return;
const section = document.createElement('div');
section.className = 'admin-group-section';
section.innerHTML = `
<div class="admin-group-header">
<span>${groupName}</span>
<span style="font-size: 0.8em; font-weight: normal; color: #666;">${adminList.length} 人</span>
</div>
`;
adminList.forEach(admin => {
const row = document.createElement('div');
row.className = 'admin-row';
row.innerHTML = `
<div class="admin-info">
<div>
<span class="admin-steamid">${admin.steamId}</span>
<span class="admin-name">${admin.comment || '无备注'}</span>
</div>
</div>
<div class="admin-actions">
${checkPermission('admins.delete') ?
`<button class="btn btn-danger" style="padding: 4px 8px; font-size: 0.8rem;"
onclick="deleteAdmin('${admin.steamId}')">移除</button>` : ''}
</div>
`;
section.appendChild(row);
});
list.appendChild(section);
});
if (admins.length === 0) {
list.innerHTML = '<div class="empty-state">暂无管理员,请点击右上角添加</div>';
}
}
// Actions
function showAddGroupModal() {
document.getElementById('new-group-name').value = '';
document.getElementById('add-group-modal').classList.add('show');
}
function confirmAddGroup() {
const name = document.getElementById('new-group-name').value.trim();
if (!name) return alert('请输入组名称');
if (groups[name]) return alert('组名已存在');
groups[name] = [];
renderGroups();
selectGroup(name);
closeModal('add-group-modal');
}
function deleteCurrentGroup() {
if (!checkPermission('admins.delete')) return;
if (!currentGroup) return;
if (!confirm(`确定要删除权限组 "${currentGroup}" 吗?\n所有属于该组的管理员也将失去权限。`)) return;
delete groups[currentGroup];
currentGroup = null;
document.getElementById('perm-editor').classList.remove('active');
renderAll();
}
function showAddAdminModal() {
document.getElementById('new-admin-steamid').value = '';
document.getElementById('new-admin-comment').value = '';
const select = document.getElementById('new-admin-group');
select.innerHTML = '';
Object.keys(groups).forEach(g => {
const opt = document.createElement('option');
opt.value = g;
opt.textContent = g;
select.appendChild(opt);
});
if (currentGroup) select.value = currentGroup;
document.getElementById('add-admin-modal').classList.add('show');
}
function confirmAddAdmin() {
const steamId = document.getElementById('new-admin-steamid').value.trim();
const comment = document.getElementById('new-admin-comment').value.trim();
const group = document.getElementById('new-admin-group').value;
if (!steamId || !group) return alert('请填写完整信息');
if (!/^\d+$/.test(steamId)) return alert('SteamID 必须是数字');
// Check duplicate
const exists = admins.find(a => a.steamId === steamId);
if (exists) {
if (!confirm(`该 SteamID 已存在 (组: ${exists.group}),要更新吗?`)) return;
exists.group = group;
exists.comment = comment;
} else {
admins.push({ steamId, group, comment });
}
renderAdmins();
closeModal('add-admin-modal');
}
function deleteAdmin(steamId) {
if (!checkPermission('admins.delete')) return;
if (!confirm('确定要移除该管理员吗?')) return;
admins = admins.filter(a => a.steamId !== steamId);
renderAdmins();
}
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>