NewHome/admin/content.php

663 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* admin/content.php —— 内容与顺序 / 站点设置
* 含站点信息、板块名称与说明、logo 上传favicon + 内容 logo 同源)、
* 名言库quota.txt、底部备案信息、分类顺序、修改密码。
*/
$P = '../';
require_once __DIR__ . '/_guard.php';
$pdo = db();
$catKeys = ['popular', 'red', 'blue', 'tool'];
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 '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 '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' => '日常效率与安全实用小工具,直达功能区。',
];
$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>
<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>
<!-- 站点 logofavicon + 页面内容 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">上传新 LOGOpng / 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" onsubmit="return 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="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">
<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="t-on"><input type="checkbox" class="l-enabled"<?= (int)$ql['enabled'] === 1 ? ' checked' : '' ?>> 启用</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 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" onsubmit="return 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>
</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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;'); }
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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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="t-on"><input type="checkbox" class="l-enabled" checked> 启用</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')) {
if (confirm('确认删除该快捷站点?')) 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());
});
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>
</body>
</html>