1186 lines
51 KiB
PHP
1186 lines
51 KiB
PHP
<?php
|
||
/**
|
||
* SecHub - 数据管理后台
|
||
* 排序管理 + 数据CRUD + 批量导入导出 + 系统设置
|
||
*/
|
||
|
||
require_once __DIR__ . '/config.php';
|
||
|
||
session_start();
|
||
|
||
// 确保数据库目录存在
|
||
if (!is_dir(DB_DIR)) {
|
||
mkdir(DB_DIR, 0755, true);
|
||
}
|
||
|
||
// 引入数据库类
|
||
require_once __DIR__ . '/db.php';
|
||
|
||
// 处理登录
|
||
if (isset($_POST['login'])) {
|
||
if ($_POST['password'] === ADMIN_PASSWORD) {
|
||
$_SESSION['authenticated'] = true;
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
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>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.login-container {
|
||
background: white;
|
||
padding: 40px;
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||
width: 380px;
|
||
max-width: 90vw;
|
||
}
|
||
.login-container h1 {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
font-size: 1.5em;
|
||
color: #2c3e50;
|
||
}
|
||
.form-group { margin-bottom: 20px; }
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 500;
|
||
color: #555;
|
||
}
|
||
.form-group input {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 2px solid #e0e6ed;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
transition: border-color 0.2s;
|
||
}
|
||
.form-group input:focus {
|
||
border-color: #667eea;
|
||
outline: none;
|
||
}
|
||
button[type="submit"] {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: transform 0.2s;
|
||
}
|
||
button[type="submit"]:hover { transform: translateY(-2px); }
|
||
.error {
|
||
background: #ffeaea;
|
||
color: #d63031;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
font-weight: 500;
|
||
}
|
||
</style>
|
||
</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;
|
||
}
|
||
|
||
// 初始化 CSRF token
|
||
if (!isset($_SESSION['csrf_token'])) {
|
||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||
}
|
||
|
||
// 初始化数据库
|
||
try {
|
||
$database = new SecHubDatabase(DB_PATH, JSON_DIR);
|
||
$database->syncJsonToDatabase();
|
||
} catch (Exception $e) {
|
||
$error = '数据库初始化失败: ' . $e->getMessage();
|
||
}
|
||
|
||
// 消息提示
|
||
$message = '';
|
||
$messageType = 'success';
|
||
|
||
// ======================== 处理POST动作 ========================
|
||
|
||
// CSRF 校验(对非登录请求)
|
||
$csrfOk = true;
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['login'])) {
|
||
$token = $_POST['csrf_token'] ?? '';
|
||
if ($token !== $_SESSION['csrf_token']) {
|
||
$csrfOk = false;
|
||
$message = '安全令牌无效,请刷新页面重试';
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
|
||
if ($csrfOk) {
|
||
|
||
// --- 保存排序 ---
|
||
if (isset($_POST['save_order']) && isset($_POST['orders'])) {
|
||
try {
|
||
$newOrder = 1;
|
||
foreach ($_POST['orders'] as $filename) {
|
||
$filePath = JSON_DIR . $filename;
|
||
if (file_exists($filePath)) {
|
||
$content = file_get_contents($filePath);
|
||
$data = json_decode($content, true);
|
||
if (json_last_error() === JSON_ERROR_NONE && is_array($data) && !empty($data)) {
|
||
$data[0]['no'] = $newOrder;
|
||
$newContent = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||
file_put_contents($filePath, $newContent . "\n");
|
||
$newOrder++;
|
||
}
|
||
}
|
||
}
|
||
$database->syncJsonToDatabase();
|
||
$message = '排序保存成功!';
|
||
$messageType = 'success';
|
||
} catch (Exception $e) {
|
||
$message = '保存失败: ' . $e->getMessage();
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
|
||
// --- 添加工具 ---
|
||
if (isset($_POST['add_item'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
$name = trim($_POST['name'] ?? '');
|
||
$url = trim($_POST['url'] ?? '');
|
||
$description = trim($_POST['description'] ?? '');
|
||
if (empty($name)) {
|
||
$message = '工具名称不能为空';
|
||
$messageType = 'error';
|
||
} else {
|
||
$result = $database->addItemToSection($tableName, $name, $url, $description);
|
||
if ($result['success']) {
|
||
$message = '工具添加成功!';
|
||
} else {
|
||
$message = '添加失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 更新工具 ---
|
||
if (isset($_POST['update_item'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
$itemIndex = intval($_POST['item_index'] ?? 0);
|
||
$name = trim($_POST['name'] ?? '');
|
||
$url = trim($_POST['url'] ?? '');
|
||
$description = trim($_POST['description'] ?? '');
|
||
if (empty($name)) {
|
||
$message = '工具名称不能为空';
|
||
$messageType = 'error';
|
||
} else {
|
||
$result = $database->updateItemInSection($tableName, $itemIndex, $name, $url, $description);
|
||
if ($result['success']) {
|
||
$message = '工具更新成功!';
|
||
} else {
|
||
$message = '更新失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 删除工具 ---
|
||
if (isset($_POST['delete_item'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
$itemIndex = intval($_POST['item_index'] ?? 0);
|
||
$result = $database->deleteItemFromSection($tableName, $itemIndex);
|
||
if ($result['success']) {
|
||
$message = '工具已删除!';
|
||
} else {
|
||
$message = '删除失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
|
||
// --- 新增栏目 ---
|
||
if (isset($_POST['add_section'])) {
|
||
$sectionName = trim($_POST['section_name'] ?? '');
|
||
if (empty($sectionName)) {
|
||
$message = '栏目名称不能为空';
|
||
$messageType = 'error';
|
||
} else {
|
||
$result = $database->createSection($sectionName);
|
||
if ($result['success']) {
|
||
$message = '栏目 "' . htmlspecialchars($sectionName) . '" 创建成功!';
|
||
} else {
|
||
$message = '创建失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 删除栏目 ---
|
||
if (isset($_POST['delete_section'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
$confirm = $_POST['confirm'] ?? '';
|
||
if ($confirm !== 'yes') {
|
||
$message = '请确认删除操作';
|
||
$messageType = 'error';
|
||
} else {
|
||
$result = $database->removeSection($tableName);
|
||
if ($result['success']) {
|
||
$message = '栏目已删除!';
|
||
} else {
|
||
$message = '删除失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 重命名栏目 ---
|
||
if (isset($_POST['rename_section'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
$newName = trim($_POST['new_name'] ?? '');
|
||
if (empty($newName)) {
|
||
$message = '栏目名称不能为空';
|
||
$messageType = 'error';
|
||
} else {
|
||
$result = $database->renameSection($tableName, $newName);
|
||
if ($result['success']) {
|
||
$message = '栏目重命名成功!';
|
||
} else {
|
||
$message = '重命名失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 批量导入 ---
|
||
if (isset($_POST['batch_import'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
$importData = trim($_POST['import_data'] ?? '');
|
||
$format = $_POST['import_format'] ?? 'csv';
|
||
|
||
if (empty($importData)) {
|
||
$message = '导入数据不能为空';
|
||
$messageType = 'error';
|
||
} else {
|
||
$lines = explode("\n", str_replace("\r\n", "\n", $importData));
|
||
$items = [];
|
||
|
||
foreach ($lines as $line) {
|
||
$line = trim($line);
|
||
if (empty($line)) continue;
|
||
|
||
if ($format === 'csv') {
|
||
// CSV格式: 名称,URL,描述
|
||
$parts = str_getcsv($line);
|
||
$items[] = [
|
||
trim($parts[0] ?? ''),
|
||
trim($parts[1] ?? ''),
|
||
trim($parts[2] ?? '')
|
||
];
|
||
} else {
|
||
// 竖线格式: 名称 | URL | 描述
|
||
$parts = explode('|', $line);
|
||
$items[] = [
|
||
trim($parts[0] ?? ''),
|
||
trim($parts[1] ?? ''),
|
||
trim($parts[2] ?? '')
|
||
];
|
||
}
|
||
}
|
||
|
||
$result = $database->batchAddItems($tableName, $items);
|
||
if ($result['success']) {
|
||
$message = "批量导入成功!成功添加 {$result['count']} 个工具(共 {$result['total']} 条数据)";
|
||
} else {
|
||
$message = '导入失败: ' . ($result['error'] ?? '未知错误');
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 导出JSON ---
|
||
if (isset($_POST['export_json'])) {
|
||
$tableName = $_POST['table_name'] ?? 'all';
|
||
if ($tableName === 'all') {
|
||
$data = $database->exportAllSections();
|
||
$filename = 'sechub_all_sections.json';
|
||
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||
} elseif ($database->getSectionInfo($tableName)) {
|
||
$filePath = JSON_DIR . $tableName . '.json';
|
||
if (file_exists($filePath)) {
|
||
$json = file_get_contents($filePath);
|
||
$filename = $tableName . '.json';
|
||
} else {
|
||
$message = '文件不存在';
|
||
$messageType = 'error';
|
||
$json = null;
|
||
}
|
||
} else {
|
||
$message = '无效的栏目';
|
||
$messageType = 'error';
|
||
$json = null;
|
||
}
|
||
|
||
if (isset($json)) {
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||
echo $json;
|
||
$database->close();
|
||
exit;
|
||
}
|
||
}
|
||
|
||
// --- 导出CSV ---
|
||
if (isset($_POST['export_csv'])) {
|
||
$tableName = $_POST['table_name'] ?? '';
|
||
|
||
if (empty($tableName)) {
|
||
$message = '请选择要导出的栏目';
|
||
$messageType = 'error';
|
||
} else {
|
||
$csv = $database->exportSectionToCsv($tableName);
|
||
if (!empty($csv)) {
|
||
$info = $database->getSectionInfo($tableName);
|
||
$filename = ($info['title'] ?? $tableName) . '.csv';
|
||
header('Content-Type: text/csv; charset=utf-8');
|
||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||
echo "\xEF\xBB\xBF" . $csv;
|
||
$database->close();
|
||
exit;
|
||
} else {
|
||
$message = '导出失败或栏目为空';
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 强制重新同步 ---
|
||
if (isset($_POST['resync_db'])) {
|
||
try {
|
||
$database->syncJsonToDatabase();
|
||
$message = '数据库重新同步成功!';
|
||
} catch (Exception $e) {
|
||
$message = '同步失败: ' . $e->getMessage();
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
|
||
// --- 删除数据库 ---
|
||
if (isset($_POST['delete_db'])) {
|
||
if (file_exists(DB_PATH)) {
|
||
unlink(DB_PATH);
|
||
$message = '数据库已删除!刷新后自动重建。';
|
||
} else {
|
||
$message = '数据库文件不存在。';
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
|
||
// --- 修改密码 ---
|
||
if (isset($_POST['change_password'])) {
|
||
$oldPwd = $_POST['old_password'] ?? '';
|
||
$newPwd = $_POST['new_password'] ?? '';
|
||
$confirmPwd = $_POST['confirm_password'] ?? '';
|
||
|
||
if ($oldPwd !== ADMIN_PASSWORD) {
|
||
$message = '当前密码错误';
|
||
$messageType = 'error';
|
||
} elseif (strlen($newPwd) < 6) {
|
||
$message = '新密码长度不能少于6位';
|
||
$messageType = 'error';
|
||
} elseif ($newPwd !== $confirmPwd) {
|
||
$message = '两次密码输入不一致';
|
||
$messageType = 'error';
|
||
} else {
|
||
// 更新config.php中的密码
|
||
$configPath = __DIR__ . '/config.php';
|
||
$configContent = file_get_contents($configPath);
|
||
$newContent = preg_replace(
|
||
"/define\('ADMIN_PASSWORD',\s*'[^']*'\)/",
|
||
"define('ADMIN_PASSWORD', '" . addslashes($newPwd) . "')",
|
||
$configContent
|
||
);
|
||
if (file_put_contents($configPath, $newContent)) {
|
||
$message = '密码修改成功!';
|
||
// 更新当前session中的标记(不需要改密码,因为会话还在)
|
||
} else {
|
||
$message = '密码修改失败(文件写入错误)';
|
||
$messageType = 'error';
|
||
}
|
||
}
|
||
}
|
||
|
||
} // end CSRF check
|
||
|
||
// 获取栏目列表
|
||
$allSections = $database->getAllSectionsWithInfo();
|
||
|
||
// 当前选中的栏目(从URL参数或第一个)
|
||
$currentTab = $_GET['tab'] ?? 'sort';
|
||
$selectedSection = $_GET['section'] ?? ($allSections[0]['name'] ?? '');
|
||
|
||
// 获取选中栏目的工具列表
|
||
$currentItems = [];
|
||
$currentSectionInfo = null;
|
||
if ($selectedSection) {
|
||
$currentItems = $database->getItemsBySection($selectedSection);
|
||
$currentSectionInfo = $database->getSectionInfo($selectedSection);
|
||
}
|
||
?>
|
||
<!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>
|
||
<link rel="stylesheet" href="assets/css/admin.css">
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<!-- Header -->
|
||
<div class="header">
|
||
<div>
|
||
<h1>SecHub 管理后台</h1>
|
||
<p class="header-sub">排序管理 / 数据管理 / 批量操作 / 系统设置</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button id="themeToggle" class="theme-btn" title="切换暗黑模式">
|
||
<span id="themeIcon">☾</span>
|
||
</button>
|
||
<a href="index.php" class="btn btn-outline" target="_blank">预览站点</a>
|
||
<a href="?logout=1" class="btn btn-danger">退出登录</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Message -->
|
||
<?php if (!empty($message)): ?>
|
||
<div id="pageAlert" class="alert alert-<?= $messageType === 'error' ? 'error' : ($messageType === 'warning' ? 'warning' : 'success') ?>">
|
||
<?= ($messageType === 'success' ? '✓ ' : '⚠ ') . htmlspecialchars($message) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Tabs -->
|
||
<div class="tabs">
|
||
<button class="tab-btn <?= $currentTab === 'sort' ? 'active' : '' ?>" onclick="switchTab('sort')">排序管理</button>
|
||
<button class="tab-btn <?= $currentTab === 'data' ? 'active' : '' ?>" onclick="switchTab('data')">数据管理</button>
|
||
<button class="tab-btn <?= $currentTab === 'import' ? 'active' : '' ?>" onclick="switchTab('import')">批量操作</button>
|
||
<button class="tab-btn <?= $currentTab === 'settings' ? 'active' : '' ?>" onclick="switchTab('settings')">系统设置</button>
|
||
</div>
|
||
|
||
<!-- Tab: 排序管理 -->
|
||
<div class="tab-content <?= $currentTab === 'sort' ? 'active' : '' ?>" id="tab-sort">
|
||
<div class="panel">
|
||
<div class="tip">拖动行左侧的 ⋮⋮ 图标来调整栏目顺序,修改后点击"保存排序"生效。</div>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<div class="table-wrap">
|
||
<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 ($allSections as $index => $sec): ?>
|
||
<tr class="draggable-row" data-filename="<?= htmlspecialchars($sec['filename']) ?>" draggable="true">
|
||
<td><span class="drag-handle">⋮⋮</span></td>
|
||
<td><span class="order-num"><?= $index + 1 ?></span></td>
|
||
<td><span class="mono"><?= htmlspecialchars($sec['filename']) ?></span></td>
|
||
<td><?= htmlspecialchars($sec['title']) ?></td>
|
||
<td><span class="count-badge"><?= $sec['item_count'] ?> 个</span></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div id="order-inputs" style="display:none;"></div>
|
||
<div class="sort-save-bar">
|
||
<button type="submit" name="save_order" class="btn btn-success btn-lg">
|
||
保存排序
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: 数据管理 -->
|
||
<div class="tab-content <?= $currentTab === 'data' ? 'active' : '' ?>" id="tab-data">
|
||
<div class="panel">
|
||
<div class="two-col">
|
||
<!-- 左侧:栏目列表 -->
|
||
<div class="side">
|
||
<div class="action-bar">
|
||
<h3>栏目列表</h3>
|
||
<button class="btn btn-primary btn-small" onclick="showModal('addSectionModal')">+ 新增</button>
|
||
</div>
|
||
<div class="section-list">
|
||
<?php foreach ($allSections as $sec): ?>
|
||
<div class="section-list-item <?= $sec['name'] === $selectedSection ? 'active' : '' ?>"
|
||
onclick="selectSection('<?= htmlspecialchars($sec['name']) ?>')">
|
||
<div>
|
||
<div class="sname"><?= htmlspecialchars($sec['title']) ?></div>
|
||
<div class="scount"><?= $sec['item_count'] ?> 个工具</div>
|
||
</div>
|
||
<div class="sactions">
|
||
<button class="btn-text" onclick="event.stopPropagation();showRenameModal('<?= htmlspecialchars($sec['name']) ?>','<?= htmlspecialchars(addslashes($sec['title'])) ?>')" title="重命名">✎</button>
|
||
<button class="btn-text danger" onclick="event.stopPropagation();showDeleteSectionModal('<?= htmlspecialchars($sec['name']) ?>','<?= htmlspecialchars(addslashes($sec['title'])) ?>')" title="删除">✕</button>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<?php if (empty($allSections)): ?>
|
||
<div class="empty-state">暂无栏目</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:工具列表 -->
|
||
<div class="main">
|
||
<div class="section-header-row">
|
||
<h3>
|
||
工具列表
|
||
<?php if ($currentSectionInfo): ?>
|
||
- <span class="col-section-title"><?= htmlspecialchars($currentSectionInfo['title']) ?></span>
|
||
<?php endif; ?>
|
||
</h3>
|
||
<?php if ($selectedSection): ?>
|
||
<button class="btn btn-primary btn-small" onclick="showAddToolModal('<?= htmlspecialchars($selectedSection) ?>')">+ 新增工具</button>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<?php if (!$selectedSection): ?>
|
||
<div class="empty-state">请从左侧选择一个栏目</div>
|
||
<?php elseif (empty($currentItems)): ?>
|
||
<div class="empty-state">
|
||
此栏目暂无工具数据
|
||
<br><br>
|
||
<button class="btn btn-primary" onclick="showAddToolModal('<?= htmlspecialchars($selectedSection) ?>')">+ 添加第一个工具</button>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th style="width:50px;">#</th>
|
||
<th style="width:20%;">名称</th>
|
||
<th style="width:30%;">URL</th>
|
||
<th style="width:35%;">描述</th>
|
||
<th style="width:80px;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($currentItems as $index => $item): ?>
|
||
<tr>
|
||
<td><?= $index + 1 ?></td>
|
||
<td><strong><?= htmlspecialchars($item['name'] ?? '') ?></strong></td>
|
||
<td class="cell-url">
|
||
<a href="<?= htmlspecialchars($item['url'] ?? '#') ?>" target="_blank"><?= htmlspecialchars($item['url'] ?? '') ?></a>
|
||
</td>
|
||
<td class="cell-desc"><?= htmlspecialchars($item['description'] ?? '') ?></td>
|
||
<td>
|
||
<button class="btn-text" onclick="showEditToolModal('<?= htmlspecialchars($selectedSection) ?>',<?= $index ?>,'<?= htmlspecialchars(addslashes($item['name'] ?? '')) ?>','<?= htmlspecialchars(addslashes($item['url'] ?? '')) ?>','<?= htmlspecialchars(addslashes($item['description'] ?? '')) ?>')">✎</button>
|
||
<button class="btn-text danger" onclick="showDeleteToolModal('<?= htmlspecialchars($selectedSection) ?>',<?= $index ?>,'<?= htmlspecialchars(addslashes($item['name'] ?? '')) ?>')">✕</button>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: 批量操作 -->
|
||
<div class="tab-content <?= $currentTab === 'import' ? 'active' : '' ?>" id="tab-import">
|
||
<div class="panel">
|
||
<div class="two-col">
|
||
<!-- 批量导入 -->
|
||
<div class="main">
|
||
<h3 style="margin-bottom:15px;">批量导入</h3>
|
||
<div class="tip">
|
||
支持两种格式导入,每行一个工具。支持CSV上传和粘贴文本。
|
||
</div>
|
||
<form method="POST" id="importForm">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="batch_import" value="1">
|
||
|
||
<div class="form-group">
|
||
<label>目标栏目</label>
|
||
<select name="table_name" required class="form-select">
|
||
<?php foreach ($allSections as $sec): ?>
|
||
<option value="<?= htmlspecialchars($sec['name']) ?>"><?= htmlspecialchars($sec['title']) ?> (<?= $sec['item_count'] ?>个工具)</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-bottom:12px;">
|
||
<label>导入格式</label>
|
||
<select name="import_format" id="importFormat" class="form-select-sm">
|
||
<option value="csv">CSV: 名称,URL,描述</option>
|
||
<option value="pipe">竖线: 名称 | URL | 描述</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="import-area">
|
||
<label>导入数据(每行一个工具)</label>
|
||
<textarea name="import_data" id="importData" placeholder="<?php
|
||
echo "CSV格式示例:\n漏洞扫描器,https://example.com/scanner,一款快速漏洞扫描工具\n资产收集工具,https://example.com/asset,自动收集子域名和资产信息\n\n或竖线格式:\n漏洞扫描器 | https://example.com/scanner | 一款快速漏洞扫描工具";
|
||
?>"></textarea>
|
||
</div>
|
||
|
||
<div class="import-preview" id="importPreview">
|
||
<div class="preview-empty">输入数据后点击"预览"查看解析结果</div>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:10px;margin-top:15px;">
|
||
<button type="button" class="btn btn-outline" onclick="previewImport()">预览解析</button>
|
||
<button type="submit" class="btn btn-success" onclick="return confirm('确认导入 ' + document.querySelector('#importPreview tbody')?.rows.length + ' 个工具?')">确认导入</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 批量导出 -->
|
||
<div class="side">
|
||
<h3 style="margin-bottom:15px;">批量导出</h3>
|
||
<div class="export-box">
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<div class="form-group">
|
||
<label>选择栏目</label>
|
||
<select name="table_name" class="form-select">
|
||
<option value="all">全部栏目(JSON)</option>
|
||
<?php foreach ($allSections as $sec): ?>
|
||
<option value="<?= htmlspecialchars($sec['name']) ?>"><?= htmlspecialchars($sec['title']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
||
<button type="submit" name="export_json" class="btn btn-primary btn-small">导出 JSON</button>
|
||
<button type="submit" name="export_csv" class="btn btn-success btn-small">导出 CSV</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab: 系统设置 -->
|
||
<div class="tab-content <?= $currentTab === 'settings' ? 'active' : '' ?>" id="tab-settings">
|
||
<div class="panel">
|
||
<div class="settings-group">
|
||
<h3>修改管理密码</h3>
|
||
<form method="POST" onsubmit="return confirm('确定要修改密码吗?')">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="change_password" value="1">
|
||
<div class="form-group">
|
||
<label>当前密码</label>
|
||
<input type="password" name="old_password" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>新密码(至少6位)</label>
|
||
<input type="password" name="new_password" required minlength="6">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>确认新密码</label>
|
||
<input type="password" name="confirm_password" required minlength="6">
|
||
</div>
|
||
<button type="submit" class="btn btn-warning">修改密码</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="settings-group">
|
||
<h3>数据库维护</h3>
|
||
<div class="settings-info">
|
||
数据库位置: <?= htmlspecialchars(DB_PATH) ?><br>
|
||
JSON目录: <?= htmlspecialchars(JSON_DIR) ?><br>
|
||
栏目数量: <?= count($allSections) ?>
|
||
</div>
|
||
<div class="settings-actions">
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<button type="submit" name="resync_db" class="btn btn-primary">强制重新同步</button>
|
||
</form>
|
||
<form method="POST" onsubmit="return confirm('确定要删除数据库吗?下次访问将自动重建。')">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<button type="submit" name="delete_db" class="btn btn-danger">删除并重建数据库</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="admin-footer">
|
||
© <?= date('Y') ?> SecHub - 网络安全工具集
|
||
</footer>
|
||
</div>
|
||
|
||
<!-- Toast 通知容器 -->
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
|
||
<!-- ==================== Modals ==================== -->
|
||
|
||
<!-- 新增栏目 -->
|
||
<div class="modal-overlay" id="addSectionModal">
|
||
<div class="modal">
|
||
<h2>新增栏目</h2>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="add_section" value="1">
|
||
<div class="form-group">
|
||
<label>栏目名称</label>
|
||
<input type="text" name="section_name" required placeholder="例如:信息收集工具" autofocus>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-outline" onclick="hideModal('addSectionModal')">取消</button>
|
||
<button type="submit" class="btn btn-primary">创建</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 重命名栏目 -->
|
||
<div class="modal-overlay" id="renameSectionModal">
|
||
<div class="modal">
|
||
<h2>重命名栏目</h2>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="rename_section" value="1">
|
||
<input type="hidden" name="table_name" id="renameTableName">
|
||
<div class="form-group">
|
||
<label>新名称</label>
|
||
<input type="text" name="new_name" id="renameNewName" required autofocus>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-outline" onclick="hideModal('renameSectionModal')">取消</button>
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 删除栏目确认 -->
|
||
<div class="modal-overlay" id="deleteSectionModal">
|
||
<div class="modal">
|
||
<h2>删除栏目</h2>
|
||
<div class="warning-box" id="deleteSectionInfo">
|
||
确定要删除栏目吗?此操作不可恢复!
|
||
</div>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="delete_section" value="1">
|
||
<input type="hidden" name="table_name" id="deleteSectionTable">
|
||
<input type="hidden" name="confirm" value="yes">
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-outline" onclick="hideModal('deleteSectionModal')">取消</button>
|
||
<button type="submit" class="btn btn-danger">确认删除</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增工具 -->
|
||
<div class="modal-overlay" id="addToolModal">
|
||
<div class="modal">
|
||
<h2>新增工具</h2>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="add_item" value="1">
|
||
<input type="hidden" name="table_name" id="addToolSection">
|
||
<div class="form-group">
|
||
<label>工具名称 <span style="color:var(--danger)">*</span></label>
|
||
<input type="text" name="name" required placeholder="例如:Nmap">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>URL</label>
|
||
<input type="url" name="url" placeholder="https://github.com/example/tool">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>描述</label>
|
||
<textarea name="description" placeholder="工具功能简介"></textarea>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-outline" onclick="hideModal('addToolModal')">取消</button>
|
||
<button type="submit" class="btn btn-primary">添加</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑工具 -->
|
||
<div class="modal-overlay" id="editToolModal">
|
||
<div class="modal">
|
||
<h2>编辑工具</h2>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="update_item" value="1">
|
||
<input type="hidden" name="table_name" id="editToolSection">
|
||
<input type="hidden" name="item_index" id="editToolIndex">
|
||
<div class="form-group">
|
||
<label>工具名称 <span style="color:var(--danger)">*</span></label>
|
||
<input type="text" name="name" id="editToolName" required>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>URL</label>
|
||
<input type="url" name="url" id="editToolUrl">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>描述</label>
|
||
<textarea name="description" id="editToolDesc"></textarea>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-outline" onclick="hideModal('editToolModal')">取消</button>
|
||
<button type="submit" class="btn btn-primary">保存</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 删除工具确认 -->
|
||
<div class="modal-overlay" id="deleteToolModal">
|
||
<div class="modal">
|
||
<h2>删除工具</h2>
|
||
<p class="warning-box" id="deleteToolInfo">
|
||
确定要删除此工具吗?
|
||
</p>
|
||
<form method="POST">
|
||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||
<input type="hidden" name="delete_item" value="1">
|
||
<input type="hidden" name="table_name" id="deleteToolSection">
|
||
<input type="hidden" name="item_index" id="deleteToolIndex">
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-outline" onclick="hideModal('deleteToolModal')">取消</button>
|
||
<button type="submit" class="btn btn-danger">确认删除</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// ======================== 暗黑模式 ========================
|
||
(function() {
|
||
const toggle = document.getElementById('themeToggle');
|
||
const icon = document.getElementById('themeIcon');
|
||
if (!toggle) return;
|
||
|
||
const current = localStorage.getItem('admin_theme') || 'light';
|
||
if (current === 'dark') {
|
||
document.body.classList.add('dark-mode');
|
||
icon.textContent = '\u2600\uFE0F';
|
||
}
|
||
|
||
toggle.addEventListener('click', function() {
|
||
document.body.classList.toggle('dark-mode');
|
||
const isDark = document.body.classList.contains('dark-mode');
|
||
icon.textContent = isDark ? '\u2600\uFE0F' : '\uD83C\uDF19';
|
||
localStorage.setItem('admin_theme', isDark ? 'dark' : 'light');
|
||
});
|
||
})();
|
||
|
||
// ======================== Toast 通知 ========================
|
||
function showToast(message, type) {
|
||
type = type || 'success';
|
||
const container = document.getElementById('toastContainer');
|
||
if (!container) return;
|
||
|
||
const toast = document.createElement('div');
|
||
const icons = { success: '\u2713', error: '\u26A0', warning: '\u26A0' };
|
||
toast.className = 'toast toast-' + type;
|
||
toast.innerHTML = '<span>' + (icons[type] || '') + '</span><span>' + message + '</span>';
|
||
container.appendChild(toast);
|
||
|
||
setTimeout(function() {
|
||
toast.classList.add('toast-out');
|
||
setTimeout(function() { toast.remove(); }, 300);
|
||
}, 4000);
|
||
}
|
||
|
||
// 页面加载时,如果存在页面级 Alert,自动转为 Toast 并隐藏
|
||
(function() {
|
||
const alert = document.getElementById('pageAlert');
|
||
if (alert) {
|
||
setTimeout(function() {
|
||
alert.style.transition = 'opacity 0.5s';
|
||
alert.style.opacity = '0';
|
||
setTimeout(function() { alert.style.display = 'none'; }, 500);
|
||
}, 4000);
|
||
}
|
||
})();
|
||
|
||
// ======================== Tab切换 ========================
|
||
function switchTab(tab) {
|
||
// 更新URL
|
||
const url = new URL(window.location);
|
||
url.searchParams.set('tab', tab);
|
||
window.location.href = url.toString();
|
||
}
|
||
|
||
// ======================== 栏目选择 ========================
|
||
function selectSection(name) {
|
||
const url = new URL(window.location);
|
||
url.searchParams.set('tab', 'data');
|
||
url.searchParams.set('section', name);
|
||
window.location.href = url.toString();
|
||
}
|
||
|
||
// ======================== 模态框 ========================
|
||
function showModal(id) { document.getElementById(id).classList.add('show'); }
|
||
function hideModal(id) { document.getElementById(id).classList.remove('show'); }
|
||
|
||
// 点击遮罩关闭
|
||
document.querySelectorAll('.modal-overlay').forEach(el => {
|
||
el.addEventListener('click', function(e) {
|
||
if (e.target === this) this.classList.remove('show');
|
||
});
|
||
});
|
||
document.addEventListener('keydown', function(e) {
|
||
if (e.key === 'Escape') {
|
||
document.querySelectorAll('.modal-overlay.show').forEach(el => el.classList.remove('show'));
|
||
}
|
||
});
|
||
|
||
// ======================== 重命名栏目 ========================
|
||
function showRenameModal(tableName, currentName) {
|
||
document.getElementById('renameTableName').value = tableName;
|
||
document.getElementById('renameNewName').value = currentName;
|
||
showModal('renameSectionModal');
|
||
}
|
||
|
||
// ======================== 删除栏目 ========================
|
||
function showDeleteSectionModal(tableName, title) {
|
||
document.getElementById('deleteSectionTable').value = tableName;
|
||
document.getElementById('deleteSectionInfo').innerHTML =
|
||
'<strong>⚠ 确定要删除栏目 "' + title + '" 吗?</strong><br>此操作会删除JSON文件和数据库数据,不可恢复!';
|
||
showModal('deleteSectionModal');
|
||
}
|
||
|
||
// ======================== 新增工具 ========================
|
||
function showAddToolModal(section) {
|
||
document.getElementById('addToolSection').value = section;
|
||
document.getElementById('addToolModal').querySelector('form').reset();
|
||
document.getElementById('addToolModal').querySelector('[name="name"]').value = '';
|
||
document.getElementById('addToolModal').querySelector('[name="url"]').value = '';
|
||
document.getElementById('addToolModal').querySelector('[name="description"]').value = '';
|
||
showModal('addToolModal');
|
||
}
|
||
|
||
// ======================== 编辑工具 ========================
|
||
function showEditToolModal(section, index, name, url, desc) {
|
||
document.getElementById('editToolSection').value = section;
|
||
document.getElementById('editToolIndex').value = index;
|
||
document.getElementById('editToolName').value = name;
|
||
document.getElementById('editToolUrl').value = url;
|
||
document.getElementById('editToolDesc').value = desc;
|
||
showModal('editToolModal');
|
||
}
|
||
|
||
// ======================== 删除工具 ========================
|
||
function showDeleteToolModal(section, index, name) {
|
||
document.getElementById('deleteToolSection').value = section;
|
||
document.getElementById('deleteToolIndex').value = index;
|
||
document.getElementById('deleteToolInfo').innerHTML =
|
||
'<strong>⚠ 确定要删除工具 "' + name + '" 吗?</strong><br>此操作不可恢复!';
|
||
showModal('deleteToolModal');
|
||
}
|
||
|
||
// ======================== 批量导入预览 ========================
|
||
function previewImport() {
|
||
const data = document.getElementById('importData').value.trim();
|
||
const format = document.getElementById('importFormat').value;
|
||
const preview = document.getElementById('importPreview');
|
||
|
||
if (!data) {
|
||
preview.innerHTML = '<div class="preview-empty">请输入数据后点击预览</div>';
|
||
return;
|
||
}
|
||
|
||
const lines = data.split('\n').filter(l => l.trim());
|
||
let html = '<h4 style="margin-bottom:8px;">解析预览(共 ' + lines.length + ' 行)</h4>';
|
||
html += '<div class="table-wrap"><table><thead><tr><th>名称</th><th>URL</th><th>描述</th></tr></thead><tbody>';
|
||
|
||
let validCount = 0;
|
||
lines.forEach((line, i) => {
|
||
let parts;
|
||
if (format === 'csv') {
|
||
parts = parseCSVLine(line);
|
||
} else {
|
||
parts = line.split('|').map(s => s.trim());
|
||
}
|
||
const name = parts[0] || '';
|
||
const url = parts[1] || '';
|
||
const desc = parts[2] || '';
|
||
if (name || url) validCount++;
|
||
html += '<tr>' +
|
||
'<td>' + escapeHtml(name) + '</td>' +
|
||
'<td style="word-break:break-all;font-size:0.9em;">' + escapeHtml(url) + '</td>' +
|
||
'<td style="color:var(--text-sec);font-size:0.9em;">' + escapeHtml(desc) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
html += '</tbody></table></div>';
|
||
html += '<p style="margin-top:8px;color:var(--text-sec);">解析有效条目: ' + validCount + ' 行</p>';
|
||
|
||
preview.innerHTML = html;
|
||
}
|
||
|
||
// 简单CSV解析(支持引号)
|
||
function parseCSVLine(line) {
|
||
const result = [];
|
||
let current = '';
|
||
let inQuotes = false;
|
||
for (let i = 0; i < line.length; i++) {
|
||
const c = line[i];
|
||
if (c === '"') {
|
||
if (inQuotes && i + 1 < line.length && line[i + 1] === '"') {
|
||
current += '"';
|
||
i++;
|
||
} else {
|
||
inQuotes = !inQuotes;
|
||
}
|
||
} else if (c === ',' && !inQuotes) {
|
||
result.push(current.trim());
|
||
current = '';
|
||
} else {
|
||
current += c;
|
||
}
|
||
}
|
||
result.push(current.trim());
|
||
return result;
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const d = document.createElement('div');
|
||
d.textContent = text;
|
||
return d.innerHTML;
|
||
}
|
||
|
||
// ======================== 拖拽排序(保留原有功能) ========================
|
||
(function() {
|
||
const tbody = document.getElementById('sortable-tbody');
|
||
if (!tbody) return;
|
||
let draggedRow = null;
|
||
|
||
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-num');
|
||
if (orderSpan) orderSpan.textContent = index + 1;
|
||
});
|
||
}
|
||
|
||
// 提交前生成排序字段
|
||
const form = tbody?.closest('form');
|
||
if (form) {
|
||
form.addEventListener('submit', function(e) {
|
||
if (!e.target.querySelector('[name="save_order"]')) return;
|
||
const orderInputs = document.getElementById('order-inputs');
|
||
orderInputs.innerHTML = '';
|
||
const rows = tbody.querySelectorAll('.draggable-row');
|
||
rows.forEach((row) => {
|
||
const input = document.createElement('input');
|
||
input.type = 'hidden';
|
||
input.name = 'orders[]';
|
||
input.value = row.getAttribute('data-filename');
|
||
orderInputs.appendChild(input);
|
||
});
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
<?php
|
||
$database->close();
|
||
?>
|