From c242bfb12c3ad088916e030b4a3b400a4b575233 Mon Sep 17 00:00:00 2001 From: mole404 <248403041+mole404@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:59:40 +0800 Subject: [PATCH] =?UTF-8?q?fix(tags):=20=E4=BB=A3=E7=A0=81=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=E5=8F=91=E7=8E=B0=E7=9A=84=206=20=E6=9D=A1=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E5=85=A8=E4=BF=AE=20+=20=E8=A1=A5=E7=9C=9F=E6=9C=BA/?= =?UTF-8?q?=E9=9D=99=E6=80=81=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查(含新加的打标部分)发现并修掉: 1. 改名/删除标签会清空选中 ✗ —— refreshAfterTagChange() 走 setTags() → resetImages(), 会清空图片墙并 ds.clearSelection(),把用户正在打标的这批图连同弹窗勾选态一起丢掉 (老师问过的「编辑/删除会整页刷新」)。改为 patchCardsTag() 就地同步(卡片角标 + data-json + 详情卡 chip);只有「被删的标签正用作筛选项」这种结果集真的变了的情况才调 setTags()。 2. 删图片/删用户不清 image_tag / tags ✗ —— 两个迁移的注释都写着「关联行由应用代码显式删除」, 图片这条路径没落实,残留行会无声累积。UserService::deleteImages() 补 detach(); 后台删用户时一并清标签与关联行。(本机 SQLite 未启用 PRAGMA foreign_keys,不能指望级联。) 附带 tools/check-tag-integrity.php:只读自检残留行,退出码非 0 即为异常。 3. 标签名进 data-json(单引号属性)未转义 ✗ —— 名字里带 ' 就能提前闭合属性(自伤型 XSS 面)。 三处注入(列表渲染 / 弹窗行 / syncCardTags)统一先 escapeHtml 再写。 4. 弹窗里几个 axios 调用没有 catch ✗ —— 网络错误或 422(名字超 64 字)会静默无反应。 统一走 apiErrMsg() 取后端 message;三个标签名输入框补 maxlength=64。 5. 后端 trim() 在校验之后 ✗ —— 只输入空格会建出空名标签。TagRequest 加 prepareForValidation()。 6. 两处硬编码路径 ✗(action="/user/tags/__id__"、axios.delete('/user/tags/'+id))→ 改用 route()。 测试: - jsdom 118/118(新增 7 条:改名的局部同步、data-json 转义、maxlength+catch、 deleteImages/删用户的清理、TagRequest trim;测试台补 renderTagRoutes 渲染带 id 的路由)。 - 真机 70/70(新增 #2c:改标签名后选中不丢 / 图片墙不重载 / 角标就地换名, 6 项断言,在整轮末尾单开 context 跑,避免本机 1vCPU/965MB 下的页面 OOM)。 - 真机实证:图片挂 1 条关联 → deleteImages → 残留 0;check-tag-integrity.php 干净 ✓。 - tools/diff-vs-upstream.sh:已知偏离清单补登 UserService.php / Admin\UserController.php, 校验通过(103 项,恰好等于实际偏差)。 - patches/ios-longpress.patch 按原范围(3 个文件)重新生成。 --- Dockerfile | 4 +- patches/ios-longpress.patch | 456 +++++++++++++----- .../Http/Controllers/Admin/UserController.php | 4 + src/app/Http/Requests/TagRequest.php | 14 + src/app/Services/UserService.php | 4 + src/resources/views/user/images.blade.php | 95 +++- test/tag-feature.test.mjs | 49 +- tools/check-tag-integrity.php | 39 ++ tools/diff-vs-upstream.sh | 7 + 9 files changed, 516 insertions(+), 156 deletions(-) create mode 100644 tools/check-tag-integrity.php diff --git a/Dockerfile b/Dockerfile index ae96cd1..ff47455 100644 --- a/Dockerfile +++ b/Dockerfile @@ -165,7 +165,7 @@ RUN php -r "file_exists('.env') || copy('.env.example', '.env');" \ RUN printf '%s\n' \ 'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./public/js/context-js/context-js.js' \ 'c0e513ec8e93fd81b34b3c6de5cf5eb8 ./resources/js/context-js.js' \ - '1292ea8123868897023def9dc9bcf5ac ./resources/views/user/images.blade.php' \ + '23835058801c2b609c2134f0f10d0a5d ./resources/views/user/images.blade.php' \ | md5sum -c - \ && grep -q "assetVersion('js/context-js/context-js.js')" ./resources/views/user/images.blade.php \ && grep -q 'isIOSWebKit' ./public/js/context-js/context-js.js \ @@ -208,7 +208,7 @@ RUN APP_SRC_MD5=$(find app config routes -type f -print0 2>/dev/null | sort -z | "fork_sha=${FORK_SHA}" \ "lsky_commit=${LSKY_COMMIT}" \ "context_js_md5=c0e513ec8e93fd81b34b3c6de5cf5eb8" \ - "images_blade_md5=1292ea8123868897023def9dc9bcf5ac" \ + "images_blade_md5=23835058801c2b609c2134f0f10d0a5d" \ "app_src_md5=${APP_SRC_MD5}" \ "app_js_md5=${APP_JS_MD5}" \ "app_css_md5=${APP_CSS_MD5}" \ diff --git a/patches/ios-longpress.patch b/patches/ios-longpress.patch index aee2ea0..e06da16 100644 --- a/patches/ios-longpress.patch +++ b/patches/ios-longpress.patch @@ -52,7 +52,7 @@ # 三个文件各自的 md5(与 Dockerfile「自证 2」里的期望值一致): # c0e513ec8e93fd81b34b3c6de5cf5eb8 src/resources/js/context-js.js # c0e513ec8e93fd81b34b3c6de5cf5eb8 src/public/js/context-js/context-js.js -# 1292ea8123868897023def9dc9bcf5ac src/resources/views/user/images.blade.php +# 23835058801c2b609c2134f0f10d0a5d src/resources/views/user/images.blade.php # # 上游原文件 md5(改动前,供比对): # c8e57f6232848ca8277341ddf1a3a7a6 resources/js/context-js.js @@ -62,8 +62,9 @@ # 2026-10-01 第二轮(老师试完测试版提的意见):两个标签窗口合并成一个(旧 tag-manage-modal 删除)、 # 三态勾选改成两态、加「显示图片标签」开关、修好卡片拖动框选(图片穿透 + 拖动起点放宽 + # 由卡片自己开预览)。 ---- a/resources/js/context-js.js -+++ b/resources/js/context-js.js +---- src/resources/js/context-js.js +--- /root/.hermes/cache/scratch/tmp.BQ92ALlEpf/up/resources/js/context-js.js 2026-10-02 14:31:26.703747972 +0800 ++++ src/resources/js/context-js.js 2026-10-01 15:40:13.406864745 +0800 @@ -2,9 +2,49 @@ * Context.js * Copyright Jacob Kelley, Modified by WispX @@ -1147,8 +1148,9 @@ + + return api; })(); ---- a/public/js/context-js/context-js.js -+++ b/public/js/context-js/context-js.js +---- src/public/js/context-js/context-js.js +--- /root/.hermes/cache/scratch/tmp.BQ92ALlEpf/up/public/js/context-js/context-js.js 2026-10-02 14:31:26.695747977 +0800 ++++ src/public/js/context-js/context-js.js 2026-10-02 12:22:32.584186106 +0800 @@ -1 +1,1148 @@ -window.context=window.context||function(){let e={fadeSpeed:100,filter:function(e){},above:"auto",preventDoubleContext:!0,compress:!1};function t(o,n,d,i){let a=i?" dropdown-context-sub":"",s=e.compress?" compressed-context":"",f=$(''),l=0,r="";for(;l');else if(void 0!==n[l].header)f.append('");else{let e;void 0===n[l].href&&(n[l].href="javascript:void(0)"),void 0!==n[l].target&&(r=' target="'+n[l].target+'"'),e=void 0!==n[l].subMenu?$('"):$('
  • "+n[l].text+"
  • "),"function"==typeof n[l].visible&&(n[l].visible(o)||e.hide());let i=e.find("a");if(void 0!==n[l].classes)for(const e in n[l].classes)i.addClass(n[l].classes[e]);if(void 0!==n[l].attributes)for(const e in n[l].attributes)i.attr(e,n[l].attributes[e]);if(void 0!==n[l].action){let e="event-"+(new Date).getTime()*Math.floor(1e5*Math.random()),t=n[l].action;i.attr("id",e),$("#"+e).addClass("context-event"),$(document).on("click","#"+e,(function(){t.call(this,o)}))}if(f.append(e),void 0!==n[l].subMenu){let e=t(o,n[l].subMenu,d,!0);f.find("li:last").append(e)}}"function"==typeof e.filter&&e.filter(f.find("li:last"))}return f}return{init:function(t){e=$.extend({},e,t),$(document).on("click","html",(function(){$(".dropdown-context").fadeOut(e.fadeSpeed,(function(){$(".dropdown-context").css({display:""}).find(".drop-left").removeClass("drop-left")}))})),e.preventDoubleContext&&$(document).on("contextmenu",".dropdown-context",(function(e){e.preventDefault()})),$(document).on("mouseenter",".dropdown-submenu",(function(){let e=$(this).find(".dropdown-context-sub:first");e.width()+e.offset().left>window.innerWidth&&e.addClass("drop-left")}))},settings:function(t){e=$.extend({},e,t)},attach:function(o,n){let d=(n=n||{}).data||{},i=(new Date).getTime();$(document).on("contextmenu",o,(function(a){a.preventDefault(),a.stopPropagation();let s=a.target.closest(o);"function"==typeof n.beforeOpen&&n.beforeOpen.call(a,s);let f=t(s,d,i);$("body .dropdown-menu.dropdown-context").remove(),$("body").append(f),$(".dropdown-context:not(.dropdown-context-sub)").hide();let l=$("#dropdown-"+i);if("boolean"==typeof e.above&&e.above)l.addClass("dropdown-context-up").css({top:a.pageY-20-$("#dropdown-"+i).height(),left:a.pageX-13}).fadeIn(e.fadeSpeed);else if("string"==typeof e.above&&"auto"===e.above){l.removeClass("dropdown-context-up");let t=l.height()+12;a.pageY+t>$("html").height()?l.addClass("dropdown-context-up").css({top:a.pageY-20-t,left:a.pageX-13}).fadeIn(e.fadeSpeed):l.css({top:a.pageY+10,left:a.pageX-13}).fadeIn(e.fadeSpeed)}"function"==typeof n.afterOpen&&n.afterOpen.call(a,s,l.get(0))}))},destroy:function(e){$(document).off("contextmenu",e).off("click",".context-event")}}}(); +/* @@ -2299,9 +2301,10 @@ + + return api; +})(); ---- a/resources/views/user/images.blade.php -+++ b/resources/views/user/images.blade.php -@@ -4,45 +4,88 @@ +---- src/resources/views/user/images.blade.php +--- /root/.hermes/cache/scratch/tmp.BQ92ALlEpf/up/resources/views/user/images.blade.php 2026-10-02 14:31:26.707747969 +0800 ++++ src/resources/views/user/images.blade.php 2026-10-02 14:28:58.367831314 +0800 +@@ -4,45 +4,79 @@ @@ -2330,15 +2333,6 @@ + } + } + -+ /* fork:卡片里的图片设成鼠标穿透。原因(实测):这套 DragSelect 只在 mousedown -+ 落在「已登记的可选元素本人」上时才起框选,而卡片 被里面的 -+ 铺满 —— 逐像素扫过整张卡片都没有一个点能按到 自己,所以框选永远起不来。 -+ 穿透后按下落在 上,框选恢复(predragstart → elementselect → dragstart); -+ 代价是「点图片开预览」收不到点击了,改由下面 $photos.on('click', '.images-item') 接管。 */ -+ .images-item img { -+ pointer-events: none; -+ } -+ + /* 「显示图片标签」开关关掉时,卡片角标整体不显示(角标是每次翻页重新渲染的, + 所以用容器上的一个类来控制,而不是去改每一张卡片的 DOM)。 */ + .image-tags-off .image-tags { @@ -2403,7 +2397,7 @@ -@@ -57,142 +100,352 @@ +@@ -57,151 +91,382 @@ @@ -2424,7 +2418,9 @@ + (否则会顶动 sticky 吸顶工具栏那一行)。--}} + + 标签 -+ ++ {{-- 实测:图标墨迹中心比文字高 0.5px(文字 90.0 / 图标 89.5,截图按像素量的), ++ 用 relative+top 精确压下去 0.5px。--}} ++ @@ -2440,11 +2436,16 @@ + + {{-- 卡片角标开关:控制图库里图片上的标签显不显示,状态记在本地(默认显示) --}} + ++ {{-- 之前按 canvas 字体度量把文字压低了 1.5px,实测过头(像素法:文字比行中心低 1.49px, ++ 反倒显得开关偏上)→ 撤回,恢复成自然行高。现在文字/开关都在行中心 ±0.2px 内。--}} + 显示图片标签 -+ ++ {{-- 自己画的开关:两态形状完全一样,只变颜色与滑块位置(不用 FontAwesome 的 ++ fa-toggle-on/off —— 那两个字形一粗一细,切起来画风不统一)。 --}} ++ + -+ {{-- 唯一的「标签管理」入口:新建 / 重命名 / 删除标签 + 给选中的图片打标,都在同一个窗口 --}} -+ 标签管理 + @@ -2739,7 +2740,7 @@ + 可以现场增删,也可以直接输入新名字回车新建(JS 渲染 + 绑定)。 --}} +
    +
    -+ ++ + +
    + @@ -2798,25 +2799,26 @@ +
    +

    标签管理

    +

    __hint__

    -+
    ++ {{-- 新建标签放在首行(老师要求):进窗口第一眼就能建新标签 --}} +
    -+ ++ + +
    ++
    +
    + + -+
    -+
    -+ -+ + + + + + {{-- 标签重命名的行内表单(在那一行下方就地展开,再点一次收起)。 + 名称不写进模板 —— 由 JS 用 .val() 填,避免标签名里的引号/尖括号破坏属性。 --}} + -@@ -201,7 +454,8 @@ ++ ++ ++ ++ ++ @push('styles') ++ ++ @endpush ++ + @push('scripts') @@ -2850,7 +2865,7 @@ +