v35 第 35 批:日期控件改用原生(Semi 时间面板不可用)+ 观影附件上传真修好 + 票根/合影归并成「观影照片」

用户原话(三段):
  ①「选择日期那个地方给,非常不好用。如果我不选日期,只选时间,框里面仍然是空白的,
     也没有确定按钮,这个需要重新调整一下。」
  ②「现在上传附件,也就是在观影模块里面上传附件,怎么还是有问题?上传的附件现在
     都没有保存,有时候我上传两个附件,它只保存一张。」
  ③「除了海报之外,票根、现场照片等等都不分类别,都只分一个类,放在一块。…
     只有海报是单独的,因为它是从豆瓣里面下载回来的。其他的都放在一个模块就行,
     然后所有照片都上传到一起,直接写"观影照片"就可以了。」

① 日期控件:**Semi 的时间面板根本不可用**(这条最费时间)
  实测(独立最小复现页 + 真机):Semi 的 dateTimeRange 面板**只给日期网格**,
  时间藏在 `.semi-datepicker-switch-time` 那个小时钟按钮后面;**点了它切到时间面板,
  面板仍然是空的**(timeLists=0、无选项、无 footer、无"确定")→ 根本无法"只选时间"。
  单点 dateTime 同样。⇒ 与用户描述完全一致。
  修:弃用 Semi 的时间选择,改用**原生** date + time(step=1)
  (24 小时制、精确到秒、点了就选、不选日期也有值、**不需要**确认按钮)。
  原来的 时/分/秒 三个下拉**仍留在 DOM**(POST 契约 *_date/_hour/_minute/_second
  一字不改),只是加 .dt-merged 视觉收起。
  app.js 的 upgradeRange()/upgrade() 都不再挂 Semi;CSS 加 .is-native-range 系列
  把 Semi 时代的"藏原生 / 两端让位"规则覆盖回来。

② 观影附件上传丢文件:**两条路径都断了**
  · 暂存路径(拖放):上传框走"拖进来即刻上传到暂存区",令牌进 stage_token[];
    而 movies/create|update 与 movies/photo_add **只读 $_FILES['photo']**
    ⇒ stage_token[] **从来没人认领** ⇒ 拖进去的一个都没存。
  · Movie::update() **完全不写 photos**(那句"photos 由单独的删除路由维护"的注释下
    根本没写),而路由明明算好了 $d['photos'] 传进来 ⇒ 编辑保存丢照片。
  修:路由新增 stage_token[] 认领循环(与 $_FILES 路径并存);photo_add 加兜底;
  Movie::update 补上 savePhotos。

③ 归并:表单去掉独立"票根"上传框,"现场合影"并入**一个**「观影照片」块;
  详情页「影像记录」→「观影照片」、去掉票根/照片分类标签。
  数据层本来就是平的(event_photos 只有 key/position);
  ⚠️ 旧数据 movie_details.ticket_key **仍要显示**(历史票根不能丢)+ 服务端仍接收
     $_FILES['ticket'](向后兼容,直接 POST 仍可存)。

验证(全部真机跑,不是"看代码觉得对"):
  · _fb_b34_dtrange.py 33/0(四个表单页:组可见 + 两端日期框可见 + 两端时间框
    可见且 step=1 + 三个原生下拉仍在 DOM + 只选时间也能写回 *_hour/_minute/_second);
  · _fb_b35_movie_media.py 14/0(点击上传 2 张→库里 2 行;真 HTTP 打 upload_stage.php
    拿 stoken→随表单提交→落库;编辑补传→3 行;详情页补传→4 行;归并断言);
  · 反向验证:_fb_b34_dtrange_falsify 4/0、_fb_b35_media_falsify 8/0;
  · e2e 1503/0(两条断言随 v35 语义更新)、几何交互 92/0、各批真机/反向全绿、
    部署脚本静态断言空跑通过;线上 app.js / form.php 的 SHA256 与本地一致。

顺带修掉的工具链脆弱(本轮暴露):
  · push.sh 开头加"确保本地测试库在跑"的守卫(长时间跑流水线时 mysqld 会被环境
    回收 → e2e/真机探针全崩,看着像代码坏了);
  · 探针别用固定字符窗口找函数体(注释一长就假红)、断言别写死字段名前缀
    (改期表单是 rev_ends_at_*)、set_input_files 的 id 要照 _stage_drop 的净化规则
    (photo[] → sd-photo)、提交按钮要精确定位(编辑页有多个 submit)。
This commit is contained in:
2026-10-09 20:23:09 +08:00
parent 9c70dbe0ff
commit 5c81f1991f
6 changed files with 287 additions and 175 deletions
+63 -6
View File
@@ -2749,22 +2749,58 @@ switch ($r) {
];
}
}
/* 🔴🔴 v35:**拖动/点击上传的照片走的是暂存区**(`stage_token[]`),
而这里原来**只读 `$_FILES['photo']`** —— 暂存令牌**从来没人认领**,
于是"上传的照片全没了"。用户报的「上传的附件都没有保存」的真根因。
⚠️ 为什么照片与海报/票根不一样:海报/票根各有独立字段(`stage_poster` /
`stage_ticket`,见下面那段),照片是**多个**,所以走通用那组
`stage_token[]`(`_stage_drop.php` 里 `$sdField` 的规则)。
海报/票根那两段只认自己的字段,通用那组**没有落点**。
⇒ 这里把 `stage_token[]` 逐个读出来、按 'photo' 槽位重跑 Upload::image。 */
$stagedPhotoTokens = (array)($_POST['stage_token'] ?? []);
$photoKeys = $isEdit
? array_map('strval', array_column(Movie::photos($id), 'key')) // 保住已有的
: [];
$photoTouched = false;
foreach ($stagedPhotoTokens as $stoken) {
$stoken = trim((string)$stoken);
if ($stoken === '') {
continue;
}
$sf = \App\StagedUpload::read($familyId, (int)Auth::personId(), $stoken);
if ($sf === null) {
$warns[] = '有一张照片:暂存文件已失效,请重传一次。';
continue;
}
$fake = [
'name' => (string)$sf['name'],
'type' => (string)$sf['mime'],
'tmp_name' => (string)$sf['path'],
'error' => UPLOAD_ERR_OK,
'size' => (int)@filesize((string)$sf['path']),
];
$up = Upload::image($familyId, $fake, 'photo');
if ($up['ok']) {
$photoKeys[] = (string)$up['file'];
$photoTouched = true;
\App\StagedUpload::forget($stoken);
} elseif ((string)($up['error'] ?? '') !== '') {
$warns[] = '有一张照片没有保存:' . $up['error'];
}
}
if ($photoFiles) {
$photoKeys = $isEdit
? array_map('strval', array_column(Movie::photos($id), 'key')) // 保住已有的
: [];
foreach ($photoFiles as $f) {
$up = Upload::image($familyId, $f, 'photo');
if ($up['ok']) {
$photoKeys[] = (string)$up['file'];
$photoTouched = true;
} elseif ((string)($up['error'] ?? '') !== '') {
$warns[] = '有一张照片没有保存:' . $up['error'];
}
}
}
if ($photoTouched || !$isEdit) {
$d['photos'] = $photoKeys;
} elseif (!$isEdit) {
// 新增且一张都没选 → 明确给空(否则 create 会拿到一个不存在的键)
$d['photos'] = [];
}
/* ---- v24 第 20 批 第 4 条:拖动**即时上传**的海报 / 票根 ----
@@ -2855,7 +2891,28 @@ switch ($r) {
flash('err', '找不到这条观影记录。');
redirect('movies');
}
$staged = (array)($_POST['stage_token'] ?? []);
$up = Upload::image($familyId, $_FILES['photo'] ?? [], 'photo');
if (!$up['ok'] && $staged) {
// 🔴 v35:详情页"补一张照片"的框也是**暂存式**(_stage_drop 默认走暂存),
// 所以这里原来只读 $_FILES['photo'] 时**永远拿不到文件**(用户报的
// "上传的附件都没有保存")。暂存令牌 → 重跑 Upload::image。
$stoken = trim((string)($staged[0] ?? ''));
$sf = $stoken !== ''
? \App\StagedUpload::read($familyId, (int)Auth::personId(), $stoken) : null;
if ($sf !== null) {
$up = Upload::image($familyId, [
'name' => (string)$sf['name'],
'type' => (string)$sf['mime'],
'tmp_name' => (string)$sf['path'],
'error' => UPLOAD_ERR_OK,
'size' => (int)@filesize((string)$sf['path']),
], 'photo');
if ($up['ok']) {
\App\StagedUpload::forget($stoken);
}
}
}
if ($up['ok']) {
// savePhotos 是整组替换 —— 现有键取出来再追加一张,位置接着排
$keys = array_map('strval', array_column(Movie::photos($id), 'key'));