From 252812ed9384f797e5b10641ae46d777499e6fa5 Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 15 Oct 2026 02:57:19 +0800 Subject: [PATCH] =?UTF-8?q?v50=EF=BC=9A=E6=97=85=E7=A8=8B=E9=A2=98?= =?UTF-8?q?=E5=9B=BE=E8=A3=81=E5=89=AA=E4=BF=AE=E5=A5=BD=EF=BC=88AJAX=20?= =?UTF-8?q?=E7=89=87=E6=AE=B5=E6=BC=8F=E7=BB=91=EF=BC=89/=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E8=A1=A8=E5=8D=95=E6=8D=A2=E6=9C=80=E6=96=B0=E6=8E=A7?= =?UTF-8?q?=E4=BB=B6+=E5=8F=96=E6=B6=88=20/=20=E6=89=93=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E8=AF=AF=E6=8A=A5=E4=BF=AE=E5=A4=8D=20/=20=E5=90=8D=E5=86=8C?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=BC=96=E8=BE=91=E8=93=9D=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户 2026-10-14 四条。 ① 旅程详情点点「编辑」,里面的图片改不了、拖入/选中都不弹裁剪框 用户原话:「应该弹出来一个让我裁剪的那个框。但是现在却没有那个裁剪的框。」 **真根因**(assets/js/app.js):裁剪那段 IIFE 是**首屏一次性**的 —— `var inputs = querySelectorAll('[data-crop-ratio]'); if (!inputs.length) return;` 在脚本加载那一刻扫一遍,扫不到就整段放弃。而「编辑旅程」是 **AJAX 取片段、 innerHTML 塞进弹框**的(templates/trips/show.php 的 loadInto)⇒ 详情页首屏 **没有**题图框 ⇒ 当场 return ⇒ 片段里的框**从来没有 change 监听** ⇒ 拖入/选中什么都不发生。 修法:拆成 `bindOne(input)`(幂等,打 `data-crop-bound` 标记)+ `scan(scope)`; 首屏调一次 + **MutationObserver 补挂**;库的按需加载改成"首次真要用时才拉"。 ⚠️ 与「拖动上传」那段(bindInput/scan/MutationObserver)**同一手法** —— 它早就解决了"动态插进来的文件框也要接得住",裁剪这段当年漏了这层。 ② 旅程「修正」表单(templates/trips/_revise_form.php) 用户三条:「开始和结束时间…应该改成我们最新的控件样式」「上面记录实际执行情况 那一段话说明也不要了」「记录这次修正的按钮应该放在右边,另外应该加一个取消」。 · 起止换 `$dtPlugin = 'antd'`(与 node_form.php 同款;当年 v41 刻意没换,现按用户要求统一) · 删 show.php 里 `.rev-tip` 整段 · 按钮行 `rev-foot` 靠右 + 新增 `[data-rev-cancel]`;取消由 show.php 的**事件委托** 处理(`details.rev-open` 收起)—— 不用内联脚本(片段会被 AJAX 换进来) ③ 名册打标签误报「这个人不在你的名册里」 用户原话:「我搜索出来一个人…给他打标签,它会显示说这个人不在我的名册里, 其实他是在我的名册里的。」 **真根因**(index.php 的 people/tags):`isset(PersonRoster::visibleIds(...)[$pid])`, 而 `visibleIds()` 返回的是 **id 值列表** `[2,3,4,5]`(键是 0,1,2,3)—— 把值列表当 id→值 映射用。只有 `$pid` 恰好等于数组下标时才为真:名册里人少时 (下标 *:first-child { margin-left: auto; } /* 修正历史对比表 */ .rev-hist { margin-top: 8px; } @@ -4842,13 +4852,26 @@ button.btn, a.btn, summary.btn, .seg-btn, .icon-btn, .rv-act-btn, .cs-btn, .bf-b border-color: var(--line); color: var(--ink); } -/* 编辑 = 品牌色淡底(主要动作用它) */ -.person-edit { - background: var(--brand-soft, #FDF3EC); - border-color: color-mix(in srgb, var(--brand) 28%, transparent); - color: var(--brand-ink); +/* 🔴🔴 v50(用户 2026-10-14 第 4 条):「名册列表里面的按钮也是一样的, + 编辑是**蓝色**的背景,删除是**红色**的背景。」 + —— 与旅程详情页/v40 影院管理页**同一套口径**:编辑蓝、删除红,都是**实心**色块。 + ⚠️ 用**作用域类**(`.person-edit` / `.person-del`),不去改全局 `.btn-edit` + / `.btn-danger` 的语义 —— 本项目踩过"裸类改色波及别页"的坑 + (v41 把影院管理页的蓝色「编辑」改成了黄,见前面那段血泪注释)。 + ⚠️ 三处形态都写(裸类 / `a.` / `button.`):`.btn`、`a.btn`、`button.btn` + 的特异性不同,漏一种就有一种是错的(v40 已验过这条规律)。 + ⚠️ `.person-del` 原来只是 `.btn-danger`(**淡红描边**)—— 用户这次要的是**实心红**, + 所以在这里覆盖它;`.btn-danger` 本身的语义不动。 */ +.person-edit, a.person-edit, button.person-edit { + background: var(--blue); color: #fff; border-color: transparent; } -.person-edit:hover { background: color-mix(in srgb, var(--brand) 16%, #fff); } +.person-del, a.person-del, button.person-del { + background: var(--red); color: #fff; border-color: transparent; +} +/* hover:同色系加深(与文末「全站悬停统一」段同一手法)—— + ⚠️ 必须在这里补,因为那段是按类名白名单写的,`.person-edit/.person-del` 不在里面, + 不补的话 hover 会退回 `.btn` 的品牌色/或 `.person-edit` 上面那条淡底规则。 */ +.person-edit:hover, .person-del:hover { filter: brightness(.92); } /* 标签 = 中性灰底;有标签时稍深一点,一眼能看出"这个人是有标记的" */ .person-tag-btn { background: var(--line-2); color: var(--muted); } .person-tag-btn:hover { background: var(--line); color: var(--ink); } diff --git a/assets/js/app.js b/assets/js/app.js index eff9816..e61ac76 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -48,11 +48,31 @@ // 服务端(app/Upload.php)继续按归一化矩形裁,绕过前端也存不进比例不对的图。 // ⚠️ Cropper.js 是按需加载的:页面上真有 data-crop-ratio 的文件框才去拉这个库 // (与 Semi 那套同一思路:不能让没用到裁剪的页面白拉)。 + // + // 🔴🔴 v50(用户 2026-10-14 第 1 条)——**根因级修法:绑定可重复、可增量**。 + // 用户原话:「旅程的详情页面,当我点击编辑的时候,里面的图片没有办法改变。 + // 实际上当我把图片拖进去,或者选中一个图片的时候,应该弹出来一个让我 + // 裁剪的那个框。但是现在却没有那个裁剪的框。」 + // + // 真根因:这段以前是**一次性**的 —— + // `var inputs = querySelectorAll('[data-crop-ratio]'); if (!inputs.length) return;` + // 在**脚本加载那一刻**扫一遍,扫到就绑、扫不到(或后来才出现的)**永远不绑**。 + // 而旅程详情页的「编辑旅程」是 **AJAX 取片段、innerHTML 塞进弹框**的 + // (见 templates/trips/show.php 的 loadInto): + // 详情页首屏**没有**任何 `[data-crop-ratio]`(题图框在片段里)⇒ 这里当场 + // `return` ⇒ 弹框里的题图框**从来没有 change 监听** ⇒ + // 拖入/选中图片什么都不发生(`bindZone` 那条拖动逻辑会派发 change, + // 但**没人听**)⇒ 「图片改不了」。 + // + // ⇒ 拆成两部分: + // ① `bindOne(input)`:给**单个**文件框绑 change(幂等,带 `data-crop-bound` 标记); + // ② `scan(scope)`:扫一遍并绑没绑过的;首屏调一次 + MutationObserver 补挂。 + // ⚠️ 与「拖动上传」那段(bindInput/scan/MutationObserver)**同一手法** —— + // 它早就解决了同样的问题("动态插进来的文件框也要接得住"), + // 裁剪这段当年漏了这层。别再用"首屏一次性"的写法。 + // ⚠️ 库(Cropper.js)的按需加载时机也要跟着改:**首次真要用时才拉** + // (不能在"扫到 0 个框"时就 return 掉 —— 那样动态来的框再也不会拉库)。 (function () { - var inputs = Array.prototype.slice.call( - document.querySelectorAll('input[type="file"][data-crop-ratio]')); - if (!inputs.length) { return; } - var meta = document.querySelector('meta[name="fb-cropper"]'); if (!meta) { return; } // 页面没登记资源地址(不该发生,见 layout.php) var conf; @@ -284,19 +304,49 @@ }); } - for (var i = 0; i < inputs.length; i++) { - (function (input) { - input.addEventListener('change', function () { - var f = input.files && input.files[0]; - if (!f) { return; } - if (!/^image\//.test(f.type)) { - alert('请选择图片文件。'); - input.value = ''; - return; - } - open(input, f); + /** + * 给**一个**文件框绑裁剪(幂等:已绑过的打标记跳过)。 + * 只有带 `data-crop-ratio` 的框才需要裁剪 —— 其它 file input 不碰。 + */ + function bindOne(input) { + if (!input || input.tagName !== 'INPUT' || input.type !== 'file') { return; } + if (!input.hasAttribute('data-crop-ratio')) { return; } + if (input.getAttribute('data-crop-bound') === '1') { return; } // 已绑,跳过 + input.setAttribute('data-crop-bound', '1'); + input.addEventListener('change', function () { + var f = input.files && input.files[0]; + if (!f) { return; } + if (!/^image\//.test(f.type)) { + alert('请选择图片文件。'); + input.value = ''; + return; + } + open(input, f); + }); + } + + /** 扫一遍(可限定 scope)并绑上所有还没绑的 `[data-crop-ratio]` 框 */ + function scan(scope) { + Array.prototype.forEach.call( + (scope || document).querySelectorAll('input[type="file"][data-crop-ratio]'), + bindOne); + } + + scan(); // 首屏 + + /* 🔴 v50:**动态插进来的框也要接得住**(旅程「编辑旅程」是 AJAX 片段)。 + ⚠️ 与上面「拖动上传」那段用同一招:MutationObserver 补挂。 + 子树监听:片段是塞进某个容器里的,框在子树深处。 */ + if (window.MutationObserver) { + new MutationObserver(function (muts) { + muts.forEach(function (m) { + Array.prototype.forEach.call(m.addedNodes || [], function (n) { + if (!n.querySelectorAll) { return; } + if (n.tagName === 'INPUT' && n.type === 'file') { bindOne(n); } + scan(n); + }); }); - })(inputs[i]); + }).observe(document.body, { childList: true, subtree: true }); } })(); diff --git a/index.php b/index.php index aae1946..2ce82ac 100644 --- a/index.php +++ b/index.php @@ -1175,7 +1175,20 @@ switch ($r) { } // ⚠️ 只允许给**在我名册里的人**打标签 —— // 否则会给一份"看不见的名册条目"写上标签,用户永远看不到、也删不掉。 - if (!isset(PersonRoster::visibleIds($familyId, $me)[$pid])) { + // + // 🔴🔴 v50(用户 2026-10-14)——**这里原来是个真 bug**,用户原话: + // 「如果我把人给搜索出来的话,然后选择给他打标签,上面它会显示一个说 + // 这个人不在我的名册里,其实他是在我的名册里的。」 + // 原写法是 `isset(PersonRoster::visibleIds(...)[$pid])`,而 + // `visibleIds()` 返回的是 **id 值列表**(`[2,3,4,5]`,键是 0,1,2,3)—— + // 拿它当"id → 值"的映射去 isset,只有当 `$pid` 恰好等于数组下标时才是真: + // · 名册里人少 ⇒ 下标都小于 pid ⇒ **谁都判"不在名册"**(用户遇到的就是这个); + // · 一般情况 ⇒ 只有"第 pid+1 个人"碰巧对得上(时好时坏,最难查)。 + // ⇒ 改用 `in_array`(与 views/show 那处 `inMyRoster` 的写法**一致**)。 + // ⚠️ 别改成"给 visibleIds 换形状":另一个调用点已经是对的 in_array, + // 改形状会把那处静默弄坏(同类陷阱:PersonGroup::visibleIds 是**集合**, + // 同名不同形状,见 PersonRoster::visibleIds 的注释)。 + if (!in_array($pid, PersonRoster::visibleIds($familyId, $me), true)) { $msg = '这个人不在你的名册里。'; if ($isAjax) { header('Content-Type: application/json; charset=utf-8', true, 200); diff --git a/templates/trips/_revise_form.php b/templates/trips/_revise_form.php index 1ca1453..da6cb7c 100644 --- a/templates/trips/_revise_form.php +++ b/templates/trips/_revise_form.php @@ -13,6 +13,21 @@ * · 修正 = 记**实际**发生了什么(计划不动,实际值进历史,供对比)。 * 两者都作用在同一条 trip_nodes 记录上(用户明确"是一条记录,不是两条")。 * + * 🔴 v50(用户 2026-10-14)三条: + * ① 「修正一个计划行程的时候的开始和结束时间,仍然是之前的那种**控件样式**, + * 应该改成我们**最新的控件**样式。」 + * ⇒ 换 `$dtPlugin = 'antd'`(与「添加/编辑行程」的 node_form.php 同款)。 + * 当年(v41)这里刻意**没换**,理由是"修正表单内联在 `
` 里、 + * 一段行程一份、可能十几份并存,antd 每份都要挂 React 组件,不划算"。 + * 用户现在明确要统一,且实测详情页通常只有一两段计划行程需要修正 —— + * 按用户要求换,代价可接受。 + * ② 「上面记录实际执行情况那一段话说明也不要了。」 + * ⇒ 删掉 `.rev-tip` 那段(那段文字在 show.php 的 `.rev-panel` 里,见那边的改动)。 + * ③ 「记录这次修正的按钮应该放在**右边**,另外应该加一个**关闭/取消**的按钮, + * 就是如果不再修正的话,应该有一个取消,不要让这个框显示了。」 + * ⇒ 按钮行右对齐;补一颗「取消」——`.rev-open` 是 `
`, + * 取消 = 把 `open` 去掉(见下面内联脚本 + show.php 的委托)。 + * * 变量(require 前设好): * $rvNode 被修正的这段旅程 * $rvTripId 活动 id @@ -28,21 +43,17 @@ $rvN = (int)($rvNode['revision_n'] ?? 0);
- +
- ` 里,一段计划行程一个实例、可能同时存在 - 十几个;antd 每个实例都要挂一批 React 组件与 popper 容器, - 这种"N 份并存"的场景明显不划算。 - 它用的是自建下拉(`_datetime.php`),**本来就能精确到分**,功能不缺。 */ ?> 默认是计划的时间,改成实际发生的即可。
@@ -72,13 +83,16 @@ $rvN = (int)($rvNode['revision_n'] ?? 0);
-
+ +
+ 0): ?> + 之前已记过 次;原计划数据都留着。 + + - 0): ?> - 之前已记过 次;原计划数据都留着。 -
\ No newline at end of file diff --git a/templates/trips/show.php b/templates/trips/show.php index 0762233..5877156 100644 --- a/templates/trips/show.php +++ b/templates/trips/show.php @@ -417,11 +417,17 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
修正
-

- 记录实际执行情况。这仍与上面那条计划是同一条记录, - 原来的计划数据会保留在「修正记录」里,方便日后对比。 - 点完这里这段行程仍是计划;确认无误后再点「完成」。 -

+ `(原生可折叠,不用 JS 也能开)—— + ⇒ 取消 = 把这个 details 的 `open` 去掉,回到收起状态。 + ⚠️ 用委托(`closest`)而不是在 `_revise_form.php` 里内联脚本: + 那份片段会被 AJAX 换进来,内联脚本的位置与幂等都要额外照看;委托一次绑定就够。 */ + var rc = t.closest('[data-rev-cancel]'); + if (rc) { + e.preventDefault(); + var det = rc.closest('details.rev-open'); + if (det) { det.open = false; } + return; + } var h = host(); if (h && t === h) { setOpen(false); return; } // 点遮罩关;点面板内部不关 });