v24 第 20 批:拖动即时上传 / 观影图片带出 / 行程保存修复 / 日程红线与删除范围与多提醒 / 悬停统一(用户 9 条)
用户 9 条原话要点:
①「观影模块,编辑观影,已经保存的海报和票根、合影,都没带进来。」
②「个人设置页面,"只影响你自己看到的样子"这个提示删除,不要了。我怎么称呼家人,
这个卡片,不要了。我的资料这个卡片,不要了。」
③「旅程模块,编辑行程,点击保存没有反应。」
④「所有上传图片视频和附件的模块,都允许多次拖动添加文件上传,拖动一个进去,
就开始上传,还可以继续拖动,继续上传,最后统一保存即可。」
⑤「添加行程,线路名、承运方,都不要了。车辆,只在出行方式选择自驾时才出现,
其他情况下默认不出现。出发地和到达地,应该在同一行。」
⑥「日程模块,周视图和日视图,应该在视图中间显示一条红线,用来表示当前进行的时间…
用一个不断从上到下的红线,来表示当前时间在视图上的刻度和位置。」
⑦「对于重复的日程,当点击删除的时候,应该提示是删除这一次,还是全部都删除,
还是删除这一次及以后的。」
⑧「日程的提醒,是可以多选的,就是说,一个日程,我可以选择让它 5 分钟提醒,
10 分钟提醒等等。」
⑨「很多按钮的悬停效果非常奇怪,感觉纯粹是换了一个背景色…应该调整所有按钮的悬停效果,
保持统一,但是不要单纯的换一个不协调的背景色。」
🔴 三个真根因(都是"看着不像 bug"的地方):
1. **③ 点保存没反应 = `<form>` 嵌套**(HTML 非法):`trips/node_form.php` 的照片缩略图上
套了 `<form action=photo_del>`,而整块在**外层行程表单内部**。浏览器解析到内层
`</form>` 时**把外层表单提前关掉** → 「保存」按钮**掉出表单**(真机实测
`button.form === null`)。源码里完全看不出,**只有真浏览器量 `btn.form` 才现形**;
且**只有这段行程已经有照片时才触发** —— 所以表现为"有时候好、有时候坏"。
→ 按钮留在原位(布局不动),用 HTML5 `form="id"` 关联到**表单外**的独立表单。
2. **⑤ 车辆藏不住(第 17 批就写过、一直没生效)**:`.field` 自己声明 `display:flex`,
作者样式压过浏览器默认的 `[hidden]{display:none}` → `hidden` 形同虚设。
**本项目第 4 次栽同一个坑**(.att-item / .pv / .pp-item 各一次)→ 补 `.field[hidden]`。
3. **④ 拖动即时上传的卡片"看不见"**:模板给 `.stage-list` 加了 `hidden`(没人上传时
不留空边框),JS 建卡片时忘了 `list.hidden = false` → 卡片在 DOM 里但整条清单
`display:none`、尺寸 0×0。**HTTP 层断言全绿**(HTML 里确实有卡片),
**只有真机量尺寸才发现** —— 由新写的真机探针抓到。
本批新增能力:
- **拖动即时上传**(第 4 条):新增 `upload_stage.php` + `app/StagedUpload.php` +
`templates/_stage_drop.php`。拖进来 → **立刻** POST 到暂存目录 → 回 `stoken` →
放进隐藏字段 `stage[]` → 保存时在 `attach_save_files()` 里**认领**(移文件 + 建附件行)。
`media.php` 加 `?stage=` 预览(拖完立刻看得见缩略图)。
⚠️ 三个必须踩准:**串行队列**(并发会让缩略图顺序乱跳)、**提交守卫**
(还有没传完就 preventDefault,否则"拖了但没保存上"必然发生)、**卡片清单要 un-hide**。
⚠️ 海报/票根走**独立字段名** `stage_poster`/`stage_ticket`(各自单槽位、要按比例重裁)。
⚠️ 落点**就近取**(`input.closest('.field')`):一个表单里多个上传区不能串到第一个去。
- **观影编辑带出已存图片**(第 1 条):编辑页显示海报 / 票根 / 合影 + 各自的删除入口;
新增 `movies/media_del`(只删不换也能删,槽位走白名单);`movies/update` 收编辑页
新选的照片时**合并**而不是替换(否则一编辑就把已有合影冲掉)。
- **行程表单**(第 5 条):去掉线路名 / 承运方(**库里的值保留**:UPDATE 时不参与,
与 seat_info/gate 同一套);出发地/到达地包进 `.loc-row` 真同一行;车辆按自驾显隐。
- **当前时间红线**(第 6 条):位置在服务端算(与 `.tg-block` 同一套"24h = 100%"坐标),
前端每分钟自己挪(对齐到下一分钟整再起定时器),跨午夜 reload;只画在**今天那一列**。
- **重复日程删除三档**(第 7 条):与「编辑」那三档同名同序(全部 / 这一次及以后 /
只这一次)。「只删这一次」→ `skipOnce`(**绝不删整条系列**);「这一次及以后」→
老系列截 UNTIL,**必须清掉 COUNT**(两者并存时 COUNT 优先,不清就截不断)。
- **提醒多选**(第 8 条):同列存逗号串(`5,15,60`);Schema 把它从 **INT 改 VARCHAR**
(INT 存逗号串会被隐式转换,多档会静默只剩第一档);ICS 导出吐**多个 VALARM**。
- **悬停统一**(第 9 条):原来 14 种互不相干的 hover 底色 → 统一成**两套手法**:
有底色 `filter: brightness(.92)` 同色系加深 + 轻抬升;描边白底 `--hov-veil` 中性薄雾
+ 边框微显。放在 CSS **最末**用一条兜底收口(新按钮自动继承),并把十几条旧规则
清成"交给统一段"。`person_picker.js` 同时改为按需加载。
验证(本地全套):
- e2e **1004/0**(+21 条本批断言)· 升级 149/0 · 桌面几何 428/0 ·
**真机探针 25/0**(连续拖两次 / 红线位置与颜色与几何 / 车辆显隐 / 出发到达同行)。
- **反向验证 5/5**:①删除按钮改回嵌套 form ②卡片清单不 un-hide ③观影编辑不渲染海报
④车辆隐藏失效 ⑤「只删这一次」做成删全部 —— 五种坏法全部被抓住,还原后复绿 1004/0。
过程中还**抓到两条弱断言**(只查 DOM 有无、不查可见性/是否真渲染),已一并加强。
- 部署脚本静态断言段本地空跑 **305 条全过**。
This commit is contained in:
@@ -281,6 +281,21 @@ function render_me_settings(array $person, array $errors = []): void
|
||||
*/
|
||||
function attach_save_files(int $familyId, string $type, int $targetId, string $field, int $uploaderId): array
|
||||
{
|
||||
// v24 第 20 批 第 4 条:先把**拖动即时上传**的暂存文件认领到这个条目上。
|
||||
// 用户原话:「拖动一个进去,就开始上传…最后统一保存即可。」
|
||||
// 那些文件在拖动时就已经传到服务器了(upload_stage.php),但**没有挂到任何条目**;
|
||||
// 保存时(这里)才真正认领 —— 于是"立刻上传"与"最后统一保存"两件事都成立。
|
||||
// ⚠️ 认领放在临时文件之前:两者最终都进同一张附件表、按 position 顺序排,
|
||||
// 先认领的在前(也就是"拖动加进来的先出现"),符合操作顺序。
|
||||
$staged = (array)($_POST['stage'] ?? []);
|
||||
$stageWarns = [];
|
||||
if ($staged) {
|
||||
$res = \App\StagedUpload::claim($familyId, $uploaderId, $staged, $type, $targetId);
|
||||
$stageWarns = (array)($res['warns'] ?? []);
|
||||
}
|
||||
// 顺手清掉过期没认领的暂存(不必挂定时任务)
|
||||
\App\StagedUpload::sweep();
|
||||
|
||||
$files = [];
|
||||
if (!empty($_FILES[$field]) && is_array($_FILES[$field])) {
|
||||
$f = $_FILES[$field];
|
||||
@@ -298,7 +313,7 @@ function attach_save_files(int $familyId, string $type, int $targetId, string $f
|
||||
$files[] = $f;
|
||||
}
|
||||
}
|
||||
$warns = [];
|
||||
$warns = $stageWarns ?? [];
|
||||
foreach ($files as $f) {
|
||||
$res = \App\Attachment::save($familyId, $type, $targetId, $f, $uploaderId);
|
||||
if (empty($res['ok']) && (string)($res['error'] ?? '') !== '') {
|
||||
@@ -583,6 +598,26 @@ function filter_family_person_ids(array $ids, int $familyId): array
|
||||
|
||||
switch ($r) {
|
||||
|
||||
/**
|
||||
* 暂存上传(v24 第 20 批 用户第 4 条)。
|
||||
*
|
||||
* 用户原话:「所有上传图片视频和附件的模块,都允许多次拖动添加文件上传,
|
||||
* 拖动一个进去,就开始上传,还可以继续拖动,继续上传,最后统一保存即可。」
|
||||
*
|
||||
* ⚠️ 真正的实现放在**独立文件** `upload_stage.php` 里,这里只做一次转发:
|
||||
* · 那个端点要自己控制响应头(纯 JSON)、自己的错误处理
|
||||
* (任何 PHP 警告被 display_errors 打成 HTML,前端 JSON.parse 就炸);
|
||||
* · 放在独立文件里也和 media.php 同一个思路:**自成一条出入口,便于审阅**。
|
||||
* ⚠️ 转发必须把这次请求的**全部**上下文原样交给它($_FILES/$_POST 是超全局,
|
||||
* 自动带过去;但 __DIR__ 与相对 require 的基准会变,所以先 chdir 到站点根)。
|
||||
* ⚠️ `require` 之后脚本**不会回来**(那个文件自己 exit),所以这里不必写 break;
|
||||
* 但语法上留着更清楚"这里就是终点"。
|
||||
*/
|
||||
case 'upload/stage':
|
||||
chdir(__DIR__);
|
||||
require __DIR__ . '/upload_stage.php';
|
||||
break;
|
||||
|
||||
// ==================== 登录 / 退出 ====================
|
||||
case 'auth/login':
|
||||
if (Auth::check()) {
|
||||
@@ -2313,21 +2348,30 @@ switch ($r) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 现场照片(多选,仅新增表单提供;编辑页在详情页里逐张补/删)
|
||||
if (!$isEdit) {
|
||||
$photoFiles = [];
|
||||
if (!empty($_FILES['photo']) && is_array($_FILES['photo']['name'])) {
|
||||
foreach ($_FILES['photo']['name'] as $i => $_n) {
|
||||
$photoFiles[] = [
|
||||
'name' => $_FILES['photo']['name'][$i],
|
||||
'type' => $_FILES['photo']['type'][$i],
|
||||
'tmp_name' => $_FILES['photo']['tmp_name'][$i],
|
||||
'error' => $_FILES['photo']['error'][$i],
|
||||
'size' => $_FILES['photo']['size'][$i],
|
||||
];
|
||||
}
|
||||
// 现场照片(多选)。
|
||||
// ⚠️ v24 第 20 批 第 1 条:**编辑时也收**这里传上来的照片了。
|
||||
// 以前只有新增表单给这个字段(编辑页只在详情页里逐张补),所以这里写着
|
||||
// `if (!$isEdit)`。现在编辑页也能一次多选加几张,所以要处理"合并"而不是替换:
|
||||
// 编辑 = 库里已有的(在编辑页逐张删过的会被 photo_del 立刻删掉,库里就是最新的)
|
||||
// + 这次新选上来的。
|
||||
// ⚠️ 不能沿用"整组替换":那样一编辑就会把已有的照片全冲掉(用户报的
|
||||
// "合影没带进来"如果按替换实现,会变成更难发现的"合影丢了")。
|
||||
$photoFiles = [];
|
||||
if (!empty($_FILES['photo']) && is_array($_FILES['photo']['name'])) {
|
||||
foreach ($_FILES['photo']['name'] as $i => $_n) {
|
||||
$photoFiles[] = [
|
||||
'name' => $_FILES['photo']['name'][$i],
|
||||
'type' => $_FILES['photo']['type'][$i],
|
||||
'tmp_name' => $_FILES['photo']['tmp_name'][$i],
|
||||
'error' => $_FILES['photo']['error'][$i],
|
||||
'size' => $_FILES['photo']['size'][$i],
|
||||
];
|
||||
}
|
||||
$photoKeys = [];
|
||||
}
|
||||
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']) {
|
||||
@@ -2337,6 +2381,53 @@ switch ($r) {
|
||||
}
|
||||
}
|
||||
$d['photos'] = $photoKeys;
|
||||
} elseif (!$isEdit) {
|
||||
// 新增且一张都没选 → 明确给空(否则 create 会拿到一个不存在的键)
|
||||
$d['photos'] = [];
|
||||
}
|
||||
|
||||
/* ---- v24 第 20 批 第 4 条:拖动**即时上传**的海报 / 票根 ----
|
||||
用户原话:「拖动一个进去,就开始上传…最后统一保存即可。」
|
||||
拖动会上传到暂存区并回一个 stoken,前端放进隐藏字段 `stage_poster` /
|
||||
`stage_ticket`(**海报/票根各一个字段名**,因为它们要区分槽位)。
|
||||
⚠️ 这里直接取 stoken 对应的暂存文件来重跑 Upload::image(按该槽位的比例),
|
||||
而不是把暂存文件原样搬过去 —— 海报是 2:3、票根是 free,
|
||||
不按槽位处理就会存成比例不对的图(页面上会变形)。
|
||||
⚠️ 临时文件上传(`$_FILES[$slot]`)优先:用户**又选又拖**时以"选"的为准
|
||||
(那是他更明确的动作),免得两个来源打架。
|
||||
⚠️ 认领失败要记进 $warns(不静默丢),且**不影响保存**(与既有口径一致)。 */
|
||||
foreach (['poster' => 'stage_poster', 'ticket' => 'stage_ticket'] as $slotKey => $fieldName) {
|
||||
if (!empty($_FILES[$slotKey]) && (int)($_FILES[$slotKey]['error'] ?? 4) === UPLOAD_ERR_OK) {
|
||||
continue; // 选了新文件 → 上面的常规路径已经处理过了
|
||||
}
|
||||
$stoken = trim((string)($_POST[$fieldName] ?? ''));
|
||||
if ($stoken === '') {
|
||||
continue;
|
||||
}
|
||||
$sf = \App\StagedUpload::read($familyId, (int)Auth::personId(), $stoken);
|
||||
if ($sf === null) {
|
||||
$warns[] = ($slotKey === 'poster' ? '海报' : '票根') . ':暂存文件已失效,请重拖一次。';
|
||||
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, $slotKey);
|
||||
if (!empty($up['ok'])) {
|
||||
$newKey = (string)$up['file'];
|
||||
$oldKey = $isEdit ? (string)($m[$slotKey . '_key'] ?? '') : '';
|
||||
$d[$slotKey . '_key'] = $newKey;
|
||||
if ($oldKey !== '' && $oldKey !== $newKey) {
|
||||
\App\Storage\Storage::delete($familyId, $oldKey);
|
||||
}
|
||||
\App\StagedUpload::forget($stoken); // 暂存那份不再需要
|
||||
} elseif ((string)($up['error'] ?? '') !== '') {
|
||||
$warns[] = ($slotKey === 'poster' ? '海报' : '票根') . '没有保存:' . $up['error'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($errors) {
|
||||
@@ -2399,9 +2490,53 @@ switch ($r) {
|
||||
}
|
||||
Movie::deletePhoto($familyId, $id, $pid);
|
||||
flash('ok', '照片已删除。');
|
||||
// ⚠️ v24 第 20 批 第 1 条:编辑页也能删照片了,所以按来路回去 ——
|
||||
// 从编辑页删的就回编辑页(否则用户一删就被甩回详情页,还得再点一次编辑)。
|
||||
// 来路用表单里的 `back` 判断(由模板给出,白名单,免得开放重定向)。
|
||||
$back = (string)($_POST['back'] ?? '');
|
||||
if ($back === 'edit') {
|
||||
redirect('movies/edit', ['id' => $id]);
|
||||
}
|
||||
redirect('movies/show', ['id' => $id]);
|
||||
break;
|
||||
|
||||
/**
|
||||
* 删掉海报 / 票根(v24 第 20 批 第 1 条)。
|
||||
*
|
||||
* 为什么需要它:编辑页现在把已存的海报/票根**显示出来**了,
|
||||
* 用户看着一张不想要的图,唯一的动作就是"再选一张新的"——
|
||||
* 而他可能只是**想去掉它**(海报库里没有更好的,删掉反而干净)。
|
||||
*
|
||||
* ⚠️ 顺带把存储对象一起删(只清库不留孤儿文件,与 Movie::deletePhoto 同一口径)。
|
||||
* ⚠️ 换图时(movies/update)服务端已经会删旧图,所以这里只管"只删不换"。
|
||||
*/
|
||||
case 'movies/media_del':
|
||||
$familyId = (int)Auth::familyId();
|
||||
Auth::requireWrite();
|
||||
csrf_check();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$slot = (string)($_POST['slot'] ?? '');
|
||||
$m = Movie::find($familyId, $id, (int)Auth::personId());
|
||||
if (!$m) {
|
||||
flash('err', '找不到这条观影记录。');
|
||||
redirect('movies');
|
||||
}
|
||||
// 白名单:只认这两个槽位(表单可以被改,不能凭它去删别的字段/别的对象)
|
||||
$col = ['poster' => 'poster_key', 'ticket' => 'ticket_key'][$slot] ?? '';
|
||||
if ($col === '') {
|
||||
flash('err', '不知道要删哪一样。');
|
||||
redirect('movies/edit', ['id' => $id]);
|
||||
}
|
||||
$old = (string)($m[$col] ?? '');
|
||||
Database::get()->prepare('UPDATE movie_details SET ' . $col . ' = NULL WHERE event_id = ?')
|
||||
->execute([$id]);
|
||||
if ($old !== '') {
|
||||
\App\Storage\Storage::delete($familyId, $old);
|
||||
}
|
||||
flash('ok', ($slot === 'poster' ? '海报' : '票根') . '已删除。');
|
||||
redirect('movies/edit', ['id' => $id]);
|
||||
break;
|
||||
|
||||
case 'movies/destroy':
|
||||
$familyId = (int)Auth::familyId();
|
||||
Auth::requireWrite();
|
||||
@@ -3590,11 +3725,21 @@ switch ($r) {
|
||||
// 点空白处只带日期/时刻过来,标题等还是空的(不能替用户瞎填)。
|
||||
$newAt = trim((string)($_GET['new_at'] ?? ''));
|
||||
|
||||
/* ---- 当前时间红线(v24 第 20 批 用户第 6 条)----
|
||||
用户原话:「周视图和日视图,应该在视图中间显示一条红线…来表示当前时间
|
||||
在视图上的刻度和位置。」
|
||||
⚠️ 位置在**服务端**算好(与 .tg-block 的 top/height 同一套算法:
|
||||
列高固定 24 小时 → 1 分钟 = 100/1440 %),前端只负责"过一会儿自己挪一下"。
|
||||
不在 CSS 里写死时间,也不在前端算 —— 算法只有一处,不会两边对不上。 */
|
||||
$nowMinutes = ((int)date('G')) * 60 + (int)date('i');
|
||||
$nowTop = round($nowMinutes / 1440 * 100, 4);
|
||||
|
||||
View::render('calendar/index', [
|
||||
'title' => '日程',
|
||||
'ym' => $ym,
|
||||
'view' => $view,
|
||||
'anchor' => $anchor,
|
||||
'nowTop' => $nowTop,
|
||||
'cols' => $view === 'month' ? [] : ($cols ?? []),
|
||||
'weeks' => $view === 'month' ? $grid : [],
|
||||
'tgrid' => $view === 'month' ? null : $grid,
|
||||
@@ -3857,7 +4002,11 @@ switch ($r) {
|
||||
'starts_at' => $startsAt,
|
||||
'ends_at' => $endsAt,
|
||||
'all_day' => $allDay,
|
||||
'reminder_minutes' => (string)($_POST['reminder_minutes'] ?? ''),
|
||||
// v24 第 20 批 第 8 条:提醒**多选** → 表单来的是数组 `reminder_minutes[]`。
|
||||
// ⚠️ 不要再 `(string)` 强转 —— 那会得到 "Array" 字样(PHP 的经典坑),
|
||||
// Calendar::reminderMinutes() 收到它就一份都认不出来,提醒静默丢光。
|
||||
// 直接原样把数组交下去,由 Calendar 那边做白名单 + 规范化。
|
||||
'reminder_minutes' => ($_POST['reminder_minutes'] ?? ''),
|
||||
'status' => (string)($_POST['status'] ?? 'confirmed'),
|
||||
'transp' => !empty($_POST['transp']),
|
||||
// 用户 2026-10-03 第 5、6 条:重复规则(RFC 5545 RRULE 原文)
|
||||
@@ -3951,7 +4100,11 @@ switch ($r) {
|
||||
'starts_at' => $startsAt,
|
||||
'ends_at' => $endsAt,
|
||||
'all_day' => $allDay,
|
||||
'reminder_minutes' => (string)($_POST['reminder_minutes'] ?? ''),
|
||||
// v24 第 20 批 第 8 条:提醒**多选** → 表单来的是数组 `reminder_minutes[]`。
|
||||
// ⚠️ 不要再 `(string)` 强转 —— 那会得到 "Array" 字样(PHP 的经典坑),
|
||||
// Calendar::reminderMinutes() 收到它就一份都认不出来,提醒静默丢光。
|
||||
// 直接原样把数组交下去,由 Calendar 那边做白名单 + 规范化。
|
||||
'reminder_minutes' => ($_POST['reminder_minutes'] ?? ''),
|
||||
'status' => (string)($_POST['status'] ?? 'confirmed'),
|
||||
'transp' => !empty($_POST['transp']),
|
||||
'rrule' => Repeat::build($_POST),
|
||||
@@ -4032,21 +4185,79 @@ switch ($r) {
|
||||
$ym = trim((string)($_POST['ym'] ?? ''));
|
||||
$ym = preg_match('/^\d{4}-\d{2}$/', $ym) ? $ym : date('Y-m');
|
||||
$st = Database::get()->prepare(
|
||||
'SELECT e.id, e.calendar_id FROM calendar_events e
|
||||
'SELECT e.id, e.calendar_id, e.rrule, e.starts_at FROM calendar_events e
|
||||
JOIN calendars c ON c.id = e.calendar_id
|
||||
WHERE e.id = ? AND c.family_id = ?'
|
||||
);
|
||||
$st->execute([$id, $familyId]);
|
||||
$ev = $st->fetch();
|
||||
if ($ev) {
|
||||
$cal = Calendar::mineOne($familyId, $me, (int)$ev['calendar_id']) ?: [];
|
||||
if (Calendar::canWrite($cal, $me)) {
|
||||
Calendar::deleteEvent($id);
|
||||
flash('ok', '日程已删除。');
|
||||
} else {
|
||||
flash('err', '这本日历你没有写权限。');
|
||||
}
|
||||
if (!$ev) {
|
||||
flash('err', '找不到这条日程。');
|
||||
redirect('calendar', ['ym' => $ym]);
|
||||
}
|
||||
$cal = Calendar::mineOne($familyId, $me, (int)$ev['calendar_id']) ?: [];
|
||||
if (!Calendar::canWrite($cal, $me)) {
|
||||
flash('err', '这本日历你没有写权限。');
|
||||
redirect('calendar', ['ym' => $ym]);
|
||||
}
|
||||
|
||||
/* ==================== 重复日程的删除范围(v24 第 20 批 用户第 7 条)====================
|
||||
用户原话:「对于重复的日程,当点击删除的时候,应该提示是删除这一次,
|
||||
还是全部都删除,还是删除这一次及以后的。」
|
||||
|
||||
⚠️ 三档与**编辑**那三档(第 13 批做的 scope)刻意对齐,用户学一次就够:
|
||||
once = 只删这一次 → 在 calendar_event_skips 里记一天(**不是**删整条系列!
|
||||
真删了就"全部都没了",正是用户要避免的);
|
||||
future = 删除这一次及以后 → 老系列截止到「这一次的前一天」;
|
||||
这一步**必须**算出截止日并写进 RRULE,否则整条系列都会留着一个
|
||||
"还没结束的重复",照样冒出来;
|
||||
all = 全部(整条删除,与以前一样)。
|
||||
|
||||
⚠️ 范围信息从哪来:`scope`(用户在弹框里选的)+ `occ`(点开的是哪一天)。
|
||||
两者缺一不可 —— 少了 occ 就定位不到"这一次"(重复日程每一天都是同一条记录 id)。
|
||||
⚠️ 不重复的日程直接按 all(没有"系列"可言),表单也不会给它选范围。
|
||||
*/
|
||||
$scope = (string)($_POST['scope'] ?? 'all');
|
||||
$occDay = substr(trim((string)($_POST['occ'] ?? '')), 0, 10);
|
||||
$rrule = (string)($ev['rrule'] ?? '');
|
||||
$hasOcc = (bool)preg_match('/^\d{4}-\d{2}-\d{2}$/', $occDay);
|
||||
|
||||
if ($rrule !== '' && $hasOcc && $scope === 'once') {
|
||||
// 只删这一次:记一个"跳过这一天的例外",系列原样保留
|
||||
Calendar::skipOnce($id, $occDay);
|
||||
flash('ok', '只删了 ' . $occDay . ' 这一次;后面的照旧。');
|
||||
redirect('calendar', ['ym' => $ym]);
|
||||
}
|
||||
if ($rrule !== '' && $hasOcc && $scope === 'future') {
|
||||
// 这一次及以后:老系列截至「这一次的前一天」。
|
||||
$rp = Repeat::parse($rrule);
|
||||
$cut = date('Y-m-d', strtotime($occDay . ' -1 day'));
|
||||
// ⚠️ 截断结果要重新拼 RRULE:只改 UNTIL、并清掉 COUNT
|
||||
// (COUNT 与 UNTIL 同时存在时以 COUNT 为准,不干掉它这次截断就失效)。
|
||||
$rp['count'] = 0;
|
||||
$rp['until'] = $cut;
|
||||
// ⚠️ 边界:如果"这一天"就是系列的第一次(cut 落在首次之前),
|
||||
// 那"这一次及以后"等于**全部** —— 截断会产出一条永不发生的系列,
|
||||
// 留着纯属垃圾,所以那种情况直接整条删掉。
|
||||
$firstDay = substr((string)$ev['starts_at'], 0, 10);
|
||||
if ($cut < $firstDay) {
|
||||
Calendar::deleteEvent($id);
|
||||
flash('ok', '这一天就是第一次,「这一次及以后」等于整条删除。');
|
||||
redirect('calendar', ['ym' => $ym]);
|
||||
}
|
||||
$newRule = 'FREQ=' . strtoupper((string)$rp['freq'])
|
||||
. (!empty($rp['interval']) && (int)$rp['interval'] > 1 ? ';INTERVAL=' . (int)$rp['interval'] : '')
|
||||
. ($rp['freq'] === 'weekday' ? ';BYDAY=MO,TU,WE,TH,FR' : '')
|
||||
. ';UNTIL=' . str_replace('-', '', $cut) . 'T235959Z';
|
||||
Database::get()->prepare('UPDATE calendar_events SET rrule = ?, sequence = sequence + 1 WHERE id = ?')
|
||||
->execute([$newRule, $id]);
|
||||
flash('ok', $occDay . ' 及以后的那些都删了(之前的保持不变)。');
|
||||
redirect('calendar', ['ym' => $ym]);
|
||||
}
|
||||
|
||||
// all(或本来就不重复):整条删除
|
||||
Calendar::deleteEvent($id);
|
||||
flash('ok', '日程已删除。');
|
||||
redirect('calendar', ['ym' => $ym]);
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user