SecHub/admin.php
2026-06-01 23:57:52 +08:00

563 lines
17 KiB
PHP
Raw Permalink 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
/**
* SecHub - JSON文件排序管理页面
*/
// 访问密码配置
define('ADMIN_PASSWORD', 'sechub2024'); // 请修改为你的密码
session_start();
// 定义路径
$jsonDir = __DIR__ . '/assets/json/';
$dbDir = __DIR__ . '/assets/db/';
$dbPath = $dbDir . 'sechub.db';
// 确保数据库目录存在
if (!is_dir($dbDir)) {
mkdir($dbDir, 0755, true);
}
// 引入数据库类
require_once __DIR__ . '/db.php';
// 处理登录
if (isset($_POST['login'])) {
if ($_POST['password'] === ADMIN_PASSWORD) {
$_SESSION['authenticated'] = true;
header('Location: admin.php');
exit;
} else {
$error = '密码错误!';
}
}
// 处理登出
if (isset($_GET['logout'])) {
session_destroy();
header('Location: admin.php');
exit;
}
// 检查是否已登录
if (!isset($_SESSION['authenticated']) || $_SESSION['authenticated'] !== true) {
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SecHub - 管理登录</title>
</head>
<body>
<div class="login-container">
<h1>🔐 SecHub 管理后台</h1>
<?php if (isset($error)): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label for="password">访问密码</label>
<input type="password" id="password" name="password" required autofocus>
</div>
<button type="submit" name="login">登录</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
// 处理保存排序
if (isset($_POST['save_order']) && isset($_POST['orders'])) {
$success = false;
$message = '';
try {
// orders 数组已经按拖拽后的顺序排列
$newOrder = 1;
foreach ($_POST['orders'] as $filename) {
$filePath = $jsonDir . $filename;
if (file_exists($filePath)) {
// 读取JSON文件保持原始格式
$content = file_get_contents($filePath);
$data = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($data) && !empty($data)) {
// 只更新第一个数据项的 no 字段
$data[0]['no'] = $newOrder;
// 写回JSON文件保持原有格式不转义
$newContent = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
file_put_contents($filePath, $newContent . "\n");
$newOrder++;
}
}
}
$success = true;
$message = '排序保存成功!';
// 重新同步数据库
$database = new SecHubDatabase($dbPath, $jsonDir);
$database->syncJsonToDatabase();
} catch (Exception $e) {
$message = '保存失败: ' . $e->getMessage();
}
}
// 处理删除数据库
if (isset($_POST['delete_db'])) {
if (file_exists($dbPath)) {
unlink($dbPath);
$success = true;
$message = '数据库已删除!刷新页面后将重新创建。';
} else {
$message = '数据库文件不存在。';
}
}
// 获取所有JSON文件信息
$jsonFiles = glob($jsonDir . '*.json');
$fileInfos = [];
foreach ($jsonFiles as $filePath) {
$filename = basename($filePath);
$content = file_get_contents($filePath);
$data = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($data) && !empty($data)) {
$firstItem = $data[0];
$fileInfos[] = [
'filename' => $filename,
'section' => $firstItem['section'] ?? pathinfo($filename, PATHINFO_FILENAME),
'no' => $firstItem['no'] ?? 0,
'item_count' => count($data) - 1 // 减去第一个配置项
];
}
}
// 按当前 no 排序
usort($fileInfos, function($a, $b) {
return $a['no'] - $b['no'];
});
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="./assets/imgs/favicon.ico" type="image/x-icon">
<title>SecHub - 排序管理</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-primary: #f5f7fa;
--bg-secondary: #ffffff;
--text-primary: #2c3e50;
--text-secondary: #7f8c8d;
--border-color: #e0e6ed;
--accent-color: #3498db;
--success-color: #27ae60;
--warning-color: #f39c12;
--hover-bg: #f0f4f8;
--shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px 20px;
}
header {
background: var(--bg-secondary);
padding: 30px;
border-radius: 12px;
box-shadow: var(--shadow);
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-size: 1.8em;
color: var(--text-primary);
}
.header-info {
display: flex;
gap: 15px;
align-items: center;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.95em;
font-weight: 500;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover {
background: #2980b9;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-success:hover {
background: #229954;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3);
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-danger:hover {
background: #c0392b;
}
.alert {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: 500;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.table-container {
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: var(--shadow);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
}
thead {
background: var(--bg-primary);
}
th {
padding: 15px 20px;
text-align: left;
font-weight: 600;
color: var(--text-primary);
border-bottom: 2px solid var(--border-color);
}
td {
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
tbody tr:hover {
background: var(--hover-bg);
}
tbody tr:last-child td {
border-bottom: none;
}
.draggable-row {
cursor: move;
user-select: none;
}
.draggable-row:hover {
background: var(--hover-bg);
}
.draggable-row.dragging {
opacity: 0.5;
background: #e3f2fd;
}
.drag-handle {
color: var(--text-secondary);
font-size: 1.2em;
cursor: move;
padding: 0 10px;
}
.order-number {
font-weight: 600;
color: var(--accent-color);
font-size: 1.1em;
}
.filename {
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: var(--text-secondary);
}
.section-name {
font-weight: 600;
color: var(--text-primary);
}
.item-count {
color: var(--text-secondary);
font-size: 0.9em;
}
.actions {
display: flex;
gap: 10px;
}
.btn-small {
padding: 6px 12px;
font-size: 0.85em;
}
.tip {
background: #e3f2fd;
border-left: 4px solid var(--accent-color);
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 4px;
color: #1976d2;
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.header-info {
flex-direction: column;
width: 100%;
}
.btn {
width: 100%;
}
table {
font-size: 0.85em;
}
th, td {
padding: 10px;
}
.actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1>📊 SecHub 排序管理</h1>
<p style="color: var(--text-secondary); margin-top: 5px;">调整栏目显示顺序</p>
</div>
<div class="header-info">
<a href="?logout=1" class="btn btn-danger">退出登录</a>
</div>
</header>
<?php if (isset($success) && $success): ?>
<div class="alert alert-success">
✅ <?= htmlspecialchars($message) ?>
</div>
<?php elseif (isset($message)): ?>
<div class="alert alert-warning">
⚠️ <?= htmlspecialchars($message) ?>
</div>
<?php endif; ?>
<div class="tip">
💡 <strong>提示:</strong>拖动行左侧的 ⋮⋮ 图标来调整顺序,序号会自动更新。修改后点击“保存排序”按钮即可生效。
</div>
<div class="table-container">
<form method="POST">
<table>
<thead>
<tr>
<th style="width: 50px;"></th>
<th style="width: 80px;">序列</th>
<th>文件名称</th>
<th>项目名称</th>
<th style="width: 120px;">内容条数</th>
</tr>
</thead>
<tbody id="sortable-tbody">
<?php foreach ($fileInfos as $index => $info): ?>
<tr class="draggable-row" data-filename="<?= htmlspecialchars($info['filename']) ?>" draggable="true">
<td>
<span class="drag-handle">⋮⋮</span>
</td>
<td>
<span class="order-number"><?= $index + 1 ?></span>
</td>
<td>
<span class="filename"><?= htmlspecialchars($info['filename']) ?></span>
</td>
<td>
<span class="section-name"><?= htmlspecialchars($info['section']) ?></span>
</td>
<td>
<span class="item-count"><?= $info['item_count'] ?> 个工具</span>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<!-- 隐藏表单字段,用于提交排序 -->
<div id="order-inputs" style="display: none;"></div>
<div style="padding: 20px; text-align: center; border-top: 2px solid var(--border-color); display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
<button type="submit" name="save_order" class="btn btn-success" style="font-size: 1.1em; padding: 12px 40px;">
💾 保存排序
</button>
<button type="submit" name="delete_db" class="btn btn-danger" style="font-size: 1.1em; padding: 12px 40px;" onclick="return confirm('确定要删除数据库吗?这将导致下次访问时重新同步所有数据。')">
🗑️ 重置数据库
</button>
</div>
</form>
</div>
</div>
<script>
// 拖拽排序功能
let draggedRow = null;
const tbody = document.getElementById('sortable-tbody');
// 添加拖拽事件监听
tbody.addEventListener('dragstart', function(e) {
draggedRow = e.target.closest('.draggable-row');
if (draggedRow) {
draggedRow.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', draggedRow.innerHTML);
}
});
tbody.addEventListener('dragend', function(e) {
if (draggedRow) {
draggedRow.classList.remove('dragging');
draggedRow = null;
updateOrderNumbers();
}
});
tbody.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const afterElement = getDragAfterElement(tbody, e.clientY);
if (afterElement == null) {
tbody.appendChild(draggedRow);
} else {
tbody.insertBefore(draggedRow, afterElement);
}
});
// 获取拖拽位置后的元素
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.draggable-row:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
// 更新序号显示
function updateOrderNumbers() {
const rows = tbody.querySelectorAll('.draggable-row');
rows.forEach((row, index) => {
const orderSpan = row.querySelector('.order-number');
if (orderSpan) {
orderSpan.textContent = index + 1;
}
});
}
// 表单提交前生成隐藏输入字段
document.querySelector('form').addEventListener('submit', function(e) {
const orderInputs = document.getElementById('order-inputs');
orderInputs.innerHTML = ''; // 清空旧数据
const rows = tbody.querySelectorAll('.draggable-row');
rows.forEach((row, index) => {
const filename = row.getAttribute('data-filename');
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'orders[]';
input.value = filename;
orderInputs.appendChild(input);
});
});
</script>
</body>
</html>