Files
FamilyBook/templates/_attachments.php
T
laoyang 1756c6265d 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)。
2026-10-05 22:56:49 +08:00

119 lines
6.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 附件缩略图墙(v24 用户第 3 条;第 19 批 第 6 条改)。
*
* 一处渲染、全站通用:旅程节点 / 资源 / 任务 / 日程的附件都长一样。
*
* 规则(用户原话):
* · 图片、视频 → **缩略图**,点开在**本页**预览(不再新开标签页)
* · PDF → 浏览器里直接预览(自带阅读器)
* · Word / Excel / PPT / 其它 → 一个**和缩略图一样大**的图标,点了直接下载
* (浏览器渲染不了的东西,绝不装一个"点了没反应"的预览)
* · 预览框**右上角**恒有一个下载按钮
*
* ────────────────────────────────────────────────────────────────
* v24 第 19 批 用户第 6 条(本次改的三点):
* 「图片和视频,文件名都是 md5 的,放在图片预览图下面没有任何意义,去掉吧。」
* 「如果是文件,则在图标上显示文件名。」
*
* ① 图片 / 视频**下面不再写文件名与大小** —— 那串是用户手机上传时的原始名,
* 而照片/视频是**看**的,一串 `9d3fcc70fe46161275f850b93470e0bb.mp4`
* 既认不出内容、也不比缩略图本身多任何信息。
* (图与视频仍带 title 悬停提示,「下载」按钮也还在 —— 功能没少,只是不占版面。)
* ② 非图片的**文件**(PDF/Word/Excel/PPT/文本):文件名挪到**图标里面**,
* 压着色块显示。文件是**读名字**的,「学位学历证书_博士学位证书.pdf」
* 这行字本身就是最重要的信息,放在图标下方的小灰字里反而看不清。
* ③ 删除按钮与下载入口保留在缩略图下方(.att-meta 仍在,只是不再放文件名/大小)。
*
* 用法(先准备变量再 require):
* $attRows 附件行(Attachment::list 的结果)
* $attCanEdit 是否显示删除按钮
* $attDelUrl 删除动作的路由名(如 'resources/attach_del')
* $attHostId 删除时要回传的宿主 id(表单里的隐藏字段用 $attHostKey 指定键名)
* $attDelField 删除表单里「附件 id」的字段名(各路由不一致:有的叫 id、有的叫 attach_id)
* $attExtra 额外隐藏字段 [键 => 值](如日程要连带传宿主 id 才能判权限)
*
* ⚠️ 无 JS 也能用:可预览的是 <a href=预览地址>,不可预览的是 <a href=下载地址>,
* 脚本只是把"可预览"的那部分拦下来改成弹层。
*/
$attCanEdit = !empty($attCanEdit);
$attRows = $attRows ?? [];
$attDelField = $attDelField ?? 'id';
$attExtra = $attExtra ?? [];
$attDelUrl = $attDelUrl ?? '';
?>
<?php if ($attRows): ?>
<div class="att-wall">
<?php foreach ($attRows as $attRow): ?>
<?php
$attKind = \App\Attachment::kind($attRow);
$canPrev = \App\Attachment::previewable($attRow);
$attView = media_url((string)$attRow['key']);
$attDl = media_url((string)$attRow['key'], ['dl' => 1]);
$attNm = (string)($attRow['name'] ?? '');
$attSz = \App\Attachment::sizeWord($attRow['size'] ?? 0);
// ① 图片 / 视频:下面不放文件名(见文件头说明)
$attIsMedia = ($attKind === 'image' || $attKind === 'video');
// ② 文件:图标上显示文件名;没有名字时退回类别词(「PDF」「Word」…)
$attFileWord = $attNm !== '' ? $attNm : \App\Attachment::kindWord($attKind);
?>
<div class="att-cell">
<?php if ($canPrev): ?>
<a class="att-thumb att-kind-<?= e($attKind) ?>"
href="<?= e($attView) ?>"
data-prev="<?= e($attKind === 'pdf' ? 'pdf' : ($attKind === 'video' ? 'video' : 'image')) ?>"
data-src="<?= e($attView) ?>"
data-dl="<?= e($attDl) ?>"
data-name="<?= e($attNm !== '' ? $attNm : \App\Attachment::kindWord($attKind)) ?>"
data-size="<?= e($attSz) ?>"
title="<?= e($attNm) ?>">
<?php if ($attKind === 'image'): ?>
<img src="<?= e($attView) ?>" alt="<?= e($attNm) ?>" loading="lazy">
<?php elseif ($attKind === 'video'): ?>
<?php /* #t=0.1 让浏览器把第 0.1 秒那一帧当封面显示出来 ——
没有它视频位就是一块黑板,看不出这是段什么视频。 */ ?>
<video src="<?= e($attView) ?>#t=0.1" preload="metadata" muted playsinline></video>
<span class="att-play" aria-hidden="true">▶</span>
<?php else: ?>
<?php /* PDF:文件名压在图标上(用户第 6 条) */ ?>
<span class="att-doc att-doc-pdf">
<span class="att-doc-name"><?= e($attFileWord) ?></span>
</span>
<?php endif; ?>
</a>
<?php else: ?>
<a class="att-thumb att-kind-<?= e($attKind) ?>" href="<?= e($attDl) ?>"
title="<?= e($attNm) ?>(点击下载)">
<span class="att-doc att-doc-<?= e($attKind) ?>">
<span class="att-doc-name"><?= e($attFileWord) ?></span>
</span>
</a>
<?php endif; ?>
<div class="att-meta">
<?php /* 图片/视频不显示那串哈希名;文件的名字已经在图标上了,这里也不必再抄一遍 */ ?>
<?php if (!$attIsMedia): ?>
<span class="att-fsize"><?= e($attSz) ?></span>
<?php endif; ?>
<a class="link-quiet att-dl" href="<?= e($attDl) ?>">下载</a>
<?php if ($attCanEdit && !empty($attDelUrl)): ?>
<form method="post" action="<?= e(url($attDelUrl)) ?>" class="att-del">
<?= csrf_field() ?>
<input type="hidden" name="<?= e($attDelField) ?>" value="<?= (int)$attRow['id'] ?>">
<?php foreach ($attExtra as $ek => $ev): ?>
<input type="hidden" name="<?= e((string)$ek) ?>" value="<?= e((string)$ev) ?>">
<?php endforeach; ?>
<button class="btn btn-ghost btn-sm" type="submit"
onclick="return confirm('删掉这个附件?')">删除</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php /* ⚠️ 循环里用到的临时变量一律用完即清:宿主模板里很可能也有 $attRow / $attKind /
$attView / $attDl / $attNm / $attSz(各模块页面都爱用这种短名),留着就是静默覆盖。 */ ?>
<?php unset($attRow, $attKind, $canPrev, $attView, $attDl, $attNm, $attSz,
$attIsMedia, $attFileWord); ?>
</div>
<?php endif; ?>