6124d6060c
Co-authored-by: Cursor <cursoragent@cursor.com>
744 lines
34 KiB
PHP
744 lines
34 KiB
PHP
<?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>项目配置 JSON(install / 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
|
||
}
|