feat(nav): 导航站点支持自定义图标上传(含两处连带修复) #16
需求:网页版导航,新增/编辑导航地址时没有自定义图标功能。
## 功能
- 站点表单加 enctype=multipart/form-data,新增「自定义图标」控件:
40px 预览 + 选择图片 + 清除自定义图标(仅编辑且有自定义图时出现)
- 选择文件后本地即时预览(URL.createObjectURL,用完 revoke,不落 base64)
- 按文件内容判类型(finfo + <svg 探测),不信任扩展名;上限 1MB
- icon_custom 三态语义:不选文件=不动 icon 列;选文件=icon_custom=1;
勾清除=icon='' & icon_custom=0。普通改名不碰 icon 列,不会丢图
- saveUploaded() 改随机文件名。原实现按 md5(icon_key(url)) 命名,
单用户系统没事、多用户会互相覆盖
- 新增 Nav::iconFileInUse():图标文件按内容共享,删旧文件前先查还有没有
别的记录在用
- 图标从 uploads/ 目录取不到(线上 403),走 /nav_icon.php?f= 公开出口
## 连带修复 1:ICON_STORE 常量从未定义
SimpleNav 搬迁时漏搬 define('ICON_STORE', 128),NavFetch.php 第 667/739
行在用。PHP 8 抛 Error: Undefined constant "ICON_STORE",只要走到 GD
位图分支就 500。因为 saveUploaded() 一直是死代码(有定义无调用点),
这个错一直没被触发过。现补进 app/bootstrap.php(带 if (!defined) 保护)。
## 连带修复 2:22 处裸函数调用缺 self::
搬迁时把自由函数改成了 NavFetch 的静态方法,但类内部调用点没跟着改。
PHP 报 Error: Call to undefined function App\http_get() @ NavFetch.php:171。
这既是「全部更新」按钮 500 的根因,也是「图标一直抓不到」的真相 ——
整条抓取链路(http_get / attr_value / absolutize / third_party_icon_urls /
default_icon_candidates / meta_content / parse_icon_links / manifest_icons /
is_third_party_icon / looks_generated_tile / grab_icon / ensure_utf8)
一处都没跑起来过。
修后 CLI 实测 NavFetch::meta('https://www.baidu.com') 返回
name=[百度一下,你就知道]、icons=3、图标落盘 3818 字节。
## 验证
- php -l 5 个文件全过
- 端到端 21/0:真实上传 PNG → 列表出 img.nav-ico → nav_icon.php 返 200
+ PNG 头 → 编辑换图 → 清除 → 只改简称不丢图标 → 清理
- 语义验收:带自定义图的站点点「全部更新」后 icon 文件名不变、custom=1;
不传图的站点 custom=0(以服务端回报条数为证据,不看"未变")
This commit is contained in:
@@ -7601,14 +7601,78 @@ $want = array_keys($want);
|
||||
|
||||
case 'site_save':
|
||||
$url = Nav::normUrl((string)($_POST['url'] ?? ''));
|
||||
$id = Nav::saveSite($me, [
|
||||
|
||||
/* 🔴 工单 #16:自定义图标上传(用户 2026-10-28「增加导航地址的时候,
|
||||
怎么没有自定义图标功能啊,修改也没有」)。
|
||||
|
||||
放在 saveSite **之前**:先把文件落盘拿到文件名,再连同其它字段一起存,
|
||||
这样"新增站点 + 上传图标"一次提交就完成,不用先建再改。
|
||||
⚠️ 上传失败(类型不对 / 太大 / 不是图)时**不能**静默继续保存 ——
|
||||
否则用户以为图标传上去了,列表里却还是老样子。
|
||||
但也不能整条拒绝:他填的网址/名字是有效的。
|
||||
⇒ 策略:图标失败就报错退出,让他重传(比"保存了但图标没了"好懂)。
|
||||
⚠️ 只在**真的选了文件**时才处理:`UPLOAD_ERR_NO_FILE` 表示这次没打算换图标,
|
||||
必须当成"不改",不能当成"清空"(那就是"改个名字图标就没了")。 */
|
||||
$iconFile = '';
|
||||
$iconClear = !empty($_POST['icon_clear']);
|
||||
$up = $_FILES['icon'] ?? null;
|
||||
if (is_array($up) && (int)($up['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
|
||||
if ((int)$up['error'] !== UPLOAD_ERR_OK) {
|
||||
$bad('图标上传失败(服务器限制 ' . ini_get('upload_max_filesize')
|
||||
. '),换一张小一点的图试试。');
|
||||
}
|
||||
$tmp = (string)($up['tmp_name'] ?? '');
|
||||
if ($tmp === '' || !is_uploaded_file($tmp)) {
|
||||
$bad('图标没传上来,请重试。');
|
||||
}
|
||||
// 体积上限 1MB:图标不需要更大(128×128 输出,几 KB 就够)
|
||||
if ((int)($up['size'] ?? 0) > 1024 * 1024) {
|
||||
$bad('图标太大了(上限 1MB),换一张小一点的。');
|
||||
}
|
||||
$bin = (string)@file_get_contents($tmp);
|
||||
if ($bin === '') {
|
||||
$bad('图标读不出来,请重试。');
|
||||
}
|
||||
// 🔴 按**内容**判类型,绝不信扩展名(与 media.php / nav_icon.php 同一口径)
|
||||
$mime = '';
|
||||
if (function_exists('finfo_open')) {
|
||||
$fi = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = (string)finfo_buffer($fi, $bin);
|
||||
finfo_close($fi);
|
||||
}
|
||||
$head = ltrim(substr($bin, 0, 1024));
|
||||
$isSvg = stripos($mime, 'svg') !== false || stripos($head, '<svg') !== false;
|
||||
if (!$isSvg && strpos($mime, 'image/') !== 0) {
|
||||
$bad('这不是图片文件(识别为 ' . ($mime !== '' ? $mime : '未知') . ')。');
|
||||
}
|
||||
$iconFile = NavFetch::saveUploaded($bin, $url);
|
||||
if ($iconFile === '') {
|
||||
$bad('图标存不下来(格式不支持,或 SVG 里带了脚本)。');
|
||||
}
|
||||
}
|
||||
|
||||
$siteData = [
|
||||
'group_id' => (int)($_POST['group_id'] ?? 0),
|
||||
'url' => $url,
|
||||
'name' => (string)($_POST['name'] ?? ''),
|
||||
'short_name' => (string)($_POST['short_name'] ?? ''),
|
||||
'is_visible' => !empty($_POST['is_visible']),
|
||||
], $gid);
|
||||
];
|
||||
/* 三态:传了新图 → 设;勾了清除 → 清;都没做 → 一个字不碰。
|
||||
⚠️ 只有真给了键才传,`saveSite` 靠 `array_key_exists` 判断"要不要动 icon",
|
||||
随手传一个空串会变成"清空"(那正是工单 #8 那类"改个名字图标没了")。 */
|
||||
if ($iconFile !== '') {
|
||||
$siteData['icon'] = $iconFile;
|
||||
} elseif ($iconClear) {
|
||||
$siteData['icon_clear'] = true;
|
||||
}
|
||||
|
||||
$id = Nav::saveSite($me, $siteData, $gid);
|
||||
if ($id <= 0) {
|
||||
// 站点没存成 → 刚落盘的图标成了孤儿,删掉(连同它一起白费)
|
||||
if ($iconFile !== '') {
|
||||
Nav::deleteIconFile($iconFile);
|
||||
}
|
||||
$bad('站点地址和所属板块都不能空。');
|
||||
}
|
||||
/* 🔴 v24 第 26 批 用户第 4 条:**复用别人加过的站点**。
|
||||
|
||||
Reference in New Issue
Block a user