v38 第 38 批:观影试点 Ant Design 日期控件 + 弹框内联排版(对齐)+ 去提示 + 默认带自己

用户原话(四条):
  ①「我终于找到了一个我比较认可的日期选择插件
     https://ant-design.antgroup.com/components/date-picker-cn 现在观影这个模块进行
     测试吧,测试那个有开始、结束时间的。等这个模块测试完没问题了,再更新到其他的模块。」
  ②「记录一场电影,这个弹框因为标题和下面的输入框都是两行的,所以导致弹框非常长。
     能不能排排版,让前面的标题后面加个冒号,后面的输入框直接跟在它后面,这样就变成一行。
     包括开始、结束时间、影院、同行的人、简介等等,能把它排成一行的都排成一行,
     但是一定要注意对齐的问题,如果不对齐的话可能不好看。」
  ③「里面的一些提示,比如片名可以从豆瓣自动补充,还有刚新建的影院会自动选中,
     观影照片可以放这里且可多选,这些都把它去掉吧,节省一下上下的空间。」
  ④「另外一个同行的人,这里每个人添加的时候,应该默认带上自己,也就是自己不用选择
     就应该已经选上了。」

① Ant Design RangePicker —— **只在观影试点**(用户明确"试完再推广")
  · 资源 `assets/vendor/antd/{antd.min.js, dayjs.min.js, reset.css}`(unpkg 下载、随包上线);
    layout.php 加 `fb-antd` 清单;`_datetime.php` 加 `$dtPlugin = 'antd'` 开关 +
    `data-dt-antd="1"` 标记;app.js 加 `bootAntd()` / `mountAntd()`(按需加载)。
  · 🔴 UMD 的硬依赖:antd 从**全局**取 React / ReactDOM / dayjs。缺 React 的报错是
    `Cannot read properties of undefined (reading 'createContext')` —— 表现是"控件完全
    不出现",不是样式问题。项目本来就有一套 React 18.3.1(Semi 用的),直接复用同一份。
  · 🔴 实测出的关键行为(独立探针 `_antd_probe`,15/0):面板里有 **42 个日期格 +
    2 个内联时间列(84 项)+ 「确定」按钮** —— 正是之前 Semi 缺的那两样;
    且 **`onChange` 只在两端都齐了才触发**,单端改动走 `onCalendarChange` ⇒
    回写必须两个都挂,否则"只填了开始时间就提交"会丢值。
  · ⚠️ 原生那对下拉**全部保留**(值载体 + 拉不动时的兜底),挂上 antd 后用 clip 视觉收起
    (不用 display:none —— 保持可聚焦)。
  · 部署断言钉住试点边界:日程/任务**不该**出现 `$dtPlugin = 'antd'`。

② 弹框排版:标签内联 + 控件左缘对齐
  · `.mv-row` = 标签(固定 62px、右对齐、**冒号由 CSS ::after 给**):控件(占满剩余)。
  · 并排对:简介+备注、海报+观影照片 → `.mv-pair`(grid 1fr 1fr 保证两列等宽);窄屏回单列。
  · 🔴 真机量出**两个真对齐缺陷**(不是猜的):
    1) 时间行的控件左缘比别的行**偏 8px** —— `.dt-cols` 是 `gap: 10px 18px`(列距 18),
       而普通 `.mv-row` 只有 `gap:10px` ⇒ 改 `.mv-row.dt-range.dt-cols { column-gap: 10px }`;
    2) 海报/观影照片两行歪了 **130px** —— `_stage_drop.php` 在最前面插的 `.sr-only`
       `<input>`(绝对定位 1px)**占掉了一整份 flex:1 1 auto**,把上传框推出去
       ⇒ 这两块改纵向 `.mv-media`(标签一行、内容从 72px 起,与主列对齐)。
    ⇒ 现在**所有字段控件左缘都是 444**;A/B 实测**弹框 1036px → 817px(矮 219px)**。

③ 删掉三句冗余提示(豆瓣补充 / 新建影院自动选中 / 观影照片),简介 rows 6→3。

④ 同行的人默认带上自己:新增时预置 `[我]`;编辑以库值为准;`old()` 存在时尊重用户上次勾选。

🔴 顺带修的真 bug:**antd 面板与弹框抢 Esc** —— 两个处理器都挂 document 上,
  按一次 Esc 把日期面板和整个弹框一起关掉。已改成"antd 面板开着时弹框不吃 Esc"。

验证(全部真机 + 反向验证):
  · _fb_b38_movie_form.py **33/0**(antd 挂上/面板有时间列与确定/值写回隐藏 *_date/
    标签同行与左缘对齐/三句提示消失/默认带自己/试点边界);
  · _fb_b38_narrow.py **6/0**(宽屏并排、窄屏回单列、无横向溢出);
  · _fb_b38_falsify.py **8/0**(排版退回上下两行、antd 只挂 onChange、不预置自己 → 各自必须红);
  · e2e **1503/0**、日期控件真机 **53/0** + 反向 **7/0**、v37 两套 **27/0 + 5/0**、
    观影照片 **16/0** + **8/0**、b30 三件套、b18 选人 **10/0**、几何交互 **109/0**、
    DAV 三套、部署静态断言空跑 —— 全绿;
  · 线上 app.js / app.css / antd.min.js 的 SHA256 与本地一致。

⚠️ 顺带修了探针/夹具的三处**测试侧**问题(都不是页面 bug):
  · e2e 里 6 条 jsdom 用例(测选人弹框"暂存区机制")前提是"一个都没选",
    默认带自己后全假红 ⇒ 在**测试自己的 HTML 副本**上清掉 `data-pp-selected`;
  · `_fb_b18_picker` 真机探针同理,改成**点 chip 上的 ×**(组件把已选存在闭包里,
    改属性没用 —— 试过两种写法都是白试);
  · `_fb_b34_dtrange` 的"标签文案"断言写死了「开始 / 结束」,
    而 v38 把观影的标签简化成了「时间」⇒ 改成"有非空标签即可";
    另补一条 antd 分支(挂 antd 后原生被裁成 1px,`1px > 0` 会让旧判据**假通过**)。
  · 顺带:夹具名册只有 1 人时会误报"无法验证多选",已把实际候选数打出来。
This commit is contained in:
2026-10-10 11:01:22 +08:00
parent d83c94e127
commit 8661ab6475
9 changed files with 1003 additions and 36 deletions
+74 -32
View File
@@ -28,7 +28,22 @@ $dtEnd = $dtFromOld('ends_at', $m['ends_at'] ?? null);
// 优先级:老输入 > ?cinema=(刚新建的)> 记录里的值。
$selCinema = (int)old('cinema_id', (int)($_GET['cinema'] ?? 0) ?: ($m['cinema_id'] ?? 0));
$curAttendees = array_map('intval', array_column($m['attendees'] ?? [], 'id'));
$selAttendees = array_map('intval', (array)old('attendees', $curAttendees));
/* 🔴 v38(用户 2026-10-10):「同行的人,这里每个人添加的时候,应该默认带上自己,
也就是自己不用选择就应该已经选上了。」
⇒ **新增**时把"我"预置进已选列表;**编辑**时以库里的值为准
(用户可能就是自己改了,不能每次打开都把"我"塞回去)。
⚠️ `old()` 存在(= 刚才这次提交校验失败重画)时,尊重用户上一次的勾选 ——
否则他取消掉"自己"、填错别处再提交,会发现"自己"又回来了。 */
$meId = (int)\App\Auth::personId();
$hasOld = array_key_exists('attendees', (array)($_SESSION['_old'] ?? []));
if ($hasOld) {
$selAttendees = array_map('intval', (array)old('attendees', []));
} elseif ($isEdit) {
$selAttendees = $curAttendees;
} else {
// 新增:默认带上自己(名册里查得到才加 —— 免得给一个不存在的 id)
$selAttendees = $meId > 0 ? [$meId] : [];
}
?>
<?php /* 🔴 v37:本模板现在有两种用法 ——
① **整页**(`movies/edit` 的编辑页;以及 `movies/new` 的直接访问 / 校验失败重画);
@@ -87,9 +102,19 @@ $mvOpen = $mvModal && !$mvModalOnly;
<?php if ($mvModal): ?><div class="cal-modal-body"><?php endif; ?>
<div class="<?= $mvModal ? 'mv-form-inner' : 'card card-pad' ?>">
<div class="form-grid">
<div class="field full">
<label for="title">片名 *</label>
<?php /* 🔴🔴 v38(用户 2026-10-10):**标签内联 + 固定宽度对齐**。
用户原话:「记录一场电影,这个弹框因为标题和下面的输入框都是两行的,
所以导致弹框非常长。能不能排排版,让前面的标题后面加个冒号,后面的输入框
直接跟在它后面,这样就变成一行。包括开始、结束时间、影院、同行的人、简介等等,
能把它排成一行的都排成一行,但是一定要注意对齐的问题,如果不对齐的话可能不好看。」
⇒ 换成 `.mv-grid`:每行 "标签(固定 62px,右对齐) : 控件(占满剩余)"。
固定宽度是"对齐"的关键 —— 标签长短不一(片名/时间/影院/同行的人)时,
控件左缘仍然在**同一条竖线**上。
⚠️ 不用 `.form-grid`(它是 2 列 grid,本来就是"标题在上、控件在下"):
那一套是给普通页面表单用的,别为了弹框把它改掉。 */ ?>
<div class="mv-grid">
<div class="mv-row">
<label class="mv-lb" for="title">片名<i>*</i></label>
<div class="db-title-row">
<input class="input" id="title" name="title" value="<?= e(old('title', $m['title'] ?? '')) ?>"
required autocomplete="off">
@@ -105,21 +130,24 @@ $mvOpen = $mvModal && !$mvModalOnly;
<img id="dbPosterImg" alt="海报预览">
<span class="tip">海报已从豆瓣取回,保存后生效。</span>
</div>
<span class="tip">片名可从豆瓣自动补充:输入后从下拉里选,或点「自动获取」。</span>
<?php if (!empty($errors['title'])): ?><span class="err"><?= e($errors['title']) ?></span><?php endif; ?>
</div>
<?php /* 用户 2026-10-04 第 7 条:起止是一个**范围**。用户 2026-10-09 第 2 次:
「前面是日期,后面是时和分,只记录到时和分就行了。时和分,是下拉的。」
⇒ 形态 = 日期框 + 时/分两个下拉(**没有秒**,见 app.js upgradeRange 的说明)。 */ ?>
<div class="field full dt-range">
<span class="dt-range-label">开始 / 结束时间(时:分)</span>
<div class="mv-row dt-range">
<span class="mv-lb dt-range-label">时间</span>
<?php /* v37:秒去掉、"散场时间可不填。"那句也删了(用户要求省空间)。
v38:`$dtPlugin = 'antd'` ⇒ app.js 把这对端点升级成
**Ant Design 的 RangePicker**(用户 2026-10-10 点名要的插件,
先在观影模块试点)。原生下拉**照旧在**,拉不动时就是它们。 */ ?>
<?php $dtName = 'starts_at'; $dtValue = $dtStart; $dtId = 'starts_at_date';
$dtRange = 'start';
$dtRange = 'start'; $dtPlugin = 'antd';
require __DIR__ . '/../_datetime.php'; ?>
<?php if (!empty($errors['starts_at'])): ?><span class="err"><?= e($errors['starts_at']) ?></span><?php endif; ?>
<?php $dtName = 'ends_at'; $dtValue = $dtEnd; $dtId = '';
$dtRange = 'end';
$dtRange = 'end'; $dtPlugin = 'antd';
require __DIR__ . '/../_datetime.php'; ?>
<?php if (!empty($errors['ends_at'])): ?><span class="err"><?= e($errors['ends_at']) ?></span><?php endif; ?>
</div>
@@ -133,8 +161,8 @@ $mvOpen = $mvModal && !$mvModalOnly;
→ 弹框里放"地方/名称/颜色"三个字段(影院管理页那张表单的核心),
提交时带 back=movies/new,服务端存完回到本页并自动选中新影院。
③ 下拉/弹框的样式与全站统一(宣纸底色、品牌色描边)。 */ ?>
<div class="field full">
<label for="cinema_id">影院</label>
<div class="mv-row">
<label class="mv-lb" for="cinema_id">影院</label>
<div class="cinema-select-row">
<div class="cs" id="cinemaPicker" data-open="0">
<button type="button" class="cs-btn" id="csBtn" aria-haspopup="listbox" aria-expanded="false">
@@ -160,11 +188,10 @@ $mvOpen = $mvModal && !$mvModalOnly;
</div>
<button type="button" class="btn btn-ghost btn-sm" id="cinemaNewBtn">新建影院</button>
</div>
<span class="tip">刚新建的影院会自动选中;下拉里的色点就是它在日历与列表里的颜色。</span>
</div>
<div class="field full">
<label>同行的人</label>
<div class="mv-row">
<span class="mv-lb">同行的人</span>
<?php /* v24 第 11 条:换成全局人员选择器(templates/_person_picker.php)。
原来这套是观影页私有的 .att-*,它的「过滤」其实一直是坏的 ——
.att-item 自己写了 display:flex,压过了浏览器的 [hidden]{display:none},
@@ -185,15 +212,20 @@ $mvOpen = $mvModal && !$mvModalOnly;
<?php endif; ?>
</div>
<div class="field full">
<label for="info">简介</label>
<textarea class="input" id="info" name="info" rows="6"
placeholder="剧情简介;点「自动获取」会把导演/主演/年份/类型等主要信息一起写进来"><?= e(old('info', $m['info'] ?? '')) ?></textarea>
</div>
<?php /* 🔴 v38 用户:「能把它排成一行的都排成一行」——
简介与备注都是多行文本、又都不长,并排成一对最省高度
(各自仍带自己的内联标签,左右两列**各自内部对齐**)。 */ ?>
<div class="mv-pair">
<div class="mv-row">
<label class="mv-lb" for="info">简介</label>
<textarea class="input" id="info" name="info" rows="3"
placeholder="剧情简介"><?= e(old('info', $m['info'] ?? '')) ?></textarea>
</div>
<div class="field full">
<label for="note">备注</label>
<textarea class="input" id="note" name="note" rows="2"><?= e(old('note', $m['note'] ?? '')) ?></textarea>
<div class="mv-row">
<label class="mv-lb" for="note">备注</label>
<textarea class="input" id="note" name="note" rows="3"><?= e(old('note', $m['note'] ?? '')) ?></textarea>
</div>
</div>
<?php
@@ -217,7 +249,16 @@ $mHasTicket = $isEdit && !empty($m['ticket_key']);
$mPhotos = $isEdit ? (array)($m['photos'] ?? []) : [];
$mDelForms = []; // 收集起来,在表单**外面**统一渲染
?>
<div class="form-grid" style="margin-top:4px">
<?php /* 🔴 v38 排版:海报 / 观影照片这两块不用 `.mv-row`。
原因(真机量出来的):它们的控件区里有**多个**东西 ——
· `_stage_drop.php` 会吐一个 `.sr-only` 的 `<input>`(绝对定位、1px)
排在 `.upfield` **前面**;
· 于是"谁才是控件"就说不清了 —— 按 `.mv-row` 的 flex 排,那个 1px 的
隐框会占掉一份 `flex:1 1 auto`,把后面的上传框**推到 130px 之外**
(实测左缘 443 vs 隔壁 573,一眼就是歪的)。
⇒ 这两块改成纵向:**标签一行、内容另起一行**,内容从**标签列右侧**开始
(`margin-left` 对齐那 72px),这样既不会歪,也不会被无关元素挤走。 */ ?>
<div class="mv-grid mv-pair mv-grid-media" style="margin-top:10px">
<?php /* ---- 观影照片(v35:不再分类,合并成一块)----
用户原话:「除了海报之外,票根、现场照片等等都不分类别,都只分一个类,
放在一块。…只有海报是单独的…其他的都放在一个模块就行,
@@ -228,8 +269,9 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
⚠️ **旧的 ticket_key 仍然要显示**(历史数据不能丢):编辑页把已有票根
列进下面这块的网格里(它没有 `event_photos` 行 id,所以只能"看"、
不能用照片的删除按钮删;要换就直接上传新的)。 */ ?>
<div class="field">
<label for="sd-poster">海报</label>
<div class="mv-media">
<span class="mv-lb">海报</span>
<div class="mv-media-body">
<?php if ($mHasPoster): ?>
<?php $mDelForms['poster'] = 'movies/media_del'; ?>
<div class="mv-slot">
@@ -249,13 +291,14 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
$sdMultiple = false;
$sdMsg = $mHasPoster ? '点击或拖动进行上传(换一张海报)' : '点击或拖动进行上传';
require __DIR__ . '/../_stage_drop.php'; ?>
<span class="tip"><?= $mHasPoster ? '选/拖一张新的会自动替换旧的那张。' : '还没有海报。' ?></span>
</div><?php /* /.mv-media-body */ ?>
</div>
</div>
<?php /* ---- 观影照片:票根 + 合影 + 其它相关图片,**全部放这一块** ---- */ ?>
<div class="field full">
<label for="sd-photo">观影照片</label>
<div class="mv-media">
<span class="mv-lb">观影照片</span>
<div class="mv-media-body">
<?php if ($mHasTicket || $mPhotos): ?>
<div class="photo-grid">
<?php if ($mHasTicket): /* 历史数据:旧票根(不在 event_photos 里,只展示) */ ?>
@@ -289,10 +332,9 @@ $mDelForms = []; // 收集起来,在表单**外面**统一渲染
<?php $sdName = 'photo[]'; $sdSlot = 'photo'; $sdAccept = 'image/*';
$sdMsg = '点击或拖动进行上传';
require __DIR__ . '/../_stage_drop.php'; ?>
<span class="tip"><?= $isEdit ? '观影照片都可以放这里(可多选);删掉的在保存后生效。'
: '观影照片都可以放这里(可多选);之后也能在详情页里逐张补传。' ?></span>
</div><?php /* /.field.full(观影照片) */ ?>
</div><?php /* /.form-grid */ ?>
</div><?php /* /.mv-media-body */ ?>
</div><?php /* /.mv-media */ ?>
</div><?php /* /.mv-grid */ ?>
</div><?php /* /.mv-form-inner */ ?>
<?php /* 🔴🔴 下面这一段**严格按分支配对**,别再把 </div> 与 <?php if ?> 混着写 ——
v37 前两版就是在这里把 `.mv-form-inner` / `.cal-modal-body` 关错了层数,