751 lines
40 KiB
PHP
751 lines
40 KiB
PHP
<?php
|
||
/**
|
||
* admin/content.php —— 内容与顺序 / 站点设置
|
||
* 含:站点信息、板块名称与说明、logo 上传(favicon + 内容 logo 同源)、
|
||
* 名言库(quota.txt)、底部备案信息、分类顺序、修改密码。
|
||
*/
|
||
$P = '../';
|
||
require_once __DIR__ . '/_guard.php';
|
||
|
||
$pdo = db();
|
||
$catKeys = ['popular', 'red', 'blue', 'tool', 'article'];
|
||
|
||
function settings_set(string $key, string $value): void
|
||
{
|
||
$st = db()->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
|
||
{
|
||
echo '<div class="' . ($kind === 'ok' ? 'ok-msg' : 'err-msg') . '">' . he($msg) . '</div>';
|
||
}
|
||
|
||
$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'] ?? '知识导航站')));
|
||
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 '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'] ?? '知识导航站';
|
||
|
||
layout_head('内容与顺序');
|
||
admin_topbar('content');
|
||
?>
|
||
<main class="page-main">
|
||
<div class="wrap page-body">
|
||
<h2 style="margin-bottom:6px">内容与顺序</h2>
|
||
<p class="tip">对全站展示内容与 logo 进行维护,保存后自动更新首页"上次更新时间"。</p>
|
||
|
||
<?php if ($msgText !== ''): alert_html($msgKind, $msgText); endif; ?>
|
||
|
||
<div class="form-grid">
|
||
<!-- 站长提示总开关 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">站长提示(全站辅助说明文案总开关)</div>
|
||
<?= csrf_field() ?>
|
||
<div class="field-row" style="align-items:center;margin:2px 0">
|
||
<label class="sw" title="点击立即启用/关闭站长提示(即时生效,无需保存)"><input type="checkbox" class="sw-in tips-toggle"<?= ((string)($all['show_st_tips'] ?? '1') === '1') ? ' checked' : '' ?>><span class="sw-b"><span class="sw-on">ON</span><span class="sw-off">OFF</span></span></label>
|
||
<span class="tip" style="margin:0">开启时,前台显示“可切换搜索引擎进行搜索…”等辅助旁白</span>
|
||
</div>
|
||
<p class="tip" style="margin-top:6px">前台类似“可切换搜索引擎进行搜索(4 个可用,后台可维护)”、各分区说明条等辅助旁白,以及文案中带后台入口字样的引导句,均带 <code>st-tip</code> 标记;关闭后这些文案在全部前台页面统一隐藏,站点导航、搜索、内容等核心功能不受影响。各分区/页面在没有数据时展示的空态占位框(如“该库还没有导航内容”)不受本开关影响,会始终显示以避免空页突兀。</p>
|
||
</div>
|
||
|
||
<!-- 站点信息 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">站点信息</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="site">
|
||
<label class="fl">站点名称(浏览器标题 / 页面头部显示)</label>
|
||
<input type="text" name="site_name" value="<?= he($site) ?>">
|
||
<label class="fl">副标题 / Slogan</label>
|
||
<input type="text" name="site_slogan" value="<?= he($all['site_slogan'] ?? '') ?>">
|
||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">保存站点信息</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 站点 logo(favicon + 页面内容 logo) -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">站点 LOGO(浏览器标签页图标与页面内容同步生效)</div>
|
||
<div class="logo-preview">
|
||
<img src="<?= he($P . site_logo_rel()) ?>" alt="当前logo">
|
||
<span class="tip">当前生效的 logo;更换后浏览器标签页图标(favicon)与页面头部 logo 都会更新。</span>
|
||
</div>
|
||
<form method="post" enctype="multipart/form-data">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="logo">
|
||
<label class="fl">上传新 LOGO(png / jpg / gif / webp / svg / ico,≤ 2MB)</label>
|
||
<input type="file" name="logo_file" accept=".png,.jpg,.jpeg,.gif,.webp,.svg,.ico,image/*" required>
|
||
<div style="margin-top:12px" class="field-row">
|
||
<button type="submit" class="btn btn-primary">上传并应用</button>
|
||
</div>
|
||
</form>
|
||
<?php if ($logo !== ''): ?>
|
||
<form method="post" data-confirm="确认恢复默认 LOGO?当前自定义图标将被移除。">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="logo_clear">
|
||
<div style="margin-top:8px"><button type="submit" class="btn btn-sm btn-danger">恢复默认 LOGO</button></div>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<!-- 首页顶部标识栏背景 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">首页顶部标识栏背景(Hero 区,支持纯色或 CSS 渐变)</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="hero">
|
||
<label class="fl">背景样式(纯色值或 CSS 渐变;留空恢复主题默认渐变)</label>
|
||
<input type="text" name="hero_bg" id="heroBg" value="<?= he($all['hero_bg'] ?? '') ?>" placeholder="如 #2563eb 或 linear-gradient(135deg, #1f3a5f 0%, #2563eb 100%)">
|
||
<div class="field-row" style="margin:10px 0;align-items:center">
|
||
<input type="color" id="heroColor" value="#2563eb" title="拾取纯色并填入" style="width:40px;height:34px;padding:2px">
|
||
<button type="button" class="btn btn-sm hero-pre" data-v="">默认渐变蓝</button>
|
||
<button type="button" class="btn btn-sm hero-pre" data-v="#2563eb">纯色蓝</button>
|
||
<button type="button" class="btn btn-sm hero-pre" data-v="linear-gradient(135deg,#1e3a8a 0%,#1d4ed8 55%,#7c3aed 100%)">深蓝紫渐变</button>
|
||
<button type="button" class="btn btn-sm hero-pre" data-v="linear-gradient(135deg,#0f766e 0%,#06b6d4 100%)">青碧渐变</button>
|
||
<button type="button" class="btn btn-sm hero-pre" data-v="linear-gradient(135deg,#7f1d1d 0%,#dc2626 100%)">绯红渐变</button>
|
||
<button type="button" class="btn btn-sm hero-pre" data-v="linear-gradient(135deg,#111827 0%,#374151 100%)">石墨渐变</button>
|
||
</div>
|
||
<p class="tip" style="margin-top:2px">纯色直接填色值(如 #2563eb);渐变填完整 CSS,如 <code>linear-gradient(135deg, #1f3a5f 0%, #2563eb 100%)</code>。夜间模式沿用同一背景。</p>
|
||
<div style="margin-top:10px"><button type="submit" class="btn btn-primary">保存顶部背景</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 底部信息 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">底部信息</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="footer">
|
||
<label class="fl">ICP 备案号</label>
|
||
<input type="text" name="icp_no" value="<?= he($all['icp_no'] ?? '') ?>" placeholder="如:京ICP备XXXXXXXX号-1">
|
||
<label class="fl">公安备案号</label>
|
||
<input type="text" name="gongan_no" value="<?= he($all['gongan_no'] ?? '') ?>">
|
||
<label class="fl">公安备案号链接(可选,填写后备案号可点击跳转;建议使用官方查询页 https://beian.mps.gov.cn/)</label>
|
||
<input type="url" name="gongan_link" value="<?= he($all['gongan_link'] ?? '') ?>" placeholder="https://beian.mps.gov.cn/…">
|
||
<label class="fl">版权声明(可用 {site_name} 占位)</label>
|
||
<input type="text" name="copyright" value="<?= he($all['copyright'] ?? '') ?>">
|
||
<label class="fl">其他底部文本(可选)</label>
|
||
<input type="text" name="footer_text" value="<?= he($all['footer_text'] ?? '') ?>">
|
||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">保存底部信息</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 搜索引擎(导航区搜索栏) -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">搜索引擎(导航区搜索栏可切换的引擎)</div>
|
||
<form method="post" id="engineForm">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="engines">
|
||
<textarea name="data" id="engData" hidden></textarea>
|
||
<p class="tip">图标用于搜索栏切换按钮常驻展示;搜索引擎全称在展开下拉列表时才显示。搜索地址需包含 {kw} 占位符,例如 https://www.bing.com/search?q={kw}。列表按从上到下顺序生效,可先删行再重新添加来调整顺序。</p>
|
||
<div id="engineRows">
|
||
<?php $curEngines = search_engines(); foreach ($curEngines as $e): ?>
|
||
<div class="e-row">
|
||
<input type="text" class="e-icon" value="<?= he((string)($e['icon'] ?? 'S')) ?>" maxlength="2" placeholder="图标" title="按钮图标(1 个字符或 emoji)">
|
||
<input type="text" class="e-name" value="<?= he((string)($e['name'] ?? '')) ?>" placeholder="名称(如:百度)">
|
||
<input type="text" class="e-url" value="<?= he((string)($e['url'] ?? '')) ?>" placeholder="搜索地址模板,必须包含 {kw}">
|
||
<button type="button" class="btn btn-sm btn-danger e-del" title="移除该引擎">✕</button>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="field-row" style="margin:10px 0">
|
||
<button type="button" class="btn btn-sm" id="engAdd">+ 添加引擎</button>
|
||
<button type="submit" class="btn btn-primary">保存搜索引擎</button>
|
||
</div>
|
||
</form>
|
||
<form method="post" data-confirm="确认恢复为默认的 4 个搜索引擎?当前自定义引擎将全部被覆盖。">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="engines_reset">
|
||
<button type="submit" class="btn btn-sm btn-danger">恢复默认引擎</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 名言库 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">名言库(data/quota.txt,首页随机展示)</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="quotes">
|
||
<label class="fl">逐行一条;以 # 开头或空行会被忽略(仅作注释与分隔)</label>
|
||
<textarea name="quotes" rows="10" style="font-family:Consolas,Menlo,monospace;font-size:13px"><?= he($quotesContent) ?></textarea>
|
||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">保存名言库</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- CDN IP 段库(IP 地址处理工具用) -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">CDN IP 段库(供“IP 地址处理”工具识别 CDN 来源)</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="cdn">
|
||
<label class="fl">每行一条:<code>CIDR 段</code> + 空格 + <code>厂商名</code>;以 # 开头的行为注释,不影响识别</label>
|
||
<textarea name="cdn_ranges" rows="10" style="font-family:Consolas,Menlo,monospace;font-size:13px"><?= he(cdn_ranges_text()) ?></textarea>
|
||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">保存 CDN 段库</button></div>
|
||
<p class="tip" style="margin-top:6px">识别时会按“先命中即归为 CDN”处理;未命中且非内网的 IP 一律归为公网。默认内置 Cloudflare 官方 IPv4 段,可按需补充阿里云 / 腾讯云等厂商段。</p>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 修改密码 -->
|
||
<div class="fieldset-card">
|
||
<div class="fs-title">修改登录密码</div>
|
||
<form method="post" id="pwdForm">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="pwd">
|
||
<label class="fl">原密码</label>
|
||
<input type="password" name="old_pwd" required autocomplete="off">
|
||
<label class="fl">新密码(至少 6 位)</label>
|
||
<input type="password" name="new_pwd" id="pwdNew" required minlength="6" autocomplete="off">
|
||
<label class="fl">确认新密码</label>
|
||
<input type="password" name="new_pwd2" id="pwdNew2" required minlength="6" autocomplete="off">
|
||
<div style="margin-top:12px"><button type="submit" class="btn btn-primary">修改密码</button></div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 四大板块:名称 / 说明 / 顺序 -->
|
||
<div class="fieldset-card col-span-2">
|
||
<div class="fs-title">四大板块(名称 / 悬停说明文案 / 顺序)</div>
|
||
<form method="post">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="boards">
|
||
<?php foreach ($cats as $c): $id = (int)$c['id']; ?>
|
||
<div class="m-block" style="margin-bottom:10px">
|
||
<div class="m-block-head">
|
||
<strong><?= he((string)$c['key']) ?></strong>
|
||
<span class="ops"><input type="number" name="b_sort[<?= $id ?>]" value="<?= (int)$c['sort'] ?>" min="1" max="99" style="width:70px" title="顺序数值,越小越靠前"></span>
|
||
</div>
|
||
<div class="m-items">
|
||
<div class="icon-ctl" style="margin-top:8px">
|
||
<span class="icon-preview"></span>
|
||
<input type="text" class="icon-input" name="b_icon[<?= $id ?>]" value="<?= he((string)($c['icon'] ?? '')) ?>" placeholder="字符 / emoji,或粘贴/上传图片">
|
||
<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>
|
||
<input type="file" class="icon-file" accept="image/*" hidden>
|
||
</div>
|
||
<input type="text" name="b_name[<?= $id ?>]" value="<?= he((string)$c['name']) ?>" placeholder="板块名称" style="margin-top:8px">
|
||
<textarea name="b_desc[<?= $id ?>]" rows="2" placeholder="悬停说明文案"><?= he(trim((string)$c['description']) !== '' ? (string)$c['description'] : ($boardDefaults[(string)$c['key']] ?? '')) ?></textarea>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
<button type="submit" class="btn btn-primary">保存板块设置</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- 首页快捷站点(本域名其它网站) -->
|
||
<div class="fieldset-card col-span-2">
|
||
<div class="fs-title">首页快捷站点(四大板块上方的胶囊按钮,新标签页打开)</div>
|
||
<form method="post" id="qlForm">
|
||
<?= csrf_field() ?><input type="hidden" name="act" value="quicklinks">
|
||
<textarea name="data" id="qlData" hidden></textarea>
|
||
<p class="tip">用于放置本域名下的其它网站入口;点击后新开标签页。地址需为完整 http(s) 链接。列表顺序即展示顺序。</p>
|
||
<div id="qlRows">
|
||
<?php $quickLinks = $pdo->query('SELECT * FROM quick_links ORDER BY sort ASC, id ASC')->fetchAll(); ?>
|
||
<?php foreach ($quickLinks as $ql): ?>
|
||
<div class="l-row" data-id="<?= (int)$ql['id'] ?>">
|
||
<span class="icon-ctl">
|
||
<span class="icon-preview"></span>
|
||
<input type="text" class="icon-input l-icon" value="<?= he((string)$ql['icon']) ?>" placeholder="图标">
|
||
<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>
|
||
<input type="file" class="icon-file" accept="image/*" hidden>
|
||
</span>
|
||
<input type="text" class="l-name" value="<?= he((string)$ql['name']) ?>" placeholder="站点名称(必填)">
|
||
<input type="text" class="l-url" value="<?= he((string)$ql['url']) ?>" placeholder="完整网址,如 https://子站.域名.com">
|
||
<input type="text" class="l-note" value="<?= he((string)($ql['note'] ?? '')) ?>" placeholder="备注 / 网站介绍(悬停首页按钮显示)">
|
||
<label class="sw" title="点击立即启停(即时生效,无需保存)"><input type="checkbox" class="sw-in l-enabled"<?= (int)$ql['enabled'] === 1 ? ' checked' : '' ?>><span class="sw-b"><span class="sw-on">ON</span><span class="sw-off">OFF</span></span></label>
|
||
<span class="ops">
|
||
<button type="button" class="btn btn-sm l-up" title="上移">↑</button>
|
||
<button type="button" class="btn btn-sm l-down" title="下移">↓</button>
|
||
<button type="button" class="btn btn-sm btn-danger l-del" title="删除">✕</button>
|
||
</span>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="field-row" style="margin:12px 0">
|
||
<button type="button" class="btn btn-sm" id="qlAdd">+ 添加快捷站点</button>
|
||
<button type="submit" class="btn btn-primary">保存快捷站点</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<?php layout_theme_fab(); ?>
|
||
<script src="../assets/js/common.js"></script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
var rows = document.getElementById('engineRows');
|
||
function esc(s) { return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<'); }
|
||
function rowHTML(icon, name, url) {
|
||
return '<div class="e-row">' +
|
||
'<input type="text" class="e-icon" value="' + esc(icon) + '" maxlength="2" placeholder="图标" title="按钮图标(1 个字符或 emoji)">' +
|
||
'<input type="text" class="e-name" value="' + esc(name) + '" placeholder="名称(如:百度)">' +
|
||
'<input type="text" class="e-url" value="' + esc(url) + '" placeholder="搜索地址模板,必须包含 {kw}">' +
|
||
'<button type="button" class="btn btn-sm btn-danger e-del" title="移除该引擎">✕</button></div>';
|
||
}
|
||
|
||
rows.addEventListener('click', function (ev) {
|
||
if (ev.target.classList.contains('e-del')) {
|
||
ev.target.closest('.e-row').remove();
|
||
}
|
||
});
|
||
document.getElementById('engAdd').addEventListener('click', function () {
|
||
rows.insertAdjacentHTML('beforeend', rowHTML('', '', ''));
|
||
});
|
||
|
||
// 引擎保存:收集校验
|
||
document.getElementById('engineForm').addEventListener('submit', function (ev) {
|
||
var list = [], bad = false;
|
||
Array.prototype.forEach.call(rows.querySelectorAll('.e-row'), function (r, i) {
|
||
var name = r.querySelector('.e-name').value.trim();
|
||
var url = r.querySelector('.e-url').value.trim();
|
||
var icon = r.querySelector('.e-icon').value.trim();
|
||
if (!name && !url) return; // 空行忽略
|
||
if (!name || !url || url.indexOf('{kw}') === -1) { bad = true; return; }
|
||
list.push({ key: 'e' + (i + 1), name: name, icon: (icon || 'S'), url: url });
|
||
});
|
||
if (bad) {
|
||
ev.preventDefault();
|
||
alert('存在未填写或搜索地址缺少 {kw} 的引擎行,请检查后重试。');
|
||
return;
|
||
}
|
||
if (!list.length) {
|
||
ev.preventDefault();
|
||
alert('至少需要保留一个有效的搜索引擎。');
|
||
return;
|
||
}
|
||
document.getElementById('engData').value = JSON.stringify(list);
|
||
});
|
||
|
||
// 修改密码:两次新密码一致校验
|
||
var pwdForm = document.getElementById('pwdForm');
|
||
if (pwdForm) {
|
||
pwdForm.addEventListener('submit', function (ev) {
|
||
var a = document.getElementById('pwdNew').value;
|
||
var b = document.getElementById('pwdNew2').value;
|
||
if (a !== b) {
|
||
ev.preventDefault();
|
||
alert('两次输入的新密码不一致。');
|
||
}
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
var qlRows = document.getElementById('qlRows');
|
||
if (!qlRows) return;
|
||
function qEsc(s) { return String(s).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); }
|
||
function qRowHTML() {
|
||
return '<div class="l-row" data-id="0">' +
|
||
'<span class="icon-ctl">' +
|
||
'<span class="icon-preview"></span>' +
|
||
'<input type="text" class="icon-input l-icon" value="" placeholder="图标">' +
|
||
'<button type="button" class="btn btn-sm icon-upload" title="上传图片(≤200KB)">上传</button>' +
|
||
'<input type="file" class="icon-file" accept="image/*" hidden>' +
|
||
'</span>' +
|
||
'<input type="text" class="l-name" value="" placeholder="站点名称(必填)">' +
|
||
'<input type="text" class="l-url" value="" placeholder="完整网址,如 https://子站.域名.com">' +
|
||
'<input type="text" class="l-note" value="" placeholder="备注 / 网站介绍(悬停首页按钮显示)">' +
|
||
'<label class="sw" title="点击立即启停(即时生效,无需保存)"><input type="checkbox" class="sw-in l-enabled" checked><span class="sw-b"><span class="sw-on">ON</span><span class="sw-off">OFF</span></span></label>' +
|
||
'<span class="ops">' +
|
||
'<button type="button" class="btn btn-sm l-up" title="上移">↑</button>' +
|
||
'<button type="button" class="btn btn-sm l-down" title="下移">↓</button>' +
|
||
'<button type="button" class="btn btn-sm btn-danger l-del" title="删除">✕</button>' +
|
||
'</span></div>';
|
||
}
|
||
qlRows.addEventListener('click', function (ev) {
|
||
var row = ev.target.closest('.l-row');
|
||
if (!row) return;
|
||
if (ev.target.classList.contains('l-del')) {
|
||
(window.HomePage && HomePage.ask ? HomePage.ask('确认删除该快捷站点?', { okText: '删除' }) : Promise.resolve(window.confirm('确认删除该快捷站点?')))
|
||
.then(function (ok) { if (ok) row.remove(); });
|
||
} else if (ev.target.classList.contains('l-up') || ev.target.classList.contains('l-down')) {
|
||
var sib = ev.target.classList.contains('l-up') ? row.previousElementSibling : row.nextElementSibling;
|
||
if (sib && sib.classList.contains('l-row')) {
|
||
if (ev.target.classList.contains('l-up')) qlRows.insertBefore(row, sib);
|
||
else qlRows.insertBefore(sib, row);
|
||
}
|
||
}
|
||
});
|
||
document.getElementById('qlAdd').addEventListener('click', function () {
|
||
qlRows.insertAdjacentHTML('beforeend', qRowHTML());
|
||
});
|
||
qlRows.addEventListener('change', function (ev) {
|
||
var input = ev.target;
|
||
if (!input.classList.contains('l-enabled')) return;
|
||
var row = input.closest('.l-row');
|
||
var id = row ? parseInt(row.getAttribute('data-id'), 10) : 0;
|
||
var csrfEl = document.querySelector('input[name="csrf"]');
|
||
if (!csrfEl) return;
|
||
if (!id) {
|
||
input.checked = !input.checked;
|
||
alert('该行尚未保存过,请先点击「保存快捷站点」生成记录,之后即可直接拨动开关启停。');
|
||
return;
|
||
}
|
||
var fd = new FormData();
|
||
fd.append('act', 'qtoggle');
|
||
fd.append('csrf', csrfEl.value);
|
||
fd.append('id', String(id));
|
||
fd.append('enabled', input.checked ? '1' : '0');
|
||
fetch(window.location.href, { method: 'POST', body: fd, credentials: 'same-origin' })
|
||
.then(function (res) { return res.json().catch(function () { return { ok: false, msg: '服务器返回异常。' }; }); })
|
||
.then(function (j) {
|
||
if (!j || j.ok !== true) {
|
||
input.checked = !input.checked;
|
||
alert('启停失败:' + ((j && j.msg) || '未知错误'));
|
||
}
|
||
})
|
||
.catch(function () {
|
||
input.checked = !input.checked;
|
||
alert('网络错误,启停未保存。');
|
||
});
|
||
});
|
||
document.getElementById('qlForm').addEventListener('submit', function (ev) {
|
||
var list = [], bad = false, badMsg = '';
|
||
Array.prototype.forEach.call(qlRows.querySelectorAll('.l-row'), function (r, i) {
|
||
if (bad) return;
|
||
var icon = r.querySelector('.l-icon').value.trim();
|
||
var name = r.querySelector('.l-name').value.trim();
|
||
var url = r.querySelector('.l-url').value.trim();
|
||
var note = r.querySelector('.l-note').value.trim();
|
||
var enabled = r.querySelector('.l-enabled').checked ? '1' : '0';
|
||
if (!name && !url) return;
|
||
if (!name || !url) { bad = true; badMsg = '快捷站点第 ' + (i + 1) + ' 行:名称与地址均不能为空。'; return; }
|
||
if (!/^https?:\/\//i.test(url)) { bad = true; badMsg = '快捷站点第 ' + (i + 1) + ' 行:地址需为完整 http(s) 链接。'; return; }
|
||
list.push({ id: parseInt(r.getAttribute('data-id'), 10) || 0, icon: icon, name: name, url: url, note: note, enabled: enabled });
|
||
});
|
||
if (bad) { ev.preventDefault(); alert(badMsg); return; }
|
||
if (!list.length) { ev.preventDefault(); alert('至少需要保留一个快捷站点。'); return; }
|
||
document.getElementById('qlData').value = JSON.stringify(list);
|
||
});
|
||
})();
|
||
</script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
var heroBg = document.getElementById('heroBg');
|
||
if (!heroBg) return;
|
||
var color = document.getElementById('heroColor');
|
||
if (color) {
|
||
color.addEventListener('input', function () { heroBg.value = this.value; });
|
||
}
|
||
Array.prototype.forEach.call(document.querySelectorAll('.hero-pre'), function (b) {
|
||
b.addEventListener('click', function () { heroBg.value = b.getAttribute('data-v') || ''; });
|
||
});
|
||
})();
|
||
</script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
var tips = document.querySelector('.tips-toggle');
|
||
if (!tips) return;
|
||
tips.addEventListener('change', function () {
|
||
var csrfEl = document.querySelector('input[name="csrf"]');
|
||
if (!csrfEl) return;
|
||
var fd = new FormData();
|
||
fd.append('act', 'tips');
|
||
fd.append('csrf', csrfEl.value);
|
||
fd.append('show_tips', tips.checked ? '1' : '0');
|
||
fetch(window.location.href, { method: 'POST', body: fd, credentials: 'same-origin' })
|
||
.then(function (res) { return res.json().catch(function () { return { ok: false, msg: '服务器返回异常。' }; }); })
|
||
.then(function (j) {
|
||
if (!j || j.ok !== true) {
|
||
tips.checked = !tips.checked;
|
||
alert('开关更新失败:' + ((j && j.msg) || '未知错误'));
|
||
}
|
||
})
|
||
.catch(function () {
|
||
tips.checked = !tips.checked;
|
||
alert('网络错误,开关更新未保存。');
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|