Files
fast_install/api/admin/index.php
T

744 lines
34 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
session_start();
require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../db.php';
require_once __DIR__ . '/../helpers.php';
// 登录检查
function isLoggedIn() {
return isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
}
// 处理登录
if (isset($_POST['action']) && $_POST['action'] === 'login') {
$user = $_POST['username'] ?? '';
$pass = $_POST['password'] ?? '';
if ($user === ADMIN_USER && $pass === ADMIN_PASS) {
$_SESSION['admin_logged_in'] = true;
header('Location: index.php');
exit;
}
$loginError = '用户名或密码错误';
}
// 处理登出
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
session_destroy();
header('Location: index.php');
exit;
}
// 未登录显示登录页
if (!isLoggedIn()) {
showLoginPage($loginError ?? '');
exit;
}
$db = getDb();
$page = $_GET['page'] ?? 'dashboard';
// AJAX 操作处理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax_action'])) {
handleAjaxAction($db, $_POST['ajax_action']);
}
// 页面路由
showAdminPage($db, $page);
// ========== AJAX 处理 ==========
function handleAjaxAction($db, $action) {
header('Content-Type: application/json; charset=utf-8');
switch ($action) {
case 'save_project':
$projectId = $_POST['project_id'] ?? '';
$displayName = $_POST['display_name'] ?? '';
$description = $_POST['description'] ?? '';
$iconUrl = $_POST['icon_url'] ?? '';
$enabled = isset($_POST['enabled']) ? 1 : 0;
$configJson = $_POST['config_json'] ?? '{}';
$editId = $_POST['edit_id'] ?? '';
// 验证 JSON
$parsed = json_decode($configJson, true);
if ($parsed === null && $configJson !== 'null') {
echo json_encode(['success' => false, 'message' => 'config_json 不是有效的 JSON']);
exit;
}
if ($editId) {
$db->update('projects', [
'display_name' => $displayName,
'description' => $description,
'icon_url' => $iconUrl,
'enabled' => $enabled,
'config_json' => $configJson,
], ['id' => $editId]);
} else {
$db->insert('projects', [
'project_id' => $projectId,
'display_name' => $displayName,
'description' => $description,
'icon_url' => $iconUrl,
'enabled' => $enabled,
'config_json' => $configJson,
]);
}
echo json_encode(['success' => true]);
exit;
case 'delete_project':
$id = $_POST['id'] ?? 0;
$db->delete('projects', ['id' => $id]);
echo json_encode(['success' => true]);
exit;
case 'save_installer_config':
$filenameRules = $_POST['filename_rules'] ?? '[]';
$commandRules = $_POST['command_rules'] ?? '[]';
$defaultProject = $_POST['default_project'] ?? 'r0_arena';
$defaultMode = $_POST['default_mode'] ?? 'full';
$db->update('installer_config', [
'filename_rules' => $filenameRules,
'command_rules' => $commandRules,
'default_project' => $defaultProject,
'default_mode' => $defaultMode,
], ['config_key' => 'global']);
echo json_encode(['success' => true]);
exit;
case 'save_version':
$projectId = $_POST['project_id'] ?? '';
$version = $_POST['version'] ?? '';
$downloadUrl = $_POST['download_url'] ?? '';
$fileSize = intval($_POST['file_size'] ?? 0);
$changelog = $_POST['changelog'] ?? '';
$threads = intval($_POST['threads'] ?? 8);
$extraDownloads = $_POST['extra_downloads'] ?? '{}';
$runtimeUrls = $_POST['runtime_urls'] ?? '{}';
$isLatest = isset($_POST['is_latest']) ? 1 : 0;
$silentUpdate = isset($_POST['silent_update']) ? 1 : 0;
$editId = $_POST['edit_id'] ?? '';
if ($isLatest) {
$db->update('project_versions', ['is_latest' => 0], ['project_id' => $projectId]);
}
if ($editId) {
$db->update('project_versions', [
'version' => $version,
'download_url' => $downloadUrl,
'file_size' => $fileSize,
'changelog' => $changelog,
'threads' => $threads,
'extra_downloads' => $extraDownloads,
'runtime_urls' => $runtimeUrls,
'is_latest' => $isLatest,
'silent_update' => $silentUpdate,
], ['id' => $editId]);
} else {
$db->insert('project_versions', [
'project_id' => $projectId,
'version' => $version,
'download_url' => $downloadUrl,
'file_size' => $fileSize,
'changelog' => $changelog,
'threads' => $threads,
'extra_downloads' => $extraDownloads,
'runtime_urls' => $runtimeUrls,
'is_latest' => $isLatest,
'silent_update' => $silentUpdate,
]);
}
echo json_encode(['success' => true]);
exit;
case 'delete_version':
$id = $_POST['id'] ?? 0;
$db->delete('project_versions', ['id' => $id]);
echo json_encode(['success' => true]);
exit;
case 'save_patch':
$projectId = $_POST['project_id'] ?? '';
$fromVersion = $_POST['from_version'] ?? '';
$toVersion = $_POST['to_version'] ?? '';
$downloadUrl = $_POST['download_url'] ?? '';
$fileSize = intval($_POST['file_size'] ?? 0);
$changelog = $_POST['changelog'] ?? '';
$editId = $_POST['edit_id'] ?? '';
if ($editId) {
$db->update('project_patches', [
'from_version' => $fromVersion,
'to_version' => $toVersion,
'download_url' => $downloadUrl,
'file_size' => $fileSize,
'changelog' => $changelog,
], ['id' => $editId]);
} else {
$db->insert('project_patches', [
'project_id' => $projectId,
'from_version' => $fromVersion,
'to_version' => $toVersion,
'download_url' => $downloadUrl,
'file_size' => $fileSize,
'changelog' => $changelog,
]);
}
echo json_encode(['success' => true]);
exit;
case 'delete_patch':
$id = $_POST['id'] ?? 0;
$db->delete('project_patches', ['id' => $id]);
echo json_encode(['success' => true]);
exit;
}
echo json_encode(['success' => false, 'message' => '未知操作']);
exit;
}
// ========== 页面渲染 ==========
function showLoginPage($error = '') {
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>R0Installer 管理后台 - 登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e0e0e0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.login-box { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 40px; width: 360px; }
.login-box h1 { color: #4CAF50; font-size: 22px; text-align: center; margin-bottom: 30px; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 6px; color: #999; font-size: 14px; }
.form-group input { width: 100%; padding: 10px 14px; background: #252525; border: 1px solid #444; border-radius: 6px; color: #e0e0e0; font-size: 15px; outline: none; transition: border-color 0.2s; }
.form-group input:focus { border-color: #4CAF50; }
.btn-login { width: 100%; padding: 12px; background: #4CAF50; color: #fff; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; transition: background 0.2s; }
.btn-login:hover { background: #45a049; }
.error { color: #f44336; text-align: center; margin-bottom: 16px; font-size: 14px; }
</style>
</head>
<body>
<div class="login-box">
<h1>R0Installer 管理后台</h1>
<?php if ($error): ?><div class="error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="post">
<input type="hidden" name="action" value="login">
<div class="form-group">
<label>用户名</label>
<input type="text" name="username" required autofocus>
</div>
<div class="form-group">
<label>密码</label>
<input type="password" name="password" required>
</div>
<button type="submit" class="btn-login">登录</button>
</form>
</div>
</body>
</html>
<?php
}
function showAdminPage($db, $page) {
$projects = $db->select('projects', '*');
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>R0Installer 管理后台</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f0f0f; color: #e0e0e0; }
.layout { display: flex; min-height: 100vh; }
.sidebar { width: 220px; background: #1a1a1a; border-right: 1px solid #333; padding: 20px 0; flex-shrink: 0; }
.sidebar h2 { color: #4CAF50; font-size: 16px; padding: 0 20px 20px; border-bottom: 1px solid #333; }
.sidebar a { display: block; padding: 12px 20px; color: #999; text-decoration: none; font-size: 14px; transition: all 0.2s; }
.sidebar a:hover, .sidebar a.active { color: #4CAF50; background: #252525; }
.sidebar .logout { position: absolute; bottom: 20px; width: 220px; }
.main { flex: 1; padding: 30px; overflow-x: auto; }
.main h2 { color: #4CAF50; margin-bottom: 20px; font-size: 20px; }
table { width: 100%; border-collapse: collapse; background: #1a1a1a; border-radius: 8px; overflow: hidden; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #2a2a2a; font-size: 14px; }
th { background: #252525; color: #4CAF50; font-weight: 600; }
tr:hover { background: #1f1f1f; }
.btn { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; transition: opacity 0.2s; }
.btn-primary { background: #4CAF50; color: #fff; }
.btn-danger { background: #f44336; color: #fff; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn:hover { opacity: 0.85; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: flex-start; padding-top: 60px; }
.modal-overlay.show { display: flex; }
.modal { background: #1a1a1a; border: 1px solid #333; border-radius: 12px; padding: 30px; width: 700px; max-height: 80vh; overflow-y: auto; }
.modal h3 { color: #4CAF50; margin-bottom: 20px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 4px; color: #999; font-size: 13px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px 12px; background: #252525; border: 1px solid #444; border-radius: 4px; color: #e0e0e0; font-size: 14px; outline: none; }
.form-group textarea { min-height: 200px; font-family: monospace; font-size: 13px; }
.form-group input:focus, .form-group textarea:focus { border-color: #4CAF50; }
.form-row { display: flex; gap: 16px; }
.form-row .form-group { flex: 1; }
.form-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
.badge-green { background: #1b5e20; color: #4CAF50; }
.badge-gray { background: #333; color: #999; }
.badge-blue { background: #0d47a1; color: #42a5f5; }
.toast { position: fixed; top: 20px; right: 20px; background: #4CAF50; color: #fff; padding: 12px 24px; border-radius: 6px; z-index: 2000; display: none; font-size: 14px; }
.tag { display: inline-block; padding: 2px 6px; background: #333; color: #aaa; border-radius: 3px; font-size: 11px; margin-right: 4px; }
.checkbox-label { display: flex; align-items: center; gap: 8px; color: #ccc; font-size: 14px; cursor: pointer; }
.checkbox-label input[type="checkbox"] { width: 18px; height: 18px; }
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<h2>R0Installer</h2>
<a href="?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">仪表盘</a>
<a href="?page=projects" class="<?= $page === 'projects' ? 'active' : '' ?>">项目管理</a>
<a href="?page=versions" class="<?= $page === 'versions' ? 'active' : '' ?>">版本管理</a>
<a href="?page=patches" class="<?= $page === 'patches' ? 'active' : '' ?>">增量包管理</a>
<a href="?page=installer" class="<?= $page === 'installer' ? 'active' : '' ?>">安装器配置</a>
<a href="?action=logout" style="color:#f44336; margin-top: 40px;">退出登录</a>
</div>
<div class="main">
<?php
switch ($page) {
case 'projects': renderProjectsPage($db, $projects); break;
case 'versions': renderVersionsPage($db, $projects); break;
case 'patches': renderPatchesPage($db, $projects); break;
case 'installer': renderInstallerPage($db); break;
default: renderDashboard($db, $projects); break;
}
?>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 2000);
}
function openModal(id) {
document.getElementById(id).classList.add('show');
}
function closeModal(id) {
document.getElementById(id).classList.remove('show');
}
function submitForm(formId, callback) {
const form = document.getElementById(formId);
const data = new FormData(form);
fetch('index.php', { method: 'POST', body: data })
.then(r => r.json())
.then(d => {
if (d.success) {
showToast('保存成功');
setTimeout(() => location.reload(), 500);
} else {
alert(d.message || '操作失败');
}
})
.catch(e => alert('请求失败: ' + e));
}
function deleteItem(action, id) {
if (!confirm('确定要删除吗?')) return;
const data = new FormData();
data.append('ajax_action', action);
data.append('id', id);
fetch('index.php', { method: 'POST', body: data })
.then(r => r.json())
.then(d => {
if (d.success) { showToast('已删除'); setTimeout(() => location.reload(), 500); }
else alert(d.message || '删除失败');
});
}
</script>
</body>
</html>
<?php
}
// ========== 各页面渲染 ==========
function renderDashboard($db, $projects) {
$versionCount = $db->count('project_versions');
$patchCount = $db->count('project_patches');
?>
<h2>仪表盘</h2>
<div style="display:flex;gap:20px;margin-bottom:30px;">
<div style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;flex:1;text-align:center;">
<div style="font-size:36px;color:#4CAF50;font-weight:bold;"><?= count($projects) ?></div>
<div style="color:#999;margin-top:8px;">项目总数</div>
</div>
<div style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;flex:1;text-align:center;">
<div style="font-size:36px;color:#42a5f5;font-weight:bold;"><?= $versionCount ?></div>
<div style="color:#999;margin-top:8px;">版本总数</div>
</div>
<div style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;flex:1;text-align:center;">
<div style="font-size:36px;color:#ff9800;font-weight:bold;"><?= $patchCount ?></div>
<div style="color:#999;margin-top:8px;">增量包总数</div>
</div>
</div>
<h2 style="font-size:16px;margin-bottom:12px;">项目列表</h2>
<table>
<tr><th>项目ID</th><th>名称</th><th>状态</th></tr>
<?php foreach ($projects as $p): ?>
<tr>
<td><code><?= htmlspecialchars($p['project_id']) ?></code></td>
<td><?= htmlspecialchars($p['display_name']) ?></td>
<td><?= $p['enabled'] ? '<span class="badge badge-green">启用</span>' : '<span class="badge badge-gray">禁用</span>' ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php
}
function renderProjectsPage($db, $projects) {
?>
<h2>项目管理 <button class="btn btn-primary" onclick="openModal('projectModal');document.getElementById('projectForm').reset();document.getElementById('projectEditId').value='';">新增项目</button></h2>
<table>
<tr><th>ID</th><th>项目ID</th><th>名称</th><th>状态</th><th>更新时间</th><th>操作</th></tr>
<?php foreach ($projects as $p): ?>
<tr>
<td><?= $p['id'] ?></td>
<td><code><?= htmlspecialchars($p['project_id']) ?></code></td>
<td><?= htmlspecialchars($p['display_name']) ?></td>
<td><?= $p['enabled'] ? '<span class="badge badge-green">启用</span>' : '<span class="badge badge-gray">禁用</span>' ?></td>
<td><?= $p['updated_at'] ?></td>
<td>
<button class="btn btn-primary btn-sm" onclick='editProject(<?= json_encode($p, JSON_UNESCAPED_UNICODE) ?>)'>编辑</button>
<button class="btn btn-danger btn-sm" onclick="deleteItem('delete_project', <?= $p['id'] ?>)">删除</button>
</td>
</tr>
<?php endforeach; ?>
</table>
<div class="modal-overlay" id="projectModal">
<div class="modal">
<h3>项目配置</h3>
<form id="projectForm">
<input type="hidden" name="ajax_action" value="save_project">
<input type="hidden" name="edit_id" id="projectEditId">
<div class="form-row">
<div class="form-group">
<label>项目ID(唯一标识,创建后不可改)</label>
<input type="text" name="project_id" id="projectIdInput" required>
</div>
<div class="form-group">
<label>显示名称</label>
<input type="text" name="display_name" required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>描述</label>
<input type="text" name="description">
</div>
<div class="form-group">
<label>图标URL</label>
<input type="text" name="icon_url">
</div>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" name="enabled" checked> 启用</label>
</div>
<div class="form-group">
<label>项目配置 JSONinstall / update / runtime_requirements / download_config</label>
<textarea name="config_json" style="min-height:300px;">{}</textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-danger" onclick="closeModal('projectModal')">取消</button>
<button type="button" class="btn btn-primary" onclick="submitForm('projectForm')">保存</button>
</div>
</form>
</div>
</div>
<script>
function editProject(p) {
const f = document.getElementById('projectForm');
document.getElementById('projectEditId').value = p.id;
f.querySelector('[name=project_id]').value = p.project_id;
f.querySelector('[name=project_id]').readOnly = true;
f.querySelector('[name=display_name]').value = p.display_name;
f.querySelector('[name=description]').value = p.description || '';
f.querySelector('[name=icon_url]').value = p.icon_url || '';
f.querySelector('[name=enabled]').checked = !!p.enabled;
try {
const cfg = typeof p.config_json === 'string' ? JSON.parse(p.config_json) : p.config_json;
f.querySelector('[name=config_json]').value = JSON.stringify(cfg, null, 4);
} catch(e) {
f.querySelector('[name=config_json]').value = p.config_json || '{}';
}
openModal('projectModal');
}
</script>
<?php
}
function renderVersionsPage($db, $projects) {
$filterProject = $_GET['project'] ?? '';
$where = $filterProject ? ['project_id' => $filterProject] : [];
$where['ORDER'] = ['id' => 'DESC'];
$versions = $db->select('project_versions', '*', $where);
?>
<h2>版本管理 <button class="btn btn-primary" onclick="openModal('versionModal');document.getElementById('versionForm').reset();document.getElementById('versionEditId').value='';">新增版本</button></h2>
<div style="margin-bottom:16px;">
筛选项目:
<a href="?page=versions" class="tag <?= !$filterProject ? 'style=background:#4CAF50;color:#fff' : '' ?>">全部</a>
<?php foreach ($projects as $p): ?>
<a href="?page=versions&project=<?= urlencode($p['project_id']) ?>" class="tag <?= $filterProject === $p['project_id'] ? 'style=background:#4CAF50;color:#fff' : '' ?>"><?= htmlspecialchars($p['display_name']) ?></a>
<?php endforeach; ?>
</div>
<table>
<tr><th>ID</th><th>项目</th><th>版本</th><th>最新</th><th>静默更新</th><th>线程数</th><th>创建时间</th><th>操作</th></tr>
<?php foreach ($versions as $v): ?>
<tr>
<td><?= $v['id'] ?></td>
<td><code><?= htmlspecialchars($v['project_id']) ?></code></td>
<td><strong><?= htmlspecialchars($v['version']) ?></strong></td>
<td><?= $v['is_latest'] ? '<span class="badge badge-green">是</span>' : '<span class="badge badge-gray">否</span>' ?></td>
<td><?= !empty($v['silent_update']) ? '<span class="badge badge-blue">静默</span>' : '<span class="badge badge-gray">界面</span>' ?></td>
<td><?= $v['threads'] ?></td>
<td><?= $v['created_at'] ?></td>
<td>
<button class="btn btn-primary btn-sm" onclick='editVersion(<?= json_encode($v, JSON_UNESCAPED_UNICODE) ?>)'>编辑</button>
<button class="btn btn-danger btn-sm" onclick="deleteItem('delete_version', <?= $v['id'] ?>)">删除</button>
</td>
</tr>
<?php endforeach; ?>
</table>
<div class="modal-overlay" id="versionModal">
<div class="modal">
<h3>版本配置</h3>
<form id="versionForm">
<input type="hidden" name="ajax_action" value="save_version">
<input type="hidden" name="edit_id" id="versionEditId">
<div class="form-row">
<div class="form-group">
<label>项目ID</label>
<select name="project_id" required>
<?php foreach ($projects as $p): ?>
<option value="<?= htmlspecialchars($p['project_id']) ?>"><?= htmlspecialchars($p['display_name']) ?> (<?= htmlspecialchars($p['project_id']) ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="form-group">
<label>版本号</label>
<input type="text" name="version" required placeholder="如 3.0.5">
</div>
</div>
<div class="form-group">
<label>下载地址 (download_url)</label>
<input type="text" name="download_url" required placeholder="https://cdn.r0csgo.com/releases/...">
</div>
<div class="form-row">
<div class="form-group">
<label>文件大小 (bytes)</label>
<input type="number" name="file_size" value="0">
</div>
<div class="form-group">
<label>下载线程数</label>
<input type="number" name="threads" value="8">
</div>
</div>
<div class="form-group">
<label>更新日志</label>
<textarea name="changelog" style="min-height:60px;"></textarea>
</div>
<div class="form-group">
<label>额外下载项 JSON (extra_downloads),如 {"download_url_ac_cdn": "..."}</label>
<textarea name="extra_downloads" style="min-height:60px;">{}</textarea>
</div>
<div class="form-group">
<label>运行库下载地址覆盖 JSON (runtime_urls)</label>
<textarea name="runtime_urls" style="min-height:60px;">{}</textarea>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" name="is_latest"> 设为最新版本</label>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" name="silent_update"> 静默更新(更新到该版本时不弹界面,后台自动下载安装并拉起主程序)</label>
</div>
<div class="form-actions">
<button type="button" class="btn btn-danger" onclick="closeModal('versionModal')">取消</button>
<button type="button" class="btn btn-primary" onclick="submitForm('versionForm')">保存</button>
</div>
</form>
</div>
</div>
<script>
function editVersion(v) {
const f = document.getElementById('versionForm');
document.getElementById('versionEditId').value = v.id;
f.querySelector('[name=project_id]').value = v.project_id;
f.querySelector('[name=version]').value = v.version;
f.querySelector('[name=download_url]').value = v.download_url;
f.querySelector('[name=file_size]').value = v.file_size;
f.querySelector('[name=threads]').value = v.threads;
f.querySelector('[name=changelog]').value = v.changelog || '';
f.querySelector('[name=extra_downloads]').value = v.extra_downloads || '{}';
f.querySelector('[name=runtime_urls]').value = v.runtime_urls || '{}';
f.querySelector('[name=is_latest]').checked = !!v.is_latest;
f.querySelector('[name=silent_update]').checked = !!Number(v.silent_update);
openModal('versionModal');
}
</script>
<?php
}
function renderPatchesPage($db, $projects) {
$filterProject = $_GET['project'] ?? '';
$where = $filterProject ? ['project_id' => $filterProject] : [];
$where['ORDER'] = ['id' => 'DESC'];
$patches = $db->select('project_patches', '*', $where);
?>
<h2>增量包管理 <button class="btn btn-primary" onclick="openModal('patchModal');document.getElementById('patchForm').reset();document.getElementById('patchEditId').value='';">新增增量包</button></h2>
<div style="margin-bottom:16px;">
筛选项目:
<a href="?page=patches" class="tag <?= !$filterProject ? 'style=background:#4CAF50;color:#fff' : '' ?>">全部</a>
<?php foreach ($projects as $p): ?>
<a href="?page=patches&project=<?= urlencode($p['project_id']) ?>" class="tag <?= $filterProject === $p['project_id'] ? 'style=background:#4CAF50;color:#fff' : '' ?>"><?= htmlspecialchars($p['display_name']) ?></a>
<?php endforeach; ?>
</div>
<table>
<tr><th>ID</th><th>项目</th><th>从版本</th><th>到版本</th><th>文件大小</th><th>创建时间</th><th>操作</th></tr>
<?php foreach ($patches as $p): ?>
<tr>
<td><?= $p['id'] ?></td>
<td><code><?= htmlspecialchars($p['project_id']) ?></code></td>
<td><?= htmlspecialchars($p['from_version']) ?></td>
<td><?= htmlspecialchars($p['to_version']) ?></td>
<td><?= number_format($p['file_size']) ?></td>
<td><?= $p['created_at'] ?></td>
<td>
<button class="btn btn-primary btn-sm" onclick='editPatch(<?= json_encode($p, JSON_UNESCAPED_UNICODE) ?>)'>编辑</button>
<button class="btn btn-danger btn-sm" onclick="deleteItem('delete_patch', <?= $p['id'] ?>)">删除</button>
</td>
</tr>
<?php endforeach; ?>
</table>
<div class="modal-overlay" id="patchModal">
<div class="modal">
<h3>增量包配置</h3>
<form id="patchForm">
<input type="hidden" name="ajax_action" value="save_patch">
<input type="hidden" name="edit_id" id="patchEditId">
<div class="form-group">
<label>项目ID</label>
<select name="project_id" required>
<?php foreach ($projects as $p): ?>
<option value="<?= htmlspecialchars($p['project_id']) ?>"><?= htmlspecialchars($p['display_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label>从版本</label>
<input type="text" name="from_version" required placeholder="如 3.0.5">
</div>
<div class="form-group">
<label>到版本</label>
<input type="text" name="to_version" required placeholder="如 3.0.6">
</div>
</div>
<div class="form-group">
<label>下载地址</label>
<input type="text" name="download_url" required placeholder="https://cdn.r0csgo.com/patches/...">
</div>
<div class="form-row">
<div class="form-group">
<label>文件大小 (bytes)</label>
<input type="number" name="file_size" value="0">
</div>
</div>
<div class="form-group">
<label>更新日志</label>
<textarea name="changelog" style="min-height:60px;"></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-danger" onclick="closeModal('patchModal')">取消</button>
<button type="button" class="btn btn-primary" onclick="submitForm('patchForm')">保存</button>
</div>
</form>
</div>
</div>
<script>
function editPatch(p) {
const f = document.getElementById('patchForm');
document.getElementById('patchEditId').value = p.id;
f.querySelector('[name=project_id]').value = p.project_id;
f.querySelector('[name=from_version]').value = p.from_version;
f.querySelector('[name=to_version]').value = p.to_version;
f.querySelector('[name=download_url]').value = p.download_url;
f.querySelector('[name=file_size]').value = p.file_size;
f.querySelector('[name=changelog]').value = p.changelog || '';
openModal('patchModal');
}
</script>
<?php
}
function renderInstallerPage($db) {
$config = $db->get('installer_config', '*', ['config_key' => 'global']);
$filenameRules = $config ? $config['filename_rules'] : '[]';
$commandRules = $config ? $config['command_rules'] : '[]';
// 格式化 JSON 显示
$fr = json_decode($filenameRules, true);
$cr = json_decode($commandRules, true);
$frFormatted = $fr ? json_encode($fr, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '[]';
$crFormatted = $cr ? json_encode($cr, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) : '[]';
?>
<h2>安装器全局配置</h2>
<form id="installerForm" style="background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:24px;max-width:800px;">
<input type="hidden" name="ajax_action" value="save_installer_config">
<div class="form-row">
<div class="form-group">
<label>默认项目ID</label>
<input type="text" name="default_project" value="<?= htmlspecialchars($config['default_project'] ?? 'r0_arena') ?>">
</div>
<div class="form-group">
<label>默认安装模式</label>
<select name="default_mode">
<option value="full" <?= ($config['default_mode'] ?? '') === 'full' ? 'selected' : '' ?>>full(完整安装)</option>
<option value="client_only" <?= ($config['default_mode'] ?? '') === 'client_only' ? 'selected' : '' ?>>client_only(仅客户端)</option>
<option value="ac_only" <?= ($config['default_mode'] ?? '') === 'ac_only' ? 'selected' : '' ?>>ac_only(仅反作弊)</option>
</select>
</div>
</div>
<div class="form-group">
<label>文件名匹配规则 (filename_rules) JSON 数组</label>
<textarea name="filename_rules" style="min-height:200px;"><?= htmlspecialchars($frFormatted) ?></textarea>
</div>
<div class="form-group">
<label>命令行匹配规则 (command_rules) JSON 数组</label>
<textarea name="command_rules" style="min-height:150px;"><?= htmlspecialchars($crFormatted) ?></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-primary" onclick="submitForm('installerForm')">保存配置</button>
</div>
</form>
<?php
}