Files
SimpleNav/admin/index.php
T
laoyang 92eb9d90be 后台上传站点图标:首页品牌图 + favicon + PWA 图标一起换
需求:后台新增设置,允许管理员上传自己的图标;上传后首页品牌图标与浏览器
标签页 favicon(以及「添加到手机主屏」图标)一起换成它。

要点:
- 一张上传图服务端居中裁剪成正方形,生成 16/32/48/64/180/192/512 +
  maskable 192/512 整套尺寸,并手工拼装多帧 favicon.ico。
- 单一来源 site_icon_url() 统一取址;site_icon_head() 被首页/登录页/后台
  三处共用;PWA manifest 改由 manifest.php 动态输出、maskable 也跟随 ——
  三处防线共同杜绝「首页换了、标签页还是旧的」这类半生效。
- 目录可写则落静态文件(最快);不可写的保守环境整套压进设置、由
  site-icon.php 按需输出,功能不因权限限制整块不可用。
- 与「网站列表里每个网站自己的图标」是两个维度:目录与设置键都分开,
  清理时互不误伤。

顺手修掉两个真问题:
- login.php / admin/_auth.php 各留了一行写死的 favicon,与新的
  site_icon_head() 并存 → head 里两条 rel="icon",是「上传了却没换」的
  温床。已删掉写死那两行。
- 自定义 favicon.ico 原先只塞 16+32 两帧,而内置是 16/32/48/64 四帧 ——
  换图标反而退步。已补齐 icon-64 并统一为 4 帧。
- setting() 的请求内缓存导致「同请求写后再读」拿到旧值(真实坑):
  settings_cache() 支持强制重载 + settings_reload(),写入后立即失效。

验证:单测 44/44(含只读目录动态回退)、HTTP 56/56、e2e 174/0、
反向验证 5 变体全部被抓住(含「写死残留」与「ico 只塞两帧」两个新哨兵)、
真机几何 360/390/768/1440 无溢出、线上直连桌面+移动端几何全绿、
线上诊断页走通「上传→前台全换→复原」。
2026-09-28 22:28:23 +08:00

442 lines
25 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>';
}
function move_form(string $action, int $id, string $dir, string $label, string $p): 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) . '">'
. '<button class="ico-btn" title="' . e($label) . '">' . ($dir === 'up' ? '↑' : '↓') . '</button></form>';
}
ob_start();
if ($p === 'sites'):
$rows = $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();
$customCount = 0;
foreach ($rows 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($rows) ?>)</h2>
<span class="hint">其中 <?= $customCount ?> 个使用自定义图标(不会自动更新)</span>
</div>
<div class="tbl-wrap"><table class="tbl">
<thead><tr><th>图标</th><th>简称(展示)</th><th>名称</th><th>网址</th><th>板块</th><th>显示</th><th>排序</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ($rows as $r): $label = $r['short_name'] !== '' ? $r['short_name'] : ($r['name'] !== '' ? $r['name'] : domain_of($r['url'])); $src = icon_src($r['icon']); ?>
<tr>
<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; ?>
</td>
<td><?= 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><?= e($r['gname']) ?></td>
<td><?= toggle_form('site_toggle', (int)$r['id'], (int)$r['is_visible']) ?></td>
<td class="nowrap"><?= move_form('site_move', (int)$r['id'], 'up', '上移', '') . move_form('site_move', (int)$r['id'], 'down', '下移', '') ?></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="id" value="<?= (int)$r['id'] ?>">
<button class="ico-btn danger" title="删除">✕</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table></div>
</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', '个人网站导航')) ?>"></label>
<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', 'arrows'); ?>
<select name="suggest_keys">
<option value="arrows" <?= $sk === 'arrows' ? 'selected' : '' ?>>↑ / ↓ 方向键(默认)</option>
<option value="punct" <?= $sk === 'punct' ? 'selected' : '' ?>>, 和 .(输入法手感)</option>
<option value="both" <?= $sk === 'both' ? 'selected' : '' ?>>两者都可以</option>
</select>
</label>
<p class="hint span2">
决定用哪组键在联想下拉里<b>选词</b>:选中某条后回车才按该词搜索;不选就直接回车 = 用你输入的原文。
「, 和 .」跟输入法一样,手不用离开主键区,习惯中文输入法的话更顺手。
⚠️ 选它之后,<b>联想下拉打开时</b>逗号与句号会当作选词键,不再直接输入 ——
想搜「python, pandas」这类带逗号的词,先按 <b>Esc</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);