v25 第 25 批(9 条):名册分组不刷页 / 纪念日多人 / 首页等高 / 移出不回流 /
名册点击看详情拖动才排序 / 任务复选框与状态 / 财务标签多选 / 微信读书同步与统计 /
导航线上数据与图标
用户本轮 9 条需求(原话见 templates/ 与 app/ 内的逐条注释):
1. 名册:列表里给某人分组改为 AJAX 就地挪卡片(不再整页刷新)。
2. 纪念日:关联人改**多选**(复用 _person_picker,字段 person_ids[]);
自建纪念日的按钮与其它模块统一(改=蓝 .btn、删=黄 .btn-warn);
列表新增「关联人」列并给人名前加头像。
3. 首页:日程/任务两块**完全等高**(align-items:stretch + 固定总高 + flex 子项
min-height:0);修「0 件待办」——数字与右侧列表同源(原来各算各的)。
4. 🔴 名册里被移出的人过一阵又回来:根因是 PersonRoster::seedFromPersons()
**每次 Schema::ensure() 都无条件重灌**(线上实测每跑一次 +1 行)。
修:给它加自守卫(只有在 persons 还留着 group_id/sort_order 的**老库**才跑)。
5. 名册:点击=看详情、**只有拖 ⠿ 手柄**才是排序(原来整行既能点又能拖,语义混乱)。
6. 任务:所有任务前加复选框,任何位置点勾即完成;
状态语义照业界整理为 To Do / Doing / Done 并写出说明(用户要求参考别人的做法)。
7. 财务:类别标签改**多选非互斥**(再点一次取消该类别),上下两图 + 图例 + 表格
全部按同一个集合联动;高亮条刻意不给「合计」(多选下各类金额不能相加)。
8. 微信读书:新增**懒同步**(访问时顺手做,6h TTL,只补空字段不覆盖用户填的)、
封面图**拉下来存本站存储**(原来只存外链 → 看不到)、接官方 /readdata/detail 统计。
9. 导航:线上数据只搬了一半(5 板块 29 站 → 7 板块 67 站 + 67 图标)、
展示页头像不显示 / 搜索按钮错位 / 点登录 404。三个真根因:
· 相对地址在 /nav/ 下全 404 → 新增 Nav::abs();
· 搜索容器丢了 id="search"(CSS 是 #search button.submit 绝对定位);
· Nav::iconKey() 与 SimpleNav 的 icon_key() **算法不一致**(少一次 md5 归一化)
→ 搬来的图标文件名对不上;且 uploads/ 整目录 Require all denied。
新增两个**免登录公开出口** nav_icon.php / nav_avatar.php。
顺带修掉一个「数据损坏级」的真 bug(做第 4 条线上反证时发现的,之前一直没人注意):
app/Region.php 三处用中文标点当 trim/ltrim 的 charlist,而 PHP 的 charlist 是
**按字节**匹配的 —— 「。」= E3 80 **82** → 字节 82 进了可剥集合 →
「层」= E5 B1 **82** 被切掉尾字节 → 非法 UTF-8 → backfillAddress 的 UPDATE 报
`1366 Incorrect string value` → **地址拆分升级从上线起一直在失败被跳过**(每请求报一次)。
改成字符级 preg_replace('/…/u');线上实测 ensure() 现在无报错、待拆行数真的在下降。
验证战绩:
- e2e 1289/0(新增 Region 字节级用例等)
- 真机探针:选人 19/0 · 首页/导航 24/0 · 导航模块 34/0 · b18 暂存+确定 10/0 ·
第23批 52/0
- 桌面几何 428/0 · 移动端几何 680/0 · 交互 92/0 · 窄屏 24/0
- 老库升级 153/0(含 anniversary_people 迁移 + 「再跑 ensure() 名册行数不变」)
- 部署静态断言 180 OK / 0 FAIL · 反向验证 3/3 + Region 三变体各被两条断言同时抓住
- 线上 26 个文件与本地**逐字节一致**;展示页 200、图标 66/66 200、登录 200、
越权/不存在均 404、签发短语后 ensure() 不再报错。
This commit is contained in:
+33
-42
@@ -839,33 +839,14 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 这次按下该不该算"拖动"?
|
||||
* v24 第 25 批 用户第 5 条:拖拽的起手判据只剩一条 —— **必须按在 ⠿ 手柄上**。
|
||||
*
|
||||
* ⚠️ 按钮 / 输入框 / 下拉 / <summary> 一律不算 —— 在那儿按下是"要用它",
|
||||
* 不是"要拖这张卡"。唯一例外是 `.drag-handle`(它是专门用来抓的)。
|
||||
* ⚠️ 大链接 `.person-main` **算**:整行都能拖才顺手(这也是原来的手感),
|
||||
* 靠 MIN_MOVE 门槛区分"点进去看详情"和"拖着走",并且拖过之后要掐掉那次 click。
|
||||
* 原来的 `isControl()`(判断"这一点是不是按钮/输入框/大链接")**已删除**:
|
||||
* 那个函数存在的前提是"整行都能拖",而用户明确要求改成
|
||||
* 「点击列表中的人,是直接查看这个人的信息,拖动的时候才是调整数序」。
|
||||
* 整行不再参与拖动之后,"哪些控件不该触发拖拽"这个问题就不存在了 ——
|
||||
* 留着一个没人调用的函数只会让下一个人以为还有第二条拖拽路径。
|
||||
*/
|
||||
function isControl(node) {
|
||||
var t = node;
|
||||
while (t && t !== roster) {
|
||||
if (t.nodeType !== 1) { t = t.parentNode; continue; }
|
||||
// ⚠️ 判定顺序不能乱:先认"专门用来抓的"和"整行那个大链接",
|
||||
// 再认"点了就有别的作用的控件"。
|
||||
if (t.classList && t.classList.contains('drag-handle')) { return false; }
|
||||
// `.person-main` 虽然是 <a>(点它会进详情页),但整行都能拖才顺手 ——
|
||||
// 靠 MIN_MOVE 区分"点"与"拖",拖过之后掐掉那次 click。
|
||||
if (t.classList && t.classList.contains('person-main')) { return false; }
|
||||
if (t.getAttribute && t.getAttribute('data-no-drag') !== null) { return true; }
|
||||
var tag = t.tagName;
|
||||
if (tag === 'BUTTON' || tag === 'INPUT' || tag === 'SELECT' || tag === 'TEXTAREA'
|
||||
|| tag === 'SUMMARY' || tag === 'LABEL' || tag === 'A') {
|
||||
return true;
|
||||
}
|
||||
t = t.parentNode;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 每栏人数与"空栏"标记跟着 DOM 走 —— 不然拖完之后标题上的「N 人」还是旧的 */
|
||||
function refreshCounts() {
|
||||
@@ -995,6 +976,8 @@
|
||||
|
||||
function beginDrag(card, pt) {
|
||||
dragging = card;
|
||||
// ⚠️ v24 第 25 批:`is-dragging` 从 pointerdown 挪到了这里(见 pointerdown 的说明)。
|
||||
// 只有从 ⠿ 手柄起拖,所以"提前露空栏"不再必要 —— 真开始拖时再露。
|
||||
roster.classList.add('is-dragging');
|
||||
card.classList.add('is-dragged');
|
||||
placeAt(pt);
|
||||
@@ -1020,22 +1003,31 @@
|
||||
if (e.button !== 0 && e.pointerType === 'mouse') { return; }
|
||||
var card = cardFrom(e.target);
|
||||
if (!card) { return; }
|
||||
// 触摸时整行都能拖会让页面没法上下滚 —— 触摸只认那个 ⠿ 手柄
|
||||
/* 🔴🔴 v24 第 25 批 用户第 5 条:**只有从 ⠿ 手柄上按下才算拖动**。
|
||||
*
|
||||
* 用户原话:「现在名册的操作还是很不好,应该是点击列表中的人,是直接查看这个人的信息,
|
||||
* 拖动的时候才是调整数序,现在是混乱的。」
|
||||
*
|
||||
* 改之前的行为(三处叠加,所以"混乱"):
|
||||
* ① 鼠标在整行任何非控件处按下都算"待拖";
|
||||
* ② 而且**按下那一刻**就给 .roster 加 `is-dragging`(本来是为了提前露出空栏当落点);
|
||||
* ③ 于是轻轻点一下卡片,空栏先"冒出来"、页面高度跳一下,视觉上就是"我点了一下,
|
||||
* 页面自己动了一格";抬起时又收回去 —— 用户根本分不清自己是在点还是在拖。
|
||||
* ⇒ 现在:**整行都不再触发拖拽**;只有那枚 ⠿(.drag-handle,专门用来抓的)
|
||||
* 按下才进入待拖状态。点击整行 = 沿 .person-main 链接进详情页(原样保留)。
|
||||
*
|
||||
* ⚠️ 鼠标与触摸**同一判据**(不再按 pointerType 分叉):
|
||||
* 以前触摸只认手柄、鼠标认整行 —— 两种设备手感不同,用户换台机器就"又变了"。
|
||||
* 统一成"都只认手柄"之后,语义只有一条:**拖这个 ⠿**。
|
||||
* ⚠️ `is-dragging` 也挪到 beginDrag() 里加(不再在 pointerdown 加):
|
||||
* 既然只有手柄能起拖,就没有"提前露空栏"的必要了 ——
|
||||
* 手柄本身很小,用户按下它就是明确要拖,那一刻再露空栏不影响瞄准。 */
|
||||
var fromHandle = !!(e.target.closest && e.target.closest('.drag-handle'));
|
||||
if (e.pointerType !== 'mouse' && !fromHandle) { return; }
|
||||
if (!fromHandle && isControl(e.target)) { return; }
|
||||
if (!fromHandle) { return; }
|
||||
pending = { card: card, id: e.pointerId, type: e.pointerType };
|
||||
startPt = { x: e.clientX, y: e.clientY };
|
||||
lastPt = { x: e.clientX, y: e.clientY };
|
||||
/* 🔴 空栏必须在**按下的这一刻**就露出来,不能等"真的开始拖"才露。
|
||||
露出来会让页面变高、下面的分组整体下移 ——
|
||||
如果等到拖动开始才露,用户瞄准的那个位置在手指一动之后就跑了
|
||||
(症状:想拖到「未分类」,松手却落进了别的栏,或者干脆没动静)。
|
||||
按下时就露,布局在**瞄准之前**已经稳定,之后全程不变。
|
||||
⚠️ 只点一下不拖的情况:松手时会收回去(见下面的 pointerup);
|
||||
而"点一下"多半是点这一行看详情,页面本来就要跳走,看不到这一下。 */
|
||||
roster.classList.add('is-dragging');
|
||||
if (fromHandle) { e.preventDefault(); } // 别顺手选中文字
|
||||
e.preventDefault(); // 别顺手选中文字
|
||||
});
|
||||
|
||||
document.addEventListener('pointermove', function (e) {
|
||||
@@ -1054,16 +1046,15 @@
|
||||
document.addEventListener('pointerup', function (e) {
|
||||
if (dragging) { endDrag(); }
|
||||
else if (pending && e.pointerId === pending.id) {
|
||||
// 在手柄上按下、但位移没到门槛(= 没真拖)→ 清掉待拖态,什么都不做。
|
||||
// ⚠️ v24 第 25 批:这里**不再**需要 `is-dragging` 的收回 ——
|
||||
// 那个类现在只在 beginDrag() 里加(见 pointerdown 的说明)。
|
||||
pending = null;
|
||||
roster.classList.remove('is-dragging'); // 只是点了一下:把露出来的空栏收回去
|
||||
}
|
||||
});
|
||||
document.addEventListener('pointercancel', function () {
|
||||
if (dragging) { endDrag(); }
|
||||
else if (pending) {
|
||||
pending = null;
|
||||
roster.classList.remove('is-dragging');
|
||||
}
|
||||
else if (pending) { pending = null; }
|
||||
});
|
||||
|
||||
// 拖完掐掉那一次 click(捕获阶段就拦,别等它冒泡到链接上)
|
||||
|
||||
Reference in New Issue
Block a user