prepare('INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'); $st->execute([$key, $value]); } /** 删除当前已上传的自定义 logo 文件(仅限 data/logo 目录内) */ function remove_old_logo(): void { $old = setting_get('site_logo', ''); if ($old !== '' && strpos($old, 'data/logo/') === 0) { $file = DATA_DIR . '/logo/' . basename($old); if (is_file($file)) { @unlink($file); } } } function alert_html(string $kind, string $msg): void { // 改为右下角 Toast:注入 hpToastMsg,由 common.js 在页面加载后自动弹出 echo ''; } $msgKind = ''; $msgText = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!csrf_verify()) { $msgKind = 'err'; $msgText = '安全校验失败,请刷新页面重试。'; } else { $act = (string) ($_POST['act'] ?? ''); switch ($act) { case 'site': settings_set('site_name', trim((string) ($_POST['site_name'] ?? 'SecHome'))); settings_set('site_slogan', trim((string) ($_POST['site_slogan'] ?? ''))); touch_last_updated(); $msgKind = 'ok'; $msgText = '站点信息已保存。'; break; case 'tips': header('Content-Type: application/json; charset=utf-8'); settings_set('show_st_tips', (isset($_POST['show_tips']) && (string) $_POST['show_tips'] === '1') ? '1' : '0'); touch_last_updated(); echo json_encode(['ok' => true, 'msg' => '站长提示开关已保存。'], JSON_UNESCAPED_UNICODE); exit; case 'boards': $names = (array) ($_POST['b_name'] ?? []); $descs = (array) ($_POST['b_desc'] ?? []); $icons = (array) ($_POST['b_icon'] ?? []); $sorts = (array) ($_POST['b_sort'] ?? []); $stUpd = $pdo->prepare('UPDATE categories SET name = ?, description = ?, icon = ?, sort = ? WHERE id = ?'); foreach (array_keys($names) as $i) { $name = trim((string) ($names[$i] ?? '')); $desc = trim((string) ($descs[$i] ?? '')); $icon = trim((string) ($icons[$i] ?? '')); $sort = max(0, (int) ($sorts[$i] ?? 0)); $id = (int) $i; $key = $pdo->query('SELECT key FROM categories WHERE id = ' . $id)->fetchColumn(); if ($name === '' || !$key || !in_array((string) $key, $catKeys, true)) continue; $stUpd->execute([$name, $desc, $icon, $sort, $id]); } touch_last_updated(); $msgKind = 'ok'; $msgText = '板块名称 / 说明 / 图标 / 顺序已保存。'; break; case 'footer': settings_set('icp_no', trim((string) ($_POST['icp_no'] ?? ''))); settings_set('gongan_no', trim((string) ($_POST['gongan_no'] ?? ''))); settings_set('gongan_link', trim((string) ($_POST['gongan_link'] ?? ''))); settings_set('copyright', trim((string) ($_POST['copyright'] ?? ''))); settings_set('footer_text', trim((string) ($_POST['footer_text'] ?? ''))); touch_last_updated(); $msgKind = 'ok'; $msgText = '底部信息已保存。'; break; case 'quotes': $content = (string) ($_POST['quotes'] ?? ''); $file = DATA_DIR . '/quota.txt'; if (!is_dir(DATA_DIR)) @mkdir(DATA_DIR, 0777, true); $savedBytes = @file_put_contents($file, $content); if ($savedBytes === false) { $msgKind = 'err'; $msgText = '名言文件写入失败,请检查 data 目录权限。'; } else { touch_last_updated(); $msgKind = 'ok'; $msgText = '名言库已保存。'; } break; case 'logo': $up = $_FILES['logo_file'] ?? null; if (!$up || ($up['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { $msgKind = 'err'; $msgText = '未收到有效图片,请重新选择。'; break; } if (($up['size'] ?? 0) > 2 * 1024 * 1024) { $msgKind = 'err'; $msgText = '图片不能超过 2MB。'; break; } $extMap = [ 'png' => 'png', 'jpg' => 'jpg', 'jpeg' => 'jpg', 'gif' => 'gif', 'webp' => 'webp', 'svg' => 'svg', 'ico' => 'ico', ]; $ext = strtolower(pathinfo((string) $up['name'], PATHINFO_EXTENSION)); if (!isset($extMap[$ext])) { $msgKind = 'err'; $msgText = '仅支持 png / jpg / gif / webp / svg / ico 图片。'; break; } $logoDir = DATA_DIR . '/logo'; if (!is_dir($logoDir)) { @mkdir($logoDir, 0777, true); } $filename = 'site_logo_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.' . $extMap[$ext]; if (!@move_uploaded_file($up['tmp_name'], $logoDir . '/' . $filename)) { $msgKind = 'err'; $msgText = '文件保存失败,请检查 data 目录写入权限。'; break; } remove_old_logo(); settings_set('site_logo', 'data/logo/' . $filename); touch_last_updated(); $msgKind = 'ok'; $msgText = 'LOGO 已更新并应用到全站。'; break; case 'logo_clear': remove_old_logo(); settings_set('site_logo', ''); touch_last_updated(); $msgKind = 'ok'; $msgText = '已恢复默认 LOGO。'; break; case 'hero': settings_set('hero_bg', trim((string) ($_POST['hero_bg'] ?? ''))); touch_last_updated(); $msgKind = 'ok'; $msgText = '首页顶部标识栏背景已保存。'; break; case 'hero_mode': $mode = (string) ($_POST['hero_mode'] ?? 'weather'); if (!in_array($mode, ['weather', 'info', 'blank'], true)) { $mode = 'weather'; } settings_set('hero_mode', $mode); touch_last_updated(); $modeText = ['weather' => '天气模式', 'info' => '访客信息模式', 'blank' => '空白精简模式'][$mode]; $msgKind = 'ok'; $msgText = '首页顶部组件模式已保存:' . $modeText . '。'; break; case 'cdn': settings_set('cdn_ranges', trim((string) ($_POST['cdn_ranges'] ?? ''))); touch_last_updated(); $msgKind = 'ok'; $msgText = 'CDN IP 段库已保存。'; break; case 'engines': $raw = (string) ($_POST['data'] ?? ''); $rows = json_decode($raw, true); if (!is_array($rows)) { $msgKind = 'err'; $msgText = '引擎数据格式错误。'; break; } $list = []; foreach ($rows as $e) { if (!is_array($e)) continue; $name = trim((string) ($e['name'] ?? '')); $url = trim((string) ($e['url'] ?? '')); $icon = trim((string) ($e['icon'] ?? '')); if ($name === '' || $url === '' || strpos($url, '{kw}') === false) continue; $list[] = [ 'key' => 'e' . (count($list) + 1), 'name' => $name, 'icon' => ($icon !== '' ? $icon : 'S'), 'url' => $url, ]; } if (!$list) { $msgKind = 'err'; $msgText = '至少需要保留一个有效搜索引擎(名称与含 {kw} 的地址必填)。'; break; } settings_set('search_engines', json_encode($list, JSON_UNESCAPED_UNICODE)); touch_last_updated(); $msgKind = 'ok'; $msgText = '已保存 ' . count($list) . ' 个搜索引擎。'; break; case 'engines_reset': settings_set('search_engines', ''); touch_last_updated(); $msgKind = 'ok'; $msgText = '已恢复默认搜索引擎(Bing / 百度 / GitHub / Google)。'; break; case 'quicklinks': $raw = (string) ($_POST['data'] ?? ''); $rows = json_decode($raw, true); if (!is_array($rows)) { $msgKind = 'err'; $msgText = '快捷站点数据格式错误。'; break; } $clean = []; foreach ($rows as $i => $r) { if (!is_array($r)) continue; $name = trim((string) ($r['name'] ?? '')); $url = trim((string) ($r['url'] ?? '')); $icon = trim((string) ($r['icon'] ?? '')); $note = trim((string) ($r['note'] ?? '')); if ($name === '' && $url === '') continue; if ($name === '' || $url === '') { $msgKind = 'err'; $msgText = '快捷站点第 ' . ((int) $i + 1) . ' 行:名称与地址均不能为空。'; break; } if (!preg_match('#^https?://#i', $url)) { $msgKind = 'err'; $msgText = '快捷站点第 ' . ((int) $i + 1) . ' 行:地址需为完整 http(s) 链接。'; break; } $clean[] = [ 'id' => (int) ($r['id'] ?? 0), 'icon' => ($icon !== '' ? $icon : '🔗'), 'name' => $name, 'url' => $url, 'note' => $note, 'enabled' => (((string) ($r['enabled'] ?? '1') === '1')) ? 1 : 0, ]; } if ($msgKind === '' && !$clean) { $msgKind = 'err'; $msgText = '至少需要保留一个快捷站点。'; } if ($msgKind === '') { try { $pdo->beginTransaction(); $oldIds = array_map('intval', array_column($pdo->query('SELECT id FROM quick_links')->fetchAll(), 'id')); $keep = []; foreach ($clean as $c) { if ($c['id'] > 0) $keep[] = $c['id']; } $del = array_values(array_diff($oldIds, $keep)); foreach ($del as $did) { $pdo->prepare('DELETE FROM quick_links WHERE id = ?')->execute([$did]); } $stUpd = $pdo->prepare('UPDATE quick_links SET name=?, url=?, icon=?, note=?, enabled=?, sort=? WHERE id=?'); $stIns = $pdo->prepare('INSERT INTO quick_links (name, url, icon, note, enabled, sort) VALUES (?,?,?,?,?,?)'); $sort = 0; foreach ($clean as $c) { $sort++; if ($c['id'] > 0 && in_array($c['id'], $oldIds, true)) { $stUpd->execute([$c['name'], $c['url'], $c['icon'], $c['note'], $c['enabled'], $sort, $c['id']]); } else { $stIns->execute([$c['name'], $c['url'], $c['icon'], $c['note'], $c['enabled'], $sort]); } } $pdo->commit(); touch_last_updated(); $msgKind = 'ok'; $msgText = '快捷站点已保存。'; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } $msgKind = 'err'; $msgText = '保存失败:' . $e->getMessage(); } } break; case 'qtoggle': header('Content-Type: application/json; charset=utf-8'); $qlId = (int) ($_POST['id'] ?? 0); $qlEn = ((string) ($_POST['enabled'] ?? '') === '1') ? 1 : 0; if ($qlId > 0) { $pdo->prepare('UPDATE quick_links SET enabled = ? WHERE id = ?')->execute([$qlEn, $qlId]); touch_last_updated(); echo json_encode(['ok' => true]); } else { echo json_encode(['ok' => false, 'msg' => '无效的记录 ID。'], JSON_UNESCAPED_UNICODE); } exit; case 'pwd': $np = (string) ($_POST['new_pwd'] ?? ''); $np2 = (string) ($_POST['new_pwd2'] ?? ''); if ($np !== $np2) { $msgKind = 'err'; $msgText = '两次输入的新密码不一致。'; } else { [$ok, $text] = hp_change_password((string) ($_POST['old_pwd'] ?? ''), $np); $msgKind = $ok ? 'ok' : 'err'; $msgText = $text; } break; default: $msgKind = 'err'; $msgText = '未知操作。'; } } } // 板块数据(顺序展示,sort 同值按 id 兜底) $cats = $pdo->query('SELECT * FROM categories ORDER BY sort ASC, id ASC')->fetchAll(); // 板块说明 seed(未编辑过时为空则给占位) $boardDefaults = [ 'popular' => '面向大众与初学者的计算机科学、网络安全与前沿技术科普。', 'red' => '面向渗透测试与攻防演练的红队资源集。', 'blue' => '面向防御侧:应急响应、威胁狩猎、取证溯源。', 'tool' => '日常效率与安全实用小工具,直达功能区。', 'article' => '站长原创与整理收录的图文与 Markdown 文章合集。', ]; $logo = setting_get('site_logo', ''); $quotesFile = DATA_DIR . '/quota.txt'; $quotesContent = is_file($quotesFile) ? (string) @file_get_contents($quotesFile) : ''; $all = []; foreach ($pdo->query('SELECT key, value FROM settings') as $r) { $all[$r['key']] = (string) $r['value']; } $site = $all['site_name'] ?? 'SecHome'; layout_head('网站管理'); admin_topbar('content'); ?>

网站管理

对全站展示内容与 logo 进行维护,保存后自动更新首页"上次更新时间"。

提示开关
开启时,前台显示“可切换搜索引擎进行搜索…”等辅助旁白

前台类似“可切换搜索引擎进行搜索(4 个可用,后台可维护)”、各分区说明条等辅助旁白,以及文案中带后台入口字样的引导句,均带 st-tip 标记;关闭后这些文案在全部前台页面统一隐藏,站点导航、搜索、内容等核心功能不受影响。各分区/页面在没有数据时展示的空态占位框(如“该库还没有导航内容”)不受本开关影响,会始终显示以避免空页突兀。

站点信息
站点LOGO
当前logo 当前生效的 logo;更换后浏览器标签页图标(favicon)与页面头部 logo 都会更新。
首页顶部背景

纯色直接填色值(如 #2563eb);渐变填完整 CSS,如 linear-gradient(135deg, #1f3a5f 0%, #2563eb 100%)。夜间模式沿用同一背景。

首页顶部组件

天气模式(默认):横幅展示第三方天气条;访客信息模式:横幅显示当前访问者 IP、浏览器、操作系统、内核、屏幕分辨率与系统语言;空白模式:隐藏横幅,仅保留站点信息与时间信息,顶部高度自动缩减。

底部信息
搜索引擎管理

图标用于搜索栏切换按钮常驻展示;搜索引擎全称在展开下拉列表时才显示。搜索地址需包含 {kw} 占位符,例如 https://www.bing.com/search?q={kw}。列表按从上到下顺序生效,可先删行再重新添加来调整顺序。

滚动信息栏
CDN库

识别时会按“先命中即归为 CDN”处理;未命中且非内网的 IP 一律归为公网。默认内置 Cloudflare 官方 IPv4 段,可按需补充阿里云 / 腾讯云等厂商段。

修改密码
板块设置
首页快捷站点

用于放置本域名下的其它网站入口;点击后新开标签页。地址需为完整 http(s) 链接。列表顺序即展示顺序。

query('SELECT * FROM quick_links ORDER BY sort ASC, id ASC')->fetchAll(); ?>