Files
squad-rain-ops-mini/wwwroot/map-manager.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>