Files
SimpleNav/admin/index.php
T
laoyang 717a8877bb 后台排序改造:按板块标签页 + 拖拽排序 + 顺序完全固定
- 后台网站列表按板块分标签页,每个板块单独调顺序;用 URL hash
  (#sgroup-<gid>) 定位,上移/下移提交后回跳到原来的板块
- 支持鼠标拖拽排序(原生 HTML5 拖拽,零库依赖):拖动时实时重排预览,
  松手后整组落库(Ajax 动作 site_reorder,带 CSRF 校验与板块归属校验,
  防止跨板块搬条目)
- 稀疏排序:新增链接的顺序号 = 同板块 MAX + SORT_STEP(1000),天然排最后;
  拖拽落库、历史数据迁移时整组归一化为 index * SORT_STEP
- 修复真根因:旧机制靠「交换两条 sort_order」上移/下移,一旦历史数据里
  出现重复值,交换即无效、位置退化为按 id 排序 —— 这正是「排了顺序又被
  打乱」的来源。新增一次性归一化迁移,用 settings.mig_sort_sparse 作哨兵,
  避免每次请求重复覆盖用户设置
- 新增/修改链接不再用 MAX+1(MAX+1 会落进已有间隙、插到用户排好的中间)
- 改变所属板块时,该条目自动排到新板块的最后
- 后台页面 + 拖拽手柄做了配套样式,触屏设备降级为上下移按钮
2026-09-29 09:21:51 +08:00

501 lines
29 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
/**
* 简航 SimpleNav —— 后台首页(网站 / 板块 / 搜索引擎 / 设置)
*/
declare(strict_types=1);
require_once __DIR__ . '/_auth.php';
require_login();
send_no_cache();
$pdo = db();
$p = isset($_GET['p']) ? (string)$_GET['p'] : 'sites';
if (!in_array($p, ['sites', 'groups', 'engines', 'settings'], true)) {
$p = 'sites';
}
$flash = $_SESSION['flash'] ?? null;
unset($_SESSION['flash']);
$groups = $pdo->query('SELECT * FROM groups ORDER BY sort_order, id')->fetchAll();
$engines = $pdo->query('SELECT * FROM engines ORDER BY sort_order, id')->fetchAll();
$SUGGEST_TYPES = ['bing_osjson', 'baidu_sugrec', 'google_firefox', 'sogou_ajaj', 'so360', 'ddg'];
function flash_html(?array $f): string
{
if (!$f) return '';
$cls = ($f['t'] ?? 'ok') === 'err' ? 'alert err' : 'alert ok';
return '<div class="' . $cls . '">' . e($f['m']) . '</div>';
}
/** 眼睛(显示)/ 斜杠眼(隐藏)图标 —— 用内联 SVG,避免 emoji 字体差异导致显示成方块 */
function eye_svg(bool $on): string
{
$slash = $on ? '' : '<path d="M3 3l18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>';
return '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" fill="currentColor">'
. '<path d="M12 5c-5.5 0-9.6 4.3-11 7 1.4 2.7 5.5 7 11 7s9.6-4.3 11-7c-1.4-2.7-5.5-7-11-7zm0 12a5 5 0 110-10 5 5 0 010 10z"/>'
. '<circle cx="12" cy="12" r="2.6"/>'
. $slash
. '</svg>';
}
/**
* 显隐切换按钮:表单自带 ajax=1,前端原地切换、不跳页;
* 即使浏览器禁用 JS,表单也会正常提交并回到当前列表页(p 已带上)。
*/
function toggle_form(string $action, int $id, int $state, string $p = ''): string
{
$on = $state === 1;
return '<form method="post" action="' . e(url('admin/actions.php')) . '" class="inline js-toggle" data-id="' . $id . '">'
. csrf_field()
. '<input type="hidden" name="action" value="' . e($action) . '">'
. '<input type="hidden" name="id" value="' . $id . '">'
. '<input type="hidden" name="p" value="' . e($p) . '">'
. '<input type="hidden" name="ajax" value="1">'
. '<button type="submit" class="ico-btn toggle' . ($on ? ' on' : '') . '"'
. ' data-on="' . ($on ? '1' : '0') . '"'
. ' title="' . ($on ? '当前:显示中(点击隐藏)' : '当前:已隐藏(点击显示)') . '">'
. eye_svg($on)
. '</button></form>';
}
/** 上/下移按钮(无 JS 回退用;有 JS 时以拖拽为主) */
function move_form(string $action, int $id, string $dir, string $label, string $p, string $extra = ''): string
{
return '<form method="post" action="' . e(url('admin/actions.php')) . '" class="inline">'
. csrf_field() . '<input type="hidden" name="action" value="' . e($action) . '">'
. '<input type="hidden" name="id" value="' . $id . '">'
. '<input type="hidden" name="dir" value="' . $dir . '">'
. '<input type="hidden" name="p" value="' . e($p) . '">'
. $extra
. '<button class="ico-btn" title="' . e($label) . '">' . ($dir === 'up' ? '↑' : '↓') . '</button></form>';
}
/** 拖拽手柄(纯展示,真实拖拽由 admin.js 绑在整行上) */
function drag_handle(): string
{
return '<span class="drag-handle" title="按住拖动调整顺序" aria-hidden="true">'
. '<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">'
. '<circle cx="9" cy="7" r="1.6"/><circle cx="15" cy="7" r="1.6"/>'
. '<circle cx="9" cy="12" r="1.6"/><circle cx="15" cy="12" r="1.6"/>'
. '<circle cx="9" cy="17" r="1.6"/><circle cx="15" cy="17" r="1.6"/>'
. '</svg></span>';
}
ob_start();
if ($p === 'sites'):
// 一次取全量,再按板块分桶(避免每板块一次查询)
$allRows = $pdo->query('SELECT s.*, g.name AS gname FROM sites s JOIN groups g ON g.id=s.group_id ORDER BY g.sort_order, g.id, s.sort_order, s.id')->fetchAll();
$byGroup = [];
foreach ($allRows as $rr) {
$byGroup[(int)$rr['group_id']][] = $rr;
}
$customCount = 0;
foreach ($allRows as $rr) { if ((int)($rr['icon_custom'] ?? 0) === 1) { $customCount++; } }
?>
<?= flash_html($flash) ?>
<section class="card">
<div class="card-head">
<h2>新增 / 编辑网站</h2>
<button type="button" class="btn ghost" id="btn-update-all" data-url="<?= e(url('admin/update_all.php')) ?>">⟳ 全部更新(名称 + 自动图标)</button>
</div>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form" id="site-form" enctype="multipart/form-data">
<?= csrf_field() ?>
<input type="hidden" name="action" value="site_save">
<input type="hidden" name="id" id="f-id" value="0">
<input type="hidden" name="icon" id="f-icon" value="">
<label>板块
<select name="group_id" id="f-group" required>
<?php foreach ($groups as $g): ?>
<option value="<?= (int)$g['id'] ?>"><?= e($g['name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="span2">网址
<span class="row">
<input type="text" name="url" id="f-url" placeholder="https://example.com" required>
<button type="button" class="btn ghost" id="btn-fetch" data-url="<?= e(url('admin/fetch_meta.php')) ?>">自动获取</button>
</span>
</label>
<label>网站名称(自动抓取)
<input type="text" name="name" id="f-name" placeholder="自动抓取">
</label>
<label>简称(页面显示,手动)
<input type="text" name="short_name" id="f-short" placeholder="如:知乎">
</label>
<div class="span2 icon-field">
<div class="icon-field-preview">
<span class="icon-preview" id="f-icon-preview"><span class="ph">无</span></span>
</div>
<div class="icon-field-body">
<div class="icon-field-title">图标</div>
<input type="text" name="icon_url" id="f-icon-url" placeholder="自定义图标图片地址(http/https),留空则用自动获取的">
<div class="row icon-row2">
<input type="file" name="icon_file" id="f-icon-file" accept="image/png,image/jpeg,image/gif,image/webp,image/svg+xml,image/x-icon,.svg,.ico">
<label class="chk"><input type="checkbox" name="icon_auto" id="f-icon-auto" checked> 允许自动更新图标</label>
</div>
<p class="hint">
优先级:<b>上传文件</b> > <b>自定义地址</b> > 「自动获取」抓到的图。<br>
选择上传或填写地址后会自动取消勾选「允许自动更新图标」,该网站图标此后不再被「全部更新」覆盖;
勾回去即可恢复自动。
</p>
</div>
</div>
<label class="chk"><input type="checkbox" name="is_visible" id="f-visible" checked> 显示</label>
<div class="span2 form-actions">
<button type="submit" class="btn primary">保存</button>
<button type="button" class="btn ghost" id="btn-reset">清空</button>
<span class="hint" id="fetch-hint"></span>
</div>
</form>
</section>
<section class="card">
<div class="card-head">
<h2>网站列表(<?= count($allRows) ?>)</h2>
<span class="hint">其中 <?= $customCount ?> 个使用自定义图标(不会自动更新)</span>
</div>
<p class="hint" style="margin-top:-6px">
按<b>板块</b>分成标签页,每个板块单独调整顺序:<b>按住某一行拖动</b>即可改顺序,松手即保存;
也可以用每行右侧的 ↑ ↓ 微调(不支持拖拽的环境用)。
<b>新添加的网站一律排在所属板块的最后</b>,直到你手动把它拖到别处。
</p>
<?php if (!$allRows): ?>
<p class="hint">还没有任何网站,先在上面的表单里添加一个。</p>
<?php else: ?>
<?php
// 板块标签页:只显示有网站的板块(空板块不占位)
$tabGroups = [];
foreach ($groups as $g) {
$gid = (int)$g['id'];
if (!empty($byGroup[$gid])) { $tabGroups[] = ['g' => $g, 'items' => $byGroup[$gid]]; }
}
?>
<div class="sites-tabs" id="sites-tabs" role="tablist" data-reorder-url="<?= e(url('admin/actions.php')) ?>">
<?php foreach ($tabGroups as $i => $t): $gid = (int)$t['g']['id']; ?>
<button type="button" class="tab<?= $i === 0 ? ' active' : '' ?>" role="tab"
aria-selected="<?= $i === 0 ? 'true' : 'false' ?>"
data-target="sgroup-<?= $gid ?>"><?= e($t['g']['name']) ?>
<span class="tab-count"><?= count($t['items']) ?></span>
</button>
<?php endforeach; ?>
</div>
<div class="sites-panels">
<?php foreach ($tabGroups as $i => $t): $gid = (int)$t['g']['id']; ?>
<div class="sites-panel<?= $i === 0 ? ' active' : '' ?>" id="sgroup-<?= $gid ?>" role="tabpanel"
data-group-id="<?= $gid ?>">
<div class="tbl-wrap"><table class="tbl tbl-sortable">
<thead><tr><th class="th-drag"></th><th>简称(展示)</th><th>名称</th><th>网址</th><th>显示</th><th>微调</th><th>操作</th></tr></thead>
<tbody data-group-id="<?= $gid ?>">
<?php foreach ($t['items'] as $r): $label = $r['short_name'] !== '' ? $r['short_name'] : ($r['name'] !== '' ? $r['name'] : domain_of($r['url'])); $src = icon_src($r['icon']); ?>
<tr draggable="true" data-id="<?= (int)$r['id'] ?>">
<td class="td-drag"><?= drag_handle() ?></td>
<td>
<?php if ($src): ?><img class="mini" src="<?= e($src) ?>" alt=""><?php else: ?><span class="mini letter" style="background:<?= e(letter_color($label)) ?>"><?= e(mb_substr($label, 0, 1)) ?></span><?php endif; ?>
<?php if ((int)($r['icon_custom'] ?? 0) === 1): ?><span class="badge" title="用户自定义图标,不会被自动更新覆盖">自定义</span><?php endif; ?>
<?= e($label) ?>
</td>
<td class="muted"><?= e($r['name']) ?></td>
<td class="muted ell"><a href="<?= e($r['url']) ?>" target="_blank" rel="noopener"><?= e($r['url']) ?></a></td>
<td><?= toggle_form('site_toggle', (int)$r['id'], (int)$r['is_visible'], 'sites') ?></td>
<td class="nowrap"><?= move_form('site_move', (int)$r['id'], 'up', '上移', 'sites', '<input type="hidden" name="reload" value="1"><input type="hidden" name="gid" value="' . $gid . '">') . move_form('site_move', (int)$r['id'], 'down', '下移', 'sites', '<input type="hidden" name="reload" value="1"><input type="hidden" name="gid" value="' . $gid . '">') ?></td>
<td class="nowrap">
<button type="button" class="ico-btn edit-site"
data-id="<?= (int)$r['id'] ?>" data-group="<?= (int)$r['group_id'] ?>"
data-url="<?= e($r['url']) ?>" data-name="<?= e($r['name']) ?>"
data-short="<?= e($r['short_name']) ?>" data-icon="<?= e($r['icon']) ?>"
data-icon-url="<?= e($src) ?>" data-custom="<?= (int)($r['icon_custom'] ?? 0) ?>"
data-visible="<?= (int)$r['is_visible'] ?>" title="编辑">✎</button>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="inline" onsubmit="return confirm('确定删除该网站?');">
<?= csrf_field() ?><input type="hidden" name="action" value="site_delete">
<input type="hidden" name="p" value="sites">
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
<button class="ico-btn danger" title="删除">✕</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
<p class="hint sort-status" data-group-id="<?= $gid ?>"></p>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
<?php
elseif ($p === 'groups'): ?>
<?= flash_html($flash) ?>
<section class="card">
<div class="card-head"><h2>新增 / 编辑板块</h2></div>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form" id="group-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="group_save">
<input type="hidden" name="id" id="g-id" value="0">
<input type="hidden" name="p" value="groups">
<label>板块名称<input type="text" name="name" id="g-name" required></label>
<label>展示方式
<select name="display" id="g-display">
<option value="grid">普通网格板块(如:常用 / 工具)</option>
<option value="tabs">合集标签页</option>
</select>
</label>
<label class="chk"><input type="checkbox" name="is_visible" id="g-visible" checked> 显示</label>
<div class="span2 form-actions"><button class="btn primary">保存</button><button type="button" class="btn ghost" id="g-reset">清空</button></div>
</form>
</section>
<section class="card">
<div class="tbl-wrap"><table class="tbl">
<thead><tr><th>名称</th><th>展示方式</th><th>网站数</th><th>显示</th><th>排序</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($groups as $g): $cnt = (int)$pdo->query('SELECT COUNT(*) FROM sites WHERE group_id=' . (int)$g['id'])->fetchColumn(); ?>
<tr>
<td><?= e($g['name']) ?></td>
<td><?= $g['display'] === 'tabs' ? '合集标签页' : '普通网格' ?></td>
<td><?= $cnt ?></td>
<td><?= toggle_form('group_toggle', (int)$g['id'], (int)$g['is_visible'], 'groups') ?></td>
<td class="nowrap"><?= move_form('group_move', (int)$g['id'], 'up', '上移', 'groups') . move_form('group_move', (int)$g['id'], 'down', '下移', 'groups') ?></td>
<td class="nowrap">
<button type="button" class="ico-btn edit-group" data-id="<?= (int)$g['id'] ?>" data-name="<?= e($g['name']) ?>" data-display="<?= e($g['display']) ?>" data-visible="<?= (int)$g['is_visible'] ?>">✎</button>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="inline" onsubmit="return confirm('删除板块会同时删除其下所有网站,确定?');">
<?= csrf_field() ?><input type="hidden" name="action" value="group_delete"><input type="hidden" name="p" value="groups"><input type="hidden" name="id" value="<?= (int)$g['id'] ?>">
<button class="ico-btn danger">✕</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
</section>
<?php
elseif ($p === 'engines'): ?>
<?= flash_html($flash) ?>
<section class="card">
<div class="card-head"><h2>新增 / 编辑搜索引擎</h2></div>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form" id="engine-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="engine_save">
<input type="hidden" name="id" id="e-id" value="0">
<input type="hidden" name="p" value="engines">
<label>名称<input type="text" name="name" id="e-name" required></label>
<label>标识 slug<input type="text" name="slug" id="e-slug" placeholder="bing" required></label>
<label class="span2">搜索地址<input type="text" name="search_url" id="e-search" placeholder="https://cn.bing.com/search" required></label>
<label>参数名<input type="text" name="param" id="e-param" placeholder="q / wd / query"></label>
<label>主题色<input type="text" name="color" id="e-color" placeholder="#60A504"></label>
<label class="span2">联想接口({q} 为关键词占位)<input type="text" name="suggest_url" id="e-sug" placeholder="https://api.bing.com/osjson.aspx?query={q}"></label>
<label>联想解析类型
<select name="suggest_type" id="e-sugtype">
<?php foreach ($SUGGEST_TYPES as $t): ?><option value="<?= e($t) ?>"><?= e($t) ?></option><?php endforeach; ?>
</select>
</label>
<label class="chk"><input type="checkbox" name="is_active" id="e-active"> 启用(显示在首页顶部)</label>
<p class="hint span2">
这两项只在「设置 → 联想词来源」选<b>各搜索引擎自己的接口</b>时才会用到;
默认是统一用百度出建议(搜索本身仍走各自的搜索地址)。
这里填的接口只要通了就会被优先使用,没通就自动回退到百度。
</p>
<div class="span2 form-actions"><button class="btn primary">保存</button><button type="button" class="btn ghost" id="e-reset">清空</button></div>
</form>
</section>
<section class="card">
<div class="card-head">
<h2>搜索引擎列表</h2>
<span class="hint">点最右侧「眼睛」图标即在该行原地切换,不会跳页</span>
</div>
<div class="tbl-wrap"><table class="tbl">
<thead><tr><th>名称</th><th>搜索地址</th><th>联想类型</th><th>前台显示</th><th>排序</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($engines as $en): ?>
<tr>
<td><span style="color:<?= e($en['color']) ?>">■</span> <?= e($en['name']) ?> <span class="muted">(<?= e($en['slug']) ?>)</span></td>
<td class="muted ell"><?= e($en['search_url']) ?></td>
<td class="muted"><?= e($en['suggest_type']) ?></td>
<td class="nowrap">
<span class="state <?= $en['is_active'] ? 'on' : 'off' ?> js-state" data-on-text="已显示" data-off-text="已隐藏"><?= $en['is_active'] ? '已显示' : '已隐藏' ?></span>
<?= toggle_form('engine_toggle', (int)$en['id'], (int)$en['is_active'], 'engines') ?>
</td>
<td class="nowrap"><?= move_form('engine_move', (int)$en['id'], 'up', '上移', 'engines') . move_form('engine_move', (int)$en['id'], 'down', '下移', 'engines') ?></td>
<td class="nowrap">
<button type="button" class="ico-btn edit-engine"
data-id="<?= (int)$en['id'] ?>" data-name="<?= e($en['name']) ?>" data-slug="<?= e($en['slug']) ?>"
data-search="<?= e($en['search_url']) ?>" data-param="<?= e($en['param']) ?>" data-sug="<?= e($en['suggest_url']) ?>"
data-sugtype="<?= e($en['suggest_type']) ?>" data-color="<?= e($en['color']) ?>" data-active="<?= (int)$en['is_active'] ?>">✎</button>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="inline" onsubmit="return confirm('确定删除该引擎?');">
<?= csrf_field() ?><input type="hidden" name="action" value="engine_delete"><input type="hidden" name="p" value="engines"><input type="hidden" name="id" value="<?= (int)$en['id'] ?>">
<button class="ico-btn danger">✕</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
<p class="hint">提示:联想接口需填写对应平台的地址,解析类型可选 bing_osjson / baidu_sugrec / google_firefox / sogou_ajaj / so360 / ddg。「前台显示」为该引擎是否出现在首页顶部搜索框上方的切换条里。</p>
</section>
<?php
else: // settings ?>
<?= flash_html($flash) ?>
<section class="card">
<div class="card-head"><h2>站点设置</h2></div>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="settings_save">
<label>站点名称<input type="text" name="site_name" value="<?= e(setting('site_name', '简航')) ?>"></label>
<label>副标题<input type="text" name="subtitle" value="<?= e(setting('subtitle')) ?>" placeholder="留空则不显示"></label>
<p class="hint" style="grid-column:1/-1;margin:-6px 0 6px">
副标题显示在首页站点名称下方(也会出现在浏览器标题里)。<b>留空即隐藏</b>,不会退回默认文案。
</p>
<label>默认搜索引擎
<select name="default_engine">
<?php foreach ($engines as $en): ?>
<option value="<?= e($en['slug']) ?>" <?= setting('default_engine') === $en['slug'] ? 'selected' : '' ?>><?= e($en['name']) ?></option>
<?php endforeach; ?>
</select>
</label>
<label>联想词来源
<select name="suggest_source">
<?php
$cur = setting('suggest_source', 'baidu');
$opts = [['baidu', '百度(推荐,最快)'], ['bing', '必应'], ['so360', '360'], ['auto', '各搜索引擎自己的接口']];
$known = array_column($engines, 'slug');
foreach ($opts as $o):
if ($o[0] !== 'auto' && !in_array($o[0], $known, true)) { continue; }
?>
<option value="<?= e($o[0]) ?>" <?= $cur === $o[0] ? 'selected' : '' ?>><?= e($o[1]) ?></option>
<?php endforeach; ?>
</select>
</label>
<p class="hint span2">
搜索时仍然用你选中的那个引擎,这里只决定<b>下拉建议词</b>从哪儿来。
实测(2026-09-28):<b>百度 0.12s、360 0.20s、必应 0.5~1.6s</b> 可用;
<b>搜狗接口已失效</b>(返回 0 字节)、<b>谷歌与 DuckDuckGo 国内连不上</b>。
所以默认「统一用百度」——这样切到任何引擎下拉都有词。选「各搜索引擎自己的接口」则恢复成
各用各的(没恢复好的引擎会自动回退到百度的建议)。
</p>
<label>联想词按键
<?php $sk = setting('suggest_keys', 'both'); ?>
<select name="suggest_keys">
<option value="both" <?= $sk === 'both' ? 'selected' : '' ?>>两者都可以(默认,推荐)</option>
<option value="arrows" <?= $sk === 'arrows' ? 'selected' : '' ?>>只用 ↑ / ↓ 方向键</option>
<option value="punct" <?= $sk === 'punct' ? 'selected' : '' ?>>只用 , 和 .(输入法手感)</option>
</select>
</label>
<p class="hint span2">
决定用哪组键在联想下拉里<b>选词</b>:选中某条后回车才按该词搜索;不选就直接回车 = 用你输入的原文。<br>
<b>推荐「两者都可以」</b>——手不用离开主键区,习惯中文输入法的话按 , . 更顺手,同时也保留方向键。
⚠️ 只要联想下拉<b>打开着</b>,逗号与句号就会当作选词键、不再直接输入 ——
想搜「python, pandas」这类带逗号的词,先按 <b>Esc</b> 关掉下拉再输。<br>
若只留一组键,<b>另一组会完全没反应</b>(不是坏了,是这里的选择),所以除非你明确只想用一组,建议保持默认。
</p>
<label>合集显示方式
<?php $cl = setting('collections_layout', 'tabs'); ?>
<select name="collections_layout">
<option value="tabs" <?= $cl === 'flat' ? '' : 'selected' ?>>标签页折叠(合集多时用)</option>
<option value="flat" <?= $cl === 'flat' ? 'selected' : '' ?>>全部平铺(每组各占一块)</option>
</select>
</label>
<p class="hint span2">
只影响底部<b>「合集」</b>那一区,中间那些普通网格板块(常用 / 工具…)不受影响。
<b>每个合集里条目不多</b>时选「全部平铺」,一眼看全、不用点;
<b>合集很多、条目很杂</b>时选「标签页折叠」,默认只展开当前那一个。
某个板块是进「合集」还是做成普通网格板块,由「板块 → 展示方式」决定。
</p>
<label>主题色<input type="text" name="primary_color" value="<?= e(setting('primary_color', '#2F6BF7')) ?>"></label>
<label>搜索与链接的打开方式
<?php $ob = setting('open_blank', '1'); ?>
<select name="open_blank">
<option value="1" <?= $ob === '0' ? '' : 'selected' ?>>新标签页打开(推荐)</option>
<option value="0" <?= $ob === '0' ? 'selected' : '' ?>>当前页面打开</option>
</select>
</label>
<p class="hint span2">
同时管两处:<b>顶部搜索</b>(提交后到哪儿看结果)和<b>所有网站格 / 合集链接</b>。
「新标签页打开」(默认):点完当前导航页还在,方便连着开好几个;
「当前页面打开」:在同一个标签里跳走,适合把本站当浏览器起始页、不想堆一堆标签的场景。
<b>不影响</b>后台管理界面里的链接。
</p>
<label class="chk"><input type="checkbox" name="icon_services" <?= setting('icon_services', '1') === '1' ? 'checked' : '' ?>> 联网搜索高清图标(第三方聚合服务)</label>
<p class="hint span2">勾上后,当站点自身没有提供高清图标时,会依次尝试 icon.horse、Yandex 图标服务;关掉则只用站点自己声明的图标。</p>
<div class="span2 form-actions"><button class="btn primary">保存设置</button></div>
</form>
</section>
<section class="card">
<div class="card-head"><h2>站点图标</h2></div>
<?php
$iconOn = site_icon_mode() === 'custom';
$iconPrev = site_icon_url('logo', 512);
$iconDirW = site_icon_dir_writable();
?>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" enctype="multipart/form-data" class="grid-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="site_icon_save">
<div class="span2 icon-field">
<div class="icon-field-preview big">
<span class="icon-preview big">
<img src="<?= e($iconPrev !== '' ? $iconPrev : url('assets/img/logo.svg')) ?>" alt="">
</span>
</div>
<div class="icon-field-body">
<div class="icon-field-title">
当前:<?= $iconOn ? '自定义图标' : '内置默认图标' ?>
<span class="pill" data-site-icon="<?= e(site_icon_mode()) ?>">
<?= $iconOn ? '自定义' : '默认' ?>
</span>
</div>
<div class="row icon-row2">
<input type="file" name="site_icon_file" accept="image/png,image/jpeg,image/gif,image/webp">
<button class="btn primary" type="submit">上传并应用</button>
</div>
<p class="hint">
上传后,<b>浏览器标签页图标(favicon)</b>与<b>首页顶部的品牌图标</b>会一起换成它,
「添加到手机主屏」的图标也一并更新。<br>
建议用<b>正方形</b>图片(至少 32×32,推荐 512×512 以上);非正方形会<b>居中裁剪</b>成正方形。
支持 PNG / JPG / GIF / WebP,单张限 5MB。
</p>
<?php if ($iconOn): ?>
<p class="hint" data-site-icon-files="<?= $iconDirW ? 'static' : 'inline' ?>">
<?= $iconDirW
? '已在 uploads/site/ 生成一整套尺寸(16/32/48/180/192/512 + PWA 自适应版)。'
: '站点目录不可写,整套尺寸已内置在本站数据里,由 site-icon.php 按需输出。' ?>
</p>
<?php endif; ?>
</div>
</div>
</form>
<?php if ($iconOn): ?>
<form method="post" action="<?= e(url('admin/actions.php')) ?>">
<?= csrf_field() ?>
<input type="hidden" name="action" value="site_icon_reset">
<p class="hint">想换回内置图标:<button class="btn ghost" type="submit">恢复默认图标</button></p>
</form>
<?php endif; ?>
<p class="hint">
⚠️ 这与「网站列表里每个网站自己的图标」是<b>两回事</b>:这里是<b>本站自己</b>的图标(一个),
那里是导航格子里<b>每个目标网站</b>的图标(很多个)。改这里不会动到任何一个网站格子。
</p>
</section>
<section class="card">
<div class="card-head"><h2>修改管理员密码</h2></div>
<form method="post" action="<?= e(url('admin/actions.php')) ?>" class="grid-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="password_save">
<label>原密码<input type="password" name="old_password" autocomplete="current-password" required></label>
<label>新密码<input type="password" name="new_password" autocomplete="new-password" required></label>
<div class="span2 form-actions"><button class="btn primary">修改密码</button></div>
</form>
</section>
<?php
endif;
$body = ob_get_clean();
admin_layout($p, '后台', $body);