563 lines
17 KiB
PHP
563 lines
17 KiB
PHP
<?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>
|