mirror of
https://gitee.com/xiarenalofs/squad-rain-ops-mini.git
synced 2026-08-05 21:15:52 +08:00
506 lines
18 KiB
HTML
506 lines
18 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>地图管理 - RainOps Mini</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;
|
|
--header-bg: #f8f9fa;
|
|
--secondary-color: #6c757d;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
margin: 0;
|
|
padding: 0;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.navbar {
|
|
background-color: var(--card-bg);
|
|
padding: 0 20px;
|
|
height: 50px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
z-index: 100;
|
|
}
|
|
|
|
.navbar-brand {
|
|
font-weight: 600;
|
|
font-size: 1.1rem;
|
|
color: var(--primary-color);
|
|
text-decoration: none;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.container {
|
|
flex: 1;
|
|
padding: 20px;
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
|
padding: 25px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.1rem;
|
|
font-weight: 600;
|
|
color: var(--primary-color);
|
|
margin-bottom: 20px;
|
|
border-left: 4px solid var(--primary-color);
|
|
padding-left: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
label {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
color: #555;
|
|
}
|
|
|
|
select, input {
|
|
padding: 10px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
font-size: 0.95rem;
|
|
background: #fff;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
select:focus, input:focus {
|
|
outline: none;
|
|
border-color: var(--primary-color);
|
|
}
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
gap: 15px;
|
|
margin-top: 10px;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
button {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.btn-primary {
|
|
background-color: var(--primary-color);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background-color: #005a9e;
|
|
}
|
|
|
|
.btn-danger {
|
|
background-color: #d93025;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background-color: #a50e0e;
|
|
}
|
|
|
|
.command-preview {
|
|
background: #f8f9fa;
|
|
border: 1px solid var(--border-color);
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
font-family: 'Consolas', monospace;
|
|
margin-top: 20px;
|
|
word-break: break-all;
|
|
color: #333;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.copy-btn {
|
|
background: transparent;
|
|
color: var(--primary-color);
|
|
padding: 5px 10px;
|
|
font-size: 0.85rem;
|
|
border: 1px solid var(--primary-color);
|
|
}
|
|
|
|
.copy-btn:hover {
|
|
background: #f0f8ff;
|
|
}
|
|
|
|
#message-area {
|
|
margin-top: 15px;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
display: none;
|
|
}
|
|
|
|
.msg-success {
|
|
background-color: #d4edda;
|
|
color: #155724;
|
|
border: 1px solid #c3e6cb;
|
|
}
|
|
|
|
.msg-error {
|
|
background-color: #f8d7da;
|
|
color: #721c24;
|
|
border: 1px solid #f5c6cb;
|
|
}
|
|
</style>
|
|
<script>
|
|
// Permission Check moved to body to ensure document.body exists
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<script>
|
|
(function() {
|
|
const required = 'maps';
|
|
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="container">
|
|
<div class="card">
|
|
<div class="section-title"><i class="fas fa-globe-americas" style="margin-right: 8px;"></i> 选择地图及其模式</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>选择地图</label>
|
|
<select id="map-select" onchange="onMapChange()">
|
|
<option value="">正在加载...</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>选择地图层级 (Layer)</label>
|
|
<select id="layer-select" onchange="updatePreview()">
|
|
<option value="">请先选择地图</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="section-title"><i class="fas fa-shield-alt" style="margin-right: 8px;"></i> Team 1 派别兵种选择</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>选择派别 (Faction)</label>
|
|
<select id="t1-faction-select" onchange="onFactionChange(1)">
|
|
<option value="">正在加载...</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>选择兵种部队 (Unit)</label>
|
|
<select id="t1-unit-select" onchange="updatePreview()">
|
|
<option value="">请先选择派别</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="section-title"><i class="fas fa-shield-alt" style="margin-right: 8px;"></i> Team 2 派别兵种选择</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label>选择派别 (Faction)</label>
|
|
<select id="t2-faction-select" onchange="onFactionChange(2)">
|
|
<option value="">正在加载...</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>选择兵种部队 (Unit)</label>
|
|
<select id="t2-unit-select" onchange="updatePreview()">
|
|
<option value="">请先选择派别</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="section-title"><i class="fas fa-terminal" style="margin-right: 8px;"></i> 命令生成与执行</div>
|
|
|
|
<div class="command-preview" id="preview-container">
|
|
<span id="cmd-preview-text">等待选择完整参数...</span>
|
|
<button class="copy-btn" onclick="copyCommand()"><i class="fas fa-copy"></i> 复制</button>
|
|
</div>
|
|
|
|
<div id="message-area"></div>
|
|
|
|
<div class="btn-group">
|
|
<button class="btn-primary" onclick="sendCommand('AdminSetNextLayer')"><i class="fas fa-hourglass-start"></i> 设置下一张地图</button>
|
|
<button class="btn-danger" onclick="sendCommand('AdminChangeLayer')"><i class="fas fa-exchange-alt"></i> 立即更换地图</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let biomesData = {};
|
|
let battlegroupsData = {};
|
|
let allFactions = [];
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await loadData();
|
|
});
|
|
|
|
async function loadData() {
|
|
try {
|
|
// Load Biomes (Maps & Layers)
|
|
const biomesResp = await fetch('/mapdata/Biomes.json');
|
|
biomesData = await biomesResp.json();
|
|
|
|
// Load Battlegroups (Factions & Units)
|
|
const bgResp = await fetch('/mapdata/Battlegroups.json');
|
|
battlegroupsData = await bgResp.json();
|
|
|
|
processFactions();
|
|
initDropdowns();
|
|
} catch (err) {
|
|
showMessage('加载数据失败: ' + err.message, 'error');
|
|
}
|
|
}
|
|
|
|
function processFactions() {
|
|
allFactions = [];
|
|
// Flatten factions from all alliances (BLUFOR, REDFOR, etc.)
|
|
for (const alliance in battlegroupsData) {
|
|
const factions = battlegroupsData[alliance];
|
|
if (Array.isArray(factions)) {
|
|
factions.forEach(f => {
|
|
// Avoid duplicates if any
|
|
if (!allFactions.find(x => x.faction === f.faction)) {
|
|
allFactions.push(f);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// Sort by faction code
|
|
allFactions.sort((a, b) => a.faction.localeCompare(b.faction));
|
|
}
|
|
|
|
function initDropdowns() {
|
|
// Populate Map Select
|
|
const mapSelect = document.getElementById('map-select');
|
|
mapSelect.innerHTML = '<option value="">-- 请选择地图 --</option>';
|
|
|
|
Object.keys(biomesData).sort().forEach(mapName => {
|
|
const opt = document.createElement('option');
|
|
opt.value = mapName;
|
|
opt.innerText = mapName;
|
|
mapSelect.appendChild(opt);
|
|
});
|
|
|
|
// Populate Faction Selects
|
|
const populateFaction = (selectId) => {
|
|
const select = document.getElementById(selectId);
|
|
select.innerHTML = '<option value="">-- 请选择派别 --</option>';
|
|
allFactions.forEach(f => {
|
|
const opt = document.createElement('option');
|
|
opt.value = f.faction;
|
|
// Try to find a nice name if available, or just use code
|
|
// The JSON structure is: { "faction": "ADF", "units": {...} }
|
|
// It doesn't seem to have full names in this file snippet, so we use code.
|
|
opt.innerText = f.faction;
|
|
select.appendChild(opt);
|
|
});
|
|
};
|
|
|
|
populateFaction('t1-faction-select');
|
|
populateFaction('t2-faction-select');
|
|
}
|
|
|
|
function onMapChange() {
|
|
const mapName = document.getElementById('map-select').value;
|
|
const layerSelect = document.getElementById('layer-select');
|
|
|
|
layerSelect.innerHTML = '<option value="">-- 请选择层级 --</option>';
|
|
|
|
if (mapName && biomesData[mapName]) {
|
|
const layers = biomesData[mapName].layers || [];
|
|
layers.forEach(layer => {
|
|
const opt = document.createElement('option');
|
|
opt.value = layer;
|
|
opt.innerText = layer;
|
|
layerSelect.appendChild(opt);
|
|
});
|
|
}
|
|
updatePreview();
|
|
}
|
|
|
|
function onFactionChange(teamNum) {
|
|
const factionCode = document.getElementById(`t${teamNum}-faction-select`).value;
|
|
const unitSelect = document.getElementById(`t${teamNum}-unit-select`);
|
|
|
|
unitSelect.innerHTML = '<option value="">-- 请选择兵种 --</option>';
|
|
|
|
if (factionCode) {
|
|
const factionObj = allFactions.find(f => f.faction === factionCode);
|
|
if (factionObj && factionObj.units) {
|
|
// Units is a dict: "Air Assault": [...]
|
|
Object.keys(factionObj.units).sort().forEach(unitName => {
|
|
const opt = document.createElement('option');
|
|
// The command format requires removing spaces from unit name?
|
|
// User example: "AirAssault" from "Air Assault"
|
|
// So we store the raw name in text, and cleaned name in value?
|
|
// Or just handle cleaning in generation. Let's store raw key as value for now.
|
|
opt.value = unitName;
|
|
opt.innerText = unitName;
|
|
unitSelect.appendChild(opt);
|
|
});
|
|
}
|
|
}
|
|
updatePreview();
|
|
}
|
|
|
|
function getCommandParams() {
|
|
const layer = document.getElementById('layer-select').value;
|
|
|
|
const t1Faction = document.getElementById('t1-faction-select').value;
|
|
const t1UnitRaw = document.getElementById('t1-unit-select').value;
|
|
|
|
const t2Faction = document.getElementById('t2-faction-select').value;
|
|
const t2UnitRaw = document.getElementById('t2-unit-select').value;
|
|
|
|
if (!layer || !t1Faction || !t1UnitRaw || !t2Faction || !t2UnitRaw) {
|
|
return null;
|
|
}
|
|
|
|
// Format: Faction+Unit(NoSpaces)
|
|
const t1Unit = t1UnitRaw.replace(/\s+/g, '');
|
|
const t2Unit = t2UnitRaw.replace(/\s+/g, '');
|
|
|
|
return {
|
|
layer,
|
|
team1: `${t1Faction}+${t1Unit}`,
|
|
team2: `${t2Faction}+${t2Unit}`
|
|
};
|
|
}
|
|
|
|
function updatePreview() {
|
|
const params = getCommandParams();
|
|
const previewEl = document.getElementById('cmd-preview-text');
|
|
|
|
if (params) {
|
|
// Default showing SetNextLayer as preview
|
|
previewEl.innerText = `AdminSetNextLayer ${params.layer} ${params.team1} ${params.team2}`;
|
|
previewEl.style.color = '#333';
|
|
} else {
|
|
previewEl.innerText = '请完整选择所有选项以生成命令...';
|
|
previewEl.style.color = '#999';
|
|
}
|
|
}
|
|
|
|
function copyCommand() {
|
|
const text = document.getElementById('cmd-preview-text').innerText;
|
|
if (!text || text.includes('...')) return;
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
showMessage('命令已复制到剪贴板', 'success');
|
|
}).catch(err => {
|
|
showMessage('复制失败', 'error');
|
|
});
|
|
}
|
|
|
|
async function sendCommand(cmdType) {
|
|
const params = getCommandParams();
|
|
if (!params) {
|
|
showMessage('请先完成所有选项的选择', 'error');
|
|
return;
|
|
}
|
|
|
|
const fullCommand = `${cmdType} ${params.layer} ${params.team1} ${params.team2}`;
|
|
|
|
try {
|
|
const response = await fetch('/api/map/change', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command: fullCommand })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.text();
|
|
showMessage(`命令已发送: ${result}`, 'success');
|
|
} else {
|
|
showMessage(`发送失败: ${response.status}`, 'error');
|
|
}
|
|
} catch (err) {
|
|
showMessage(`网络错误: ${err.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
function showMessage(msg, type) {
|
|
const el = document.getElementById('message-area');
|
|
el.innerText = msg;
|
|
el.className = type === 'success' ? 'msg-success' : 'msg-error';
|
|
el.style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
el.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|