v24 第 19 批:拖动上传 / 返还按钮 / TripLog 补迁 / 参与人回填 / 预览翻页 / 行程按钮(用户 8 条)
用户 8 条原话要点:
①「为所有的图片视频附件上传处,提供拖动上传的功能」
②「旅程详情页面,增加返回按钮,点击后返回到旅程列表」
③「已有行程的出行方式,包括航班号,怎么没从 triplog 系统迁移过来?」
④「题图右侧有一句话显示,但点编辑进去却看不到这句话」
⑤「编辑行程,参与的人没有带进来,还是空的。同样的问题也出在观影编辑时」
⑥「预览的时候左右有箭头可以直接看下一张;md5 的文件名放在预览图下面没有意义,去掉;
如果是文件,则在图标上显示文件名」
⑦「计划行程的修改/完成/未成行横着排列放行程右上角,修改黄、完成绿、未成行红;
实际行程的编辑删除也放右上角,编辑黄、删除红」
⑧「选人弹框暂存区太小了看不见人;前面的复选框没用,去掉」
🔴 抓到的两个真根因(都不是"没写对",是两处看似无关的地方):
1. **⑤ 参与人不回填 = 两个独立原因叠加**
(a) `Trip::node()`(单条)只 `SELECT *`,**不带 members/attachs**;而
`Trip::nodes()`(列表)是带的。编辑页走前者 → `$ppSelected=[]` 永远空。
同一处还让编辑页的照片墙永远不显示。→ 单条也挂 members/attachs/花销。
(b) `person_picker.js` 的 `init()` 末尾**无条件** `refresh()` → `syncChips()`
第一句 `chips.innerHTML=''` 再从**懒建**的候选清单重建 —— 首屏清单还没建,
一个 input 都没有 → **服务端渲染的 chips 全被清掉、补不回来**。
症状极隐蔽:curl 看到的 HTML 里有 3 个 chip,浏览器里数出来是 0。
→ `syncChips` 加 `if (!built) return;` 守卫 + `ppSetSelected` 前先 `box.ppBuild()`。
2. **④ 那句话 = 详情页读了两个字段**:`display:summary` + `events.note`(老字段,
表单里早没有输入框)→ 老数据的 note 在详情页冒出来、却无从修改。
→ 详情页只渲 summary;老数据给一句"点这里补简介"的提示;
由 `_fb_b19_backfill.php` 一次性搬进 summary。
本批新增能力:
- **拖动上传**(第 1 条):app.js 自动扫全页 `input[type=file]` 挂 DnD,**不改任何模板**
→ 全站十几处上传点一次到位;`data-pick-input` 指向的**预览图**也当落点
(题图/头像的 input 是 sr-only,往图上拖才自然)。三个必须踩准的点:
`dragover` 必须 preventDefault;`dt.items.add` 真写回 `input.files`;
写回后**手动派发 `change`**(`input.files=` 不会自己触发,预览/裁剪器都听 change)。
MutationObserver 补挂动态出现的框。
- **预览左右翻页**(第 6 条):候选 = 同页全部 `[data-prev]`(文档顺序),
`‹ ›` 按钮 + 键盘 ←→(**只在图片上**接管方向键,视频/PDF 里方向键另有含义);
只有一个附件时箭头隐藏(点了没反应比没有更糟);右上角带"第几张/共几张"。
- **附件墙**(第 6 条):缩略图下**不再写**(哈希)文件名;文件的名字压到**图标里**;
`att-doc-name` 两行封顶 + 省略号(长名不撑变形)。
- **行程按钮**(第 7 条):`.node-ops` 由 `column` 改 `row` + 贴顶;新增
`.btn-warn/.btn-ok/.btn-del`(**实心**黄/绿/红)—— 与全站淡红描边的 `.btn-danger`
刻意分开(改它会连带改掉十来处「删除」的样子)。「编辑」保留(改标题/加花销的唯一入口)。
- **选人弹框**(第 8 条):暂存区 148px 死高度 → `min-height:132px; max-height:46vh`,
chip 头像 20→28px;多选加 `.pp-compact` 隐藏复选框(**只 `display:none`,
input 仍在 DOM** —— 提交语义全靠它,绝不能 remove)。
上线补迁(`_tooling/_fb_b19_backfill.php`,幂等、不覆盖已有值):
- ③ TripLog 的 `travel_modes.mode_type` + `trip_number/line_name/carrier`
→ FamilyBook 的专列。`drive_own/drive_rental` 都映射成 `drive`(FamilyBook 的
"自驾"是一个方式,"哪台车"由 vehicle_id 表达);车辆按**车牌**对上;
顺手删掉 detail 里那段重复的「班次:…」。线上实跑 18 条、跳过 2 条。
- ④ `events.note` → `trip_details.summary`(只搬 summary 为空、note 有内容的,2 条)。
验证(本地全套 + 线上):
- e2e **960/0**(+23 条本批断言,含"成员清空后编辑页也不显示"的反向)· 升级 149/0 ·
城市解析 32/32 · 桌面几何 428/0 · 真机 UI **27/0**(拖动/翻页/按钮几何与配色)·
真机选人 chip **2/0**。
- **反向验证 4/4**:①`Trip::node` 去掉 members ②`syncChips` 去掉守卫
③详情页把 note 渲回来 ④附件墙把文件名写回去 —— 四种坏法全部被抓到,
且还原后 e2e 复绿 960/0。
- 线上:部署成功(双版本 l 通过 + 19 条站点自检全过);补迁后 18 段行程有出行方式、
7 段有车次、10 段挂车、2 个旅程有简介;线上 `Trip::node()` 实调
members/attachs 都回来了(部署前是 0)。
This commit is contained in:
@@ -328,6 +328,141 @@
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 拖动上传(v24 第 19 批 用户第 1 条)。
|
||||
*
|
||||
* 用户原话:「为所有的图片视频附件上传处,提供拖动上传的功能,就是允许用户,
|
||||
* 通过把文件拖动到那里进行上传。」
|
||||
*
|
||||
* 做法:**给页面上每一个 `<input type="file">` 自动挂上**拖动接收 —— 不改模板,
|
||||
* 于是全站的附件上传点(旅程照片、观影海报/票根/现场照片、资源、任务、日程附件、
|
||||
* 图书封面与文件、名册头像与通讯录导入…)一次性都有了拖动能力。
|
||||
* 新出现的文件框也接得住:MutationObserver 补挂(时间控件那套用过同一招)。
|
||||
*
|
||||
* 🔴 两个必须踩准的点:
|
||||
* ① **必须真的把 drop 的文件写进 input.files**,不能只"显示个名字"。
|
||||
* 写回去要用 DataTransfer:`dt.items.add(file)` 再 `input.files = dt.files`。
|
||||
* 不这么做的话,用户以为拖上去了,一提交却发现什么都没上传。
|
||||
* ② 写回之后要**手动派发一次 change** —— 页面上挂的预览 / 裁剪器都监听 change,
|
||||
* 不派发的话拖进来的图不会弹裁剪框、也不会更新预览("拖了没反应")。
|
||||
* 注意:`input.files = …` 本身**不会**触发 change(不是用户交互),必须手动补。
|
||||
*
|
||||
* ⚠️ 只接拖动落点,不改 accept 语义:accept 是给选择框用的提示,浏览器**不**拿它
|
||||
* 过滤 drop(拖什么都进得来)。服务端本来就会再判一次 MIME,这里不重复造规则。
|
||||
* ⚠️ dragover 里必须 preventDefault,否则浏览器不认这里是"可放下"的(默认行为是拒收)。
|
||||
* ⚠️ 用计数器算 dragenter/dragleave:拖过子元素时也会触发 leave,
|
||||
* 只按"进了/出了"切换高亮会让高亮疯狂闪。
|
||||
*/
|
||||
(function () {
|
||||
/**
|
||||
* 给一个"落点元素"挂拖动接收。
|
||||
*
|
||||
* @param {Element} zone 用户实际往上拖东西的那个元素(可见的那块)
|
||||
* @param {Element} input 目标 <input type="file">
|
||||
*
|
||||
* ⚠️ 为什么落点与 input 要分开:题图/头像那类上传,`<input>` 是 **sr-only**
|
||||
* (看不见),用户能看见、能点的是旁边那张预览图。只把拖动挂在 input 上,
|
||||
* 用户往图上拖是**没有任何反应**的 —— 而"拖到那块图上"才是最自然的动作。
|
||||
* 所以:有 `data-pick-input` 指向这个 input 的图,就把那图也当落点。
|
||||
*/
|
||||
function bindZone(zone, input) {
|
||||
if (!zone || !input || zone.getAttribute('data-drop-bound') === '1') { return; }
|
||||
zone.setAttribute('data-drop-bound', '1');
|
||||
zone.className += (zone.className ? ' ' : '') + 'dropzone';
|
||||
|
||||
var depth = 0; // dragenter/dragleave 计数器(见上面的说明)
|
||||
|
||||
function over(state) {
|
||||
if (state) { zone.classList.add('is-over'); } else { zone.classList.remove('is-over'); }
|
||||
}
|
||||
|
||||
zone.addEventListener('dragenter', function (e) {
|
||||
if (!hasFiles(e)) { return; }
|
||||
e.preventDefault();
|
||||
depth++;
|
||||
over(true);
|
||||
});
|
||||
zone.addEventListener('dragover', function (e) {
|
||||
if (!hasFiles(e)) { return; }
|
||||
// 🔴 少了这句浏览器就不允许放下(默认是拒收),后头的 drop 根本不会来
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) { e.dataTransfer.dropEffect = 'copy'; }
|
||||
over(true);
|
||||
});
|
||||
zone.addEventListener('dragleave', function (e) {
|
||||
if (!hasFiles(e)) { return; }
|
||||
depth--;
|
||||
if (depth <= 0) { depth = 0; over(false); }
|
||||
});
|
||||
zone.addEventListener('drop', function (e) {
|
||||
if (!hasFiles(e)) { return; }
|
||||
e.preventDefault();
|
||||
depth = 0;
|
||||
over(false);
|
||||
var files = Array.prototype.slice.call(e.dataTransfer.files || []);
|
||||
if (!files.length) { return; }
|
||||
// 目标是普通文件框时,**单选的只取第一个**(多选也塞多个会与 accept 语义打架:
|
||||
// 题图/头像那种单选框塞两个,浏览器提交的仍是第一个,但用户会以为两个都传了)
|
||||
var multi = input.hasAttribute('multiple');
|
||||
var put = multi ? files : files.slice(0, 1);
|
||||
try {
|
||||
var dt = new DataTransfer();
|
||||
put.forEach(function (f) { dt.items.add(f); });
|
||||
input.files = dt.files;
|
||||
} catch (err) {
|
||||
// 老浏览器不支持给 input.files 赋值 → 至少把光标引到框上,不让用户白拖
|
||||
if (window.console) { console.warn('拖动上传:这个浏览器不支持把文件写进控件', err); }
|
||||
input.focus();
|
||||
return;
|
||||
}
|
||||
// 🔴 手动派发 change:预览与裁剪器都听它(input.files 赋值不会自己触发)
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
/** 这次拖动里有没有真文件(拖选中的文字/链接也会触发 drag 事件,那些不该管) */
|
||||
function hasFiles(e) {
|
||||
var t = e.dataTransfer;
|
||||
if (!t) { return false; }
|
||||
if (t.types) {
|
||||
for (var i = 0; i < t.types.length; i++) {
|
||||
if (t.types[i] === 'Files') { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 一个 input:既给它自己挂落点,也给"指向它的那张预览图"挂(见 bindZone) */
|
||||
function bindInput(input) {
|
||||
bindZone(input, input);
|
||||
var sel = input.id ? ('[data-pick-input="#' + input.id + '"]') : '';
|
||||
if (sel) {
|
||||
Array.prototype.forEach.call(document.querySelectorAll(sel), function (pv) {
|
||||
bindZone(pv, input);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function scan(scope) {
|
||||
Array.prototype.forEach.call(
|
||||
(scope || document).querySelectorAll('input[type="file"]'), bindInput);
|
||||
}
|
||||
|
||||
scan();
|
||||
// 动态加进来的文件框(工作经历那种"再来一段"、日历弹框里的附件…)也要接得住
|
||||
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') { bindInput(n); }
|
||||
scan(n);
|
||||
});
|
||||
});
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
})();
|
||||
|
||||
// 头像选好后即时预览(仅用于**没走裁剪器**的文件框,避免和裁剪预览打架)
|
||||
var fileInput = document.getElementById('avatar');
|
||||
if (fileInput && !fileInput.hasAttribute('data-crop-ratio') && window.FileReader) {
|
||||
|
||||
Reference in New Issue
Block a user