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:
@@ -4982,6 +4982,31 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-size: 11px; border: 0;
|
||||
}
|
||||
/* 大号字母块:图标选择器里的预览位(与列表里的 20px 区分开) */
|
||||
.nav-ico-lg { width: 40px; height: 40px; font-size: 18px; border-radius: 8px; }
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
站点表单:自定义图标选择器(工单 #16)
|
||||
左侧预览、右侧文件选择 + 说明。预览用 40px 见方,
|
||||
和列表里的 20px 小图标区分开,让用户能看清传的是什么。
|
||||
------------------------------------------------------------ */
|
||||
.nav-icon-pick { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.nav-icon-prev {
|
||||
flex: 0 0 auto; width: 44px; height: 44px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: 1px solid var(--line-2); border-radius: 10px; background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
.nav-icon-prev img { width: 40px; height: 40px; object-fit: contain; display: block; }
|
||||
.nav-icon-ctl { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
|
||||
/* 原生 file 控件隐藏,用 <label for> 当按钮 —— 各浏览器原生按钮长相差异太大 */
|
||||
.nav-icon-file { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.nav-icon-btn { cursor: pointer; margin: 0; }
|
||||
/* 焦点可达性:file 输入被键盘聚焦时,让标签有个可见的焦点环 */
|
||||
.nav-icon-file:focus-visible + .nav-icon-btn {
|
||||
outline: 2px solid var(--brand); outline-offset: 2px;
|
||||
}
|
||||
#navIconFname { max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.nav-sname { flex: 0 0 auto; font-size: 13.5px; font-weight: 600; }
|
||||
.nav-surl { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.nav-acts { margin-left: auto; display: inline-flex; gap: 6px; flex-wrap: wrap; }
|
||||
|
||||
Reference in New Issue
Block a user