v43 第 43 批:六条反馈(界面 5 + 条目详情浮层)
用户 2026-10-11 反馈六条,逐条定位真根因后修复: ① 纪念日「日期」多出一个控件 —— 只保留最新的那个 两层根因:(a)原生 `input[type=date]` 当初没包在 `.dt-native` 里, v42 那条"AntD 挂上后收起原生框"的选择器根本不命中;(b)更要命的是 `.dt-native` 基类写着 `display: contents` —— 该元素**没有盒子**, 给它 position/width 完全无效 ⇒ 必须把 display 显式改回 block 才收得起来。 ② 编辑纪念日把底层列表页弄没 —— 「修改」原来是整页跳 `days/edit` (套 layout = 一张只有弹框的空白页)。改为 `?embed=1` 只吐弹框那一块, 列表页 JS 取回来替换 `#anNewCard` 并打开 —— 与「新建」同一观感。 内嵌形态标 `$anModalOnly` 以免打 standalone 标记(关闭时不再跳页)。 ③ 拖图裁剪的层被弹框盖住 —— `.crop-overlay` 是 z-index:100 而 `.cal-modal` 是 200 ⇒ 裁剪层被整块盖住。提到 260(与 v41 的 .cn-modal 同一处理)。 ④ 行程弹框里"参与的人"弹不出选人框 —— person_picker.js 只绑定一次, 而弹框表单是 AJAX 片段 ⇒ 里面的 `.pp` 从未被 init。三件事一起: 暴露 `window.fbInitPickers()`(幂等)+ layout 暴露 `window.fbLoadPicker()` (脚本是按需加载的)+ 塞完片段后补调初始化。 ⑤ 日程新建排版/控件/文案 —— 改走观影式 `.mv-grid`(标签对齐 + 冒号); 起止补上 `$dtPlugin='antd'`(试点正式推广到日程);标签「时间范围」→「开始 / 结束」; 删三句提示(结束留空 / 多选提醒 / 全天)。 ⑥ 条目详情浮层(首页日程 / 首页近期任务 / 日程页条目)—— 点条目不再直接跳页,弹浮层显示详情;「详细信息」跳该来源详情页; 「编辑」就地换成该来源的编辑表单(四处 `?embed=1` 片段)。 新增 `templates/_entry_popup.php` + `assets/js/entry_popup.js` + `fb_entry_json()`(三个入口共用一个编码函数)。 验证:php -l 157 文件全过;div 深度 33 组合全配平;片段体检 20 片段无越界; 本地 e2e 1510/0;v43 真机 33/0;v43 反向验证 8/0;v38 36/0;v42 33/0; b31/b32 反向验证 8/0 与 5/0;部署断言本地空跑 RC=0;完整流水线 PUSH_RC=0。
This commit is contained in:
@@ -868,6 +868,18 @@ final class Calendar
|
|||||||
return in_array($v, ['confirmed', 'tentative', 'cancelled'], true) ? $v : 'confirmed';
|
return in_array($v, ['confirmed', 'tentative', 'cancelled'], true) ? $v : 'confirmed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔴 v43(用户第 6 条):日程状态的**中文说法** —— 条目详情浮层里要显示"状态"。
|
||||||
|
* 与 `templates/calendar/_event_fields.php` 那个 <select> 的选项文案**一字不差**:
|
||||||
|
* 两处各写一套必然有一天对不上("确定" vs "已确认")。
|
||||||
|
*/
|
||||||
|
public static function statusWord(string $status): string
|
||||||
|
{
|
||||||
|
return ['confirmed' => '确定', 'tentative' => '待定', 'cancelled' => '取消'][
|
||||||
|
self::statusOf($status)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 某个月(或某段日期)里**全部**日程:
|
* 某个月(或某段日期)里**全部**日程:
|
||||||
* 自定义日程(calendar_events)+ 观影(events kind=movie)+ 旅行(events kind=trip)
|
* 自定义日程(calendar_events)+ 观影(events kind=movie)+ 旅行(events kind=trip)
|
||||||
@@ -960,6 +972,11 @@ final class Calendar
|
|||||||
'all_day' => !empty($e['all_day']),
|
'all_day' => !empty($e['all_day']),
|
||||||
'kind' => 'custom',
|
'kind' => 'custom',
|
||||||
'ref_id' => (int)$e['id'],
|
'ref_id' => (int)$e['id'],
|
||||||
|
// 🔴 v43(用户第 6 条):条目详情浮层要显示"更多信息",
|
||||||
|
// 所以地点/备注/状态也要带出去(原来模板只用得到 title/time)。
|
||||||
|
'location' => (string)($e['location'] ?? ''),
|
||||||
|
'note' => (string)($e['note'] ?? ''),
|
||||||
|
'status_word' => self::statusWord((string)($e['status'] ?? 'confirmed')),
|
||||||
// 带上这一天的日期,重复日程点进去才能定位到"改哪一次"
|
// 带上这一天的日期,重复日程点进去才能定位到"改哪一次"
|
||||||
'occ' => $day,
|
'occ' => $day,
|
||||||
'repeat' => $rr,
|
'repeat' => $rr,
|
||||||
|
|||||||
@@ -347,3 +347,68 @@ function days_until_birthday(?string $birthday): ?int
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔴 v43(用户 2026-10-11 第 6 条)—— 把一条日历/任务条目编成**详情浮层**能直接渲染的 JSON。
|
||||||
|
*
|
||||||
|
* 用户原话:
|
||||||
|
* 「首页上的日程、首页上的近期任务,以及日程里面的日程,点击它们的时候,
|
||||||
|
* 都会弹出一个框显示更详细的信息。…如果是观影啊、旅程啊,点击详细信息就会
|
||||||
|
* 进入到观影或者旅程的那个相关的页面。如果是日程的话,点击详细信息就进到
|
||||||
|
* 日程的详细页面。当然还可以直接点击编辑,点击编辑不同的来源就进入不同的编辑页面,
|
||||||
|
* 就在当前的页面弹窗…」
|
||||||
|
*
|
||||||
|
* ⇒ 每种来源由这里给出**三样**:
|
||||||
|
* `detail` 「详细信息」跳哪(该来源的详情页)
|
||||||
|
* `edit` 「编辑」取哪份编辑表单片段(`?embed=1` 的那些路由)
|
||||||
|
* `rows` 浮层正文那几行「标签:值」
|
||||||
|
*
|
||||||
|
* ⚠️ 模板里只调这一个函数,不要在各模板里各拼一份 JSON ——
|
||||||
|
* "三个入口三套写法"必然有一天漏改一个(用户第 6 条点名了三处入口)。
|
||||||
|
* ⚠️ 返回**已经 json_encode 好的字符串**,直接写进 `data-fb-entry='…'`;
|
||||||
|
* 模板里用单引号属性包住(JSON 里的双引号不会撞)。
|
||||||
|
* 用 `JSON_HEX_APOS | JSON_HEX_QUOT` 把引号也转成实体,双保险。
|
||||||
|
*
|
||||||
|
* @param array $ev 日历条目(Calendar::month 的形状:kind/ref_id/title/date/time/…)
|
||||||
|
* @param string $detail 详情页地址(没有就不显「详细信息」)
|
||||||
|
* @param string $edit 编辑表单片段地址(没有就不显「编辑」)
|
||||||
|
* @param array $extra 额外要显示的行 [[标签, 值], …]
|
||||||
|
* @return string 可直接放进 data-fb-entry 的 JSON 字符串(已转义)
|
||||||
|
*/
|
||||||
|
function fb_entry_json(array $ev, string $detail = '', string $edit = '', array $extra = []): string
|
||||||
|
{
|
||||||
|
$date = (string)($ev['date'] ?? '');
|
||||||
|
$time = (string)($ev['time'] ?? '');
|
||||||
|
$et = (string)($ev['end_time'] ?? '');
|
||||||
|
if ($date !== '') {
|
||||||
|
$when = $date;
|
||||||
|
if (!empty($ev['all_day'])) {
|
||||||
|
$when .= ' 全天';
|
||||||
|
} elseif ($time !== '') {
|
||||||
|
$when .= ' ' . $time . ($et !== '' ? '–' . $et : '');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$when = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$rows = [];
|
||||||
|
if ($when !== '') { $rows[] = ['时间', $when]; }
|
||||||
|
if ((string)($ev['calname'] ?? '') !== '') { $rows[] = ['日历本', (string)$ev['calname']]; }
|
||||||
|
if ((string)($ev['location'] ?? '') !== '') { $rows[] = ['地点', (string)$ev['location']]; }
|
||||||
|
if ((string)($ev['status_word'] ?? '') !== '') { $rows[] = ['状态', (string)$ev['status_word']]; }
|
||||||
|
if ((string)($ev['note'] ?? '') !== '') { $rows[] = ['备注', (string)$ev['note']]; }
|
||||||
|
// ⚠️ 扩展行排在"备注"之后、作为补充(调用方按需给,比如任务给「所属看板」)
|
||||||
|
foreach ($extra as $row) {
|
||||||
|
if (is_array($row) && count($row) >= 2 && (string)$row[1] !== '') {
|
||||||
|
$rows[] = [(string)$row[0], (string)$row[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'title' => trim((string)($ev['title'] ?? '')),
|
||||||
|
'rows' => $rows,
|
||||||
|
'detail' => $detail,
|
||||||
|
'edit' => $edit,
|
||||||
|
];
|
||||||
|
return json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||||
|
}
|
||||||
|
|||||||
+54
-5
@@ -1486,7 +1486,15 @@ body[class] .semi-datepicker-day-inhover { background-color: color-mix(in srgb,
|
|||||||
Cropper 的四个角手柄画成实心白点,永远落在面板可见区内。 */
|
Cropper 的四个角手柄画成实心白点,永远落在面板可见区内。 */
|
||||||
.crop-overlay { display: none; }
|
.crop-overlay { display: none; }
|
||||||
.crop-overlay.is-open {
|
.crop-overlay.is-open {
|
||||||
display: flex; position: fixed; inset: 0; z-index: 100;
|
/* 🔴🔴 v43(用户 2026-10-11 第 3 条):「新建行程的时候,如果把地图拖进去,
|
||||||
|
它不是开始裁剪吗?但是这个弹出的框把裁剪那个框又盖住了,裁剪的框应该放到前面来。」
|
||||||
|
真根因:裁剪层是 **100**,而行程/观影那些弹框用的 `.cal-modal` 是 **200**
|
||||||
|
⇒ 从弹框里拖图触发的裁剪层被整块盖在下面 —— 看不见、也点不到。
|
||||||
|
(与 v41 那个「新建影院弹框被记一场电影盖住」是**同一个坑**,当时把 .cn-modal
|
||||||
|
提到了 260;这里漏了。)
|
||||||
|
⇒ 提到 **260**:高于所有自建弹框(.cal-modal 200 / .pp-modal 等),
|
||||||
|
与 .cn-modal 同层;低于灯箱(300)、预览层(320) —— 那两者是"看大图"的顶级层。 */
|
||||||
|
display: flex; position: fixed; inset: 0; z-index: 260;
|
||||||
align-items: center; justify-content: center;
|
align-items: center; justify-content: center;
|
||||||
padding: 20px; background: rgba(44, 35, 29, .55);
|
padding: 20px; background: rgba(44, 35, 29, .55);
|
||||||
}
|
}
|
||||||
@@ -5610,14 +5618,55 @@ h5.md-h, h6.md-h { font-size: 14px; }
|
|||||||
· `.dt-antd` 是 antd 的挂载点(宽度交给它);
|
· `.dt-antd` 是 antd 的挂载点(宽度交给它);
|
||||||
· 挂上后(`.is-antd`)把**原生 `<input type="date">` 视觉收起** ——
|
· 挂上后(`.is-antd`)把**原生 `<input type="date">` 视觉收起** ——
|
||||||
它仍是值载体(antd 通过 change 写它),只是不再显示给用户。
|
它仍是值载体(antd 通过 change 写它),只是不再显示给用户。
|
||||||
⚠️ 用 clip 收起(不是 display:none):保留可聚焦、可被表单读到,
|
⚠️ 用 clip 收起(不是 display:none):保留可聚焦、可被表单读到。
|
||||||
与 v40 高级搜索那处的取舍一致。 */
|
🔴🔴 v43(用户 2026-10-11 第 1 条):「日期那里多了一个控件,
|
||||||
.dt-input.is-antd > .dt-native { position: absolute; width: 1px; height: 1px;
|
只保留我们最新的控件就行,之前那种控件删除」——真根因有两层:
|
||||||
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px; }
|
① 纪念日的原生框当初**没包在 `.dt-native` 里** ⇒ 这条选择器根本不命中
|
||||||
|
(模板已补上包装);
|
||||||
|
② **更要命的一层**:`.dt-input .dt-native { display: contents }`(第 619 行)
|
||||||
|
会让这个包装盒**自己不生成盒子**、子元素"提升"成 `.dt-input` 的直接布局项 ——
|
||||||
|
此时给它 `position:absolute; width:1px` **完全无效**(它没有盒子可定位),
|
||||||
|
原生框照样按 flex 项排出来 ⇒ 两个控件并排。
|
||||||
|
⇒ 修法:这里必须把 `display` **从 contents 改成 block**,定位才生效。
|
||||||
|
(只写 position/width/height 是不够的 —— 这是我第一版的错,探针当场抓到。) */
|
||||||
|
.dt-input.is-antd > .dt-native {
|
||||||
|
display: block; position: absolute; width: 1px; height: 1px;
|
||||||
|
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
|
||||||
|
}
|
||||||
.dt-input.is-antd > .dt-antd { display: block; width: 100%; }
|
.dt-input.is-antd > .dt-antd { display: block; width: 100%; }
|
||||||
.dt-input.dt-antd-single { display: block; width: 100%; }
|
.dt-input.dt-antd-single { display: block; width: 100%; }
|
||||||
|
/* 兜底:万一哪天原生框又变成 `.dt-input` 的**直接子元素**(不包 .dt-native 了),
|
||||||
|
这条按直接子元素也能把它收起来。 */
|
||||||
|
.dt-input.is-antd > input[type="date"] {
|
||||||
|
display: block; position: absolute; width: 1px; height: 1px;
|
||||||
|
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- v42(用户第 6 条)简介为空时的那句可点占位 ----
|
/* ---- v42(用户第 6 条)简介为空时的那句可点占位 ----
|
||||||
与 `.trip-sum` 同一视觉位置,只是颜色淡一档(它是要点进去补的提示,不是内容)。 */
|
与 `.trip-sum` 同一视觉位置,只是颜色淡一档(它是要点进去补的提示,不是内容)。 */
|
||||||
.trip-sum-empty { margin: 10px 0 0; font-size: 13px; color: var(--muted); }
|
.trip-sum-empty { margin: 10px 0 0; font-size: 13px; color: var(--muted); }
|
||||||
.trip-sum-empty a { color: var(--brand); }
|
.trip-sum-empty a { color: var(--brand); }
|
||||||
|
|
||||||
|
/* ============================================================================
|
||||||
|
🔴 v43(用户 2026-10-11 第 6 条)条目详情浮层
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
与 .cal-modal 同一形态(遮罩 + 居中面板 + 底栏),只是层级更高一点:
|
||||||
|
它可能从**另一个浮层**里打开(例如详情页弹框里点了条目),
|
||||||
|
所以要在 200 之上;但低于"看图"的灯箱(300)/预览层(320)。
|
||||||
|
⚠️ 与 .cn-modal(260)、.crop-overlay(260) 同层 —— 它们不会同时出现。
|
||||||
|
⚠️ 面板用 flex 列 + body 滚动,长内容(编辑表单)才有足够的可视区。 */
|
||||||
|
.fb-entry-modal {
|
||||||
|
position: fixed; inset: 0; z-index: 260;
|
||||||
|
display: flex; align-items: flex-start; justify-content: center;
|
||||||
|
padding: 24px 16px; overflow-y: auto;
|
||||||
|
background: rgba(44, 35, 29, .55); overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
.fb-entry-modal[hidden] { display: none; }
|
||||||
|
.fb-entry-panel { width: min(620px, 100%); max-height: calc(100vh - 64px); }
|
||||||
|
/* 编辑态下正文里就是一张完整表单(内边距由它自己那份 CSS 给)——
|
||||||
|
这里只保证它不双份留白。 */
|
||||||
|
.fb-entry-panel .cal-modal-body > .mv-form-inner,
|
||||||
|
.fb-entry-panel .cal-modal-body > .nf-wrap { padding: 0; }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.fb-entry-panel { width: 100%; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
/* ============================================================================
|
||||||
|
🔴 v43(用户 2026-10-11 第 6 条)—— 条目详情浮层
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
用户原话:
|
||||||
|
「首页上的日程、首页上的近期任务,以及日程里面的日程,点击它们的时候,
|
||||||
|
都会弹出一个框显示更详细的信息。上面有不同的功能按钮,比如说详细信息,
|
||||||
|
在日程里面,如果是观影啊、旅程啊,点击详细信息就会进入到观影或者旅程的
|
||||||
|
那个相关的页面。如果是日程的话,点击详细信息就进到日程的详细页面。
|
||||||
|
当然还可以直接点击编辑,点击编辑不同的来源就进入不同的编辑页面,
|
||||||
|
就在当前的页面弹窗,把之前的窗给去掉,然后弹出编辑的那个框来就可以了。」
|
||||||
|
|
||||||
|
⇒ 一个浮层 + 事件委托,服务全站三种入口:
|
||||||
|
· 首页「今天的日程」(`.ag-ev` / `.ag-allday-item`)
|
||||||
|
· 首页「近期任务」(`.tk-row` 的标题)
|
||||||
|
· 日程页月/周/日视图的条目(`.cal-ev` / `.tg-block`)
|
||||||
|
|
||||||
|
设计取舍(每条都对应一个踩过的坑):
|
||||||
|
① **事件委托挂 document**:列表会被 AJAX 换掉(新建日程后就地插入、
|
||||||
|
首页任务勾选后重画),一次性绑定永久有效(v40 的教训)。
|
||||||
|
② **点条目 = 弹浮层,不跳页**:拦掉 `<a href>` 的默认行为。
|
||||||
|
⚠️ 必须按 `[data-fb-entry]` 精确匹配 —— 不能笼统拦 `.cal-ev`:
|
||||||
|
页面里还有"点某天新建日程"那套逻辑(它同样排除 `.cal-ev`)。
|
||||||
|
③ **数据全在 `data-fb-entry` 上**:浮层不发请求就能画出来,秒开;
|
||||||
|
服务端不参与"点开一条"这个动作。
|
||||||
|
④ **「编辑」就地换内容**:取该来源的编辑表单片段(`?embed=1`)。
|
||||||
|
用户原话「把之前的窗给去掉,然后弹出编辑的那个框来」——
|
||||||
|
所以是"正文整块换掉",不是叠第二层浮层。
|
||||||
|
⑤ **保存走原生提交**:四个来源的 update 路由本来就收普通 POST 并跳转,
|
||||||
|
不依赖任何页面自带脚本 ⇒ 浮层在任何页面都能保存成功。
|
||||||
|
(自己写 AJAX 保存要为四个来源各写一套"成功后回哪"的规则,得不偿失。
|
||||||
|
原生提交会整页刷新 —— 对"改了数据"这件事来说,刷新一次反而更清楚。)
|
||||||
|
⑥ **Esc 只关最上面那层**:antd 面板开着时先让它收(v38/v39 的坑)。
|
||||||
|
⚠️ 本脚本**全站加载**(layout.php):三处入口分散在不同页面,按需加载的判据
|
||||||
|
会在这些页面之间来回漏;它只有几 KB,不值得为省这点流量赌漏加载。
|
||||||
|
============================================================================ */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var modal = null;
|
||||||
|
var detailCache = null; /* 详情态的数据(从编辑态「返回」时用它重画) */
|
||||||
|
|
||||||
|
function M() { if (!modal) { modal = document.getElementById('fbEntryModal'); } return modal; }
|
||||||
|
function bodyEl() { return document.getElementById('fbEntryBody'); }
|
||||||
|
function titleEl(){ return document.getElementById('fbEntryTitle'); }
|
||||||
|
function tipEl() { return document.getElementById('fbEntryTip'); }
|
||||||
|
function byId(id) { return document.getElementById(id); }
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"').replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTip(msg, err) {
|
||||||
|
var t = tipEl();
|
||||||
|
if (!t) { return; }
|
||||||
|
t.textContent = msg || '';
|
||||||
|
t.className = 'cal-modal-tip' + (err ? ' err' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 底栏两种形态 --------------------------------------------------------- */
|
||||||
|
function footDetail() {
|
||||||
|
['fbEntryDetail', 'fbEntryEdit'].forEach(function (id) {
|
||||||
|
var e = byId(id); if (e) { e.hidden = false; }
|
||||||
|
});
|
||||||
|
['fbEntryBack', 'fbEntrySave'].forEach(function (id) {
|
||||||
|
var e = byId(id); if (e) { e.hidden = true; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function footEdit() {
|
||||||
|
['fbEntryDetail', 'fbEntryEdit'].forEach(function (id) {
|
||||||
|
var e = byId(id); if (e) { e.hidden = true; }
|
||||||
|
});
|
||||||
|
['fbEntryBack', 'fbEntrySave'].forEach(function (id) {
|
||||||
|
var e = byId(id); if (e) { e.hidden = false; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function footNone() { /* 载入中 / 出错:全都先藏起来 */
|
||||||
|
['fbEntryDetail', 'fbEntryEdit', 'fbEntryBack', 'fbEntrySave'].forEach(function (id) {
|
||||||
|
var e = byId(id); if (e) { e.hidden = true; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 详情态渲染 ----------------------------------------------------------- */
|
||||||
|
function rowsHtml(list) {
|
||||||
|
var html = '';
|
||||||
|
(list || []).forEach(function (r) {
|
||||||
|
if (!r || r[1] === '' || r[1] == null) { return; }
|
||||||
|
html += '<div class="kv-row"><dt>' + esc(r[0]) + '</dt><dd>' + esc(r[1]) + '</dd></div>';
|
||||||
|
});
|
||||||
|
return html || '<p class="empty-hint">这条没有更多信息。</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintDetail(data) {
|
||||||
|
if (titleEl()) { titleEl().textContent = data.title || '详情'; }
|
||||||
|
if (bodyEl()) { bodyEl().innerHTML = '<dl class="kv">' + rowsHtml(data.rows) + '</dl>'; }
|
||||||
|
var det = byId('fbEntryDetail');
|
||||||
|
if (det) {
|
||||||
|
if (data.detail) { det.href = data.detail; det.hidden = false; }
|
||||||
|
else { det.hidden = true; }
|
||||||
|
}
|
||||||
|
var ed = byId('fbEntryEdit');
|
||||||
|
if (ed) { ed.disabled = false; ed.hidden = !data.edit; }
|
||||||
|
// 「编辑」不可用时别把它留在底栏上(否则点了没反应)
|
||||||
|
if (!data.edit) { var e2 = byId('fbEntryEdit'); if (e2) { e2.hidden = true; } }
|
||||||
|
setTip('');
|
||||||
|
footDetail();
|
||||||
|
if (!data.edit) { var e3 = byId('fbEntryEdit'); if (e3) { e3.hidden = true; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreDetail() {
|
||||||
|
if (!detailCache) { return; }
|
||||||
|
var d = detailCache;
|
||||||
|
paintDetail(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 打开 / 关闭 ---------------------------------------------------------- */
|
||||||
|
function setOpen(on) {
|
||||||
|
var m = M();
|
||||||
|
if (!m) { return; }
|
||||||
|
m.hidden = !on;
|
||||||
|
if (!on) { detailCache = null; if (bodyEl()) { bodyEl().innerHTML = ''; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
function antdOpen() {
|
||||||
|
var d = document.querySelector('.ant-picker-dropdown');
|
||||||
|
return !!(d && !d.classList.contains('ant-picker-dropdown-hidden'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 编辑片段:取回 → 剥壳 → 塞进正文 → 补跑初始化 ------------------------ */
|
||||||
|
/* innerHTML 不执行脚本(v42 的血泪结论)⇒ 抠出来重跑。 */
|
||||||
|
function runInlineScripts(container, html) {
|
||||||
|
var re = /<script\b[^>]*>([\s\S]*?)<\/script>/gi, m, code = '';
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
if (!/\bsrc\s*=/i.test(m[0].split('>')[0])) { code += m[1] + '\n;\n'; }
|
||||||
|
}
|
||||||
|
if (!code) { return; }
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.textContent = code;
|
||||||
|
(container || document.body).appendChild(s);
|
||||||
|
s.parentNode.removeChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 片段可能是三种形态:整页(带 layout)/只带弹框壳/只有表单。
|
||||||
|
* 我们要的是**表单本体** —— 弹框壳必须剥掉,否则正文里又多一层遮罩。
|
||||||
|
*/
|
||||||
|
function pickForm(html) {
|
||||||
|
var tmp = document.createElement('div');
|
||||||
|
tmp.innerHTML = html;
|
||||||
|
var form = tmp.querySelector('#anForm, #mvForm, #nodeForm, #tripForm, #calEditForm, form');
|
||||||
|
if (form) { return { form: form, html: html }; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadEdit(url) {
|
||||||
|
if (!url) { setTip('这条没有可编辑的表单。', true); return; }
|
||||||
|
setTip('');
|
||||||
|
footNone();
|
||||||
|
if (titleEl()) { titleEl().textContent = '载入中…'; }
|
||||||
|
if (bodyEl()) { bodyEl().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
||||||
|
if (!window.fetch) { window.location.href = url; return; }
|
||||||
|
fetch(url, { credentials: 'same-origin',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(function (r) { return r.text(); })
|
||||||
|
.then(function (html) {
|
||||||
|
if (!bodyEl()) { return; }
|
||||||
|
var picked = pickForm(html);
|
||||||
|
if (!picked) { window.location.href = url; return; }
|
||||||
|
bodyEl().innerHTML = '';
|
||||||
|
bodyEl().appendChild(picked.form);
|
||||||
|
// 片段里的初始化脚本 + 宿主这侧的通用升级(日期 / antd / 选人 / 上传 / 行程表单)
|
||||||
|
runInlineScripts(bodyEl(), picked.html);
|
||||||
|
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||||
|
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||||
|
if (window.fbNodeForm) { window.fbNodeForm(bodyEl()); }
|
||||||
|
if (typeof window.fbLoadPicker === 'function') { window.fbLoadPicker(); }
|
||||||
|
if (typeof window.fbInitPickers === 'function') { window.fbInitPickers(bodyEl()); }
|
||||||
|
// 标题取表单片段里的自我描述(各模板的弹框头)
|
||||||
|
var h = picked.form.querySelector('.cal-modal-head b')
|
||||||
|
|| (html.match(/aria-label="([^"]*编辑[^"]*)"/) || [])[1];
|
||||||
|
if (titleEl()) { titleEl().textContent = h ? String(h).trim() : '编辑'; }
|
||||||
|
footEdit();
|
||||||
|
var f = bodyEl().querySelector('#nf-title, #an-title, #ev-title, #title, input[name="title"]');
|
||||||
|
if (f) { f.focus(); }
|
||||||
|
})
|
||||||
|
.catch(function () { window.location.href = url; });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 编辑态「保存」= 原生提交那张表单(服务端 update 路由会跳转,刷新到新页)。 */
|
||||||
|
function submitEdit() {
|
||||||
|
var form = bodyEl() && (bodyEl().querySelector('form#anForm, form#mvForm, form#nodeForm, form#tripForm, form#calEditForm')
|
||||||
|
|| bodyEl().querySelector('form'));
|
||||||
|
if (!form) { setTip('找不到可保存的表单。', true); return; }
|
||||||
|
form.setAttribute('data-fb-entry-submit', '1');
|
||||||
|
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||||
|
else { form.submit(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 打开一条 ------------------------------------------------------------- */
|
||||||
|
function openEntry(el) {
|
||||||
|
var raw = el.getAttribute('data-fb-entry') || '';
|
||||||
|
var data;
|
||||||
|
try { data = JSON.parse(raw); } catch (e) { return; } /* 数据坏了就别拦点击 */
|
||||||
|
if (!data || typeof data !== 'object') { return; }
|
||||||
|
detailCache = data;
|
||||||
|
paintDetail(data);
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var t = e.target;
|
||||||
|
if (!t || !t.closest) { return; }
|
||||||
|
|
||||||
|
if (t.closest('[data-fb-entry-close]')) { e.preventDefault(); setOpen(false); return; }
|
||||||
|
if (t.closest('#fbEntryEdit')) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (detailCache && detailCache.edit) { loadEdit(detailCache.edit); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t.closest('#fbEntryBack')) { e.preventDefault(); restoreDetail(); return; }
|
||||||
|
if (t.closest('#fbEntrySave')) { e.preventDefault(); submitEdit(); return; }
|
||||||
|
|
||||||
|
var item = t.closest('[data-fb-entry]');
|
||||||
|
if (item) { e.preventDefault(); openEntry(item); return; }
|
||||||
|
|
||||||
|
var m = M();
|
||||||
|
if (m && t === m) { setOpen(false); } /* 点遮罩关;点面板内不关 */
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key !== 'Escape') { return; }
|
||||||
|
var m = M();
|
||||||
|
if (!m || m.hidden) { return; }
|
||||||
|
if (antdOpen()) { return; } /* 让 antd 先收它自己的面板 */
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 编辑态里表单用回车提交时,别让浮层的 Esc 逻辑挡住(默认行为即可)——
|
||||||
|
这里不再另挂 submit 监听:原生提交就是要它跳走。 */
|
||||||
|
|
||||||
|
window.fbEntryOpen = openEntry;
|
||||||
|
window.fbEntryClose = function () { setOpen(false); };
|
||||||
|
})();
|
||||||
@@ -83,6 +83,14 @@
|
|||||||
|
|
||||||
function init(root) {
|
function init(root) {
|
||||||
var id = root.getAttribute('data-pp') || '';
|
var id = root.getAttribute('data-pp') || '';
|
||||||
|
/* 🔴 v43:**幂等闸门**。组件现在可能在"动态插入"之后被重复 init
|
||||||
|
(见文件末尾 `window.fbInitPickers`)。没有这道闸门会挂上第二套监听:
|
||||||
|
· 打开弹框被处理两次、暂存区 chip 加两次;
|
||||||
|
· `change` 里来回切选中态。
|
||||||
|
⚠️ 事件委托本来就是"一次绑定、永久有效",重复调用只需要它**什么也不做**。
|
||||||
|
⚠️ 标记放在**取到必需元素之后**打 —— 否则"结构不全时提前 return"会把
|
||||||
|
这个组件永久标记成"已初始化",之后再补它也没用了。 */
|
||||||
|
if (root.getAttribute('data-pp-init') === '1') { return; }
|
||||||
var isMulti = root.getAttribute('data-pp-mode') !== 'single';
|
var isMulti = root.getAttribute('data-pp-mode') !== 'single';
|
||||||
var modal = root.querySelector('.pp-modal');
|
var modal = root.querySelector('.pp-modal');
|
||||||
var openBtn = root.querySelector('.pp-open');
|
var openBtn = root.querySelector('.pp-open');
|
||||||
@@ -92,6 +100,8 @@
|
|||||||
var chips = root.querySelector('.pp-chips');
|
var chips = root.querySelector('.pp-chips');
|
||||||
var emptyTip = root.querySelector('.pp-empty');
|
var emptyTip = root.querySelector('.pp-empty');
|
||||||
if (!modal || !list || !chips) { return; }
|
if (!modal || !list || !chips) { return; }
|
||||||
|
/* 🔴 v43:必需元素取齐了才打"已初始化"标记(见上面幂等闸门的说明)。 */
|
||||||
|
root.setAttribute('data-pp-init', '1');
|
||||||
|
|
||||||
// ---- 第 19 批 用户第 8 条:多选弹框隐藏每行前面的复选框 ----
|
// ---- 第 19 批 用户第 8 条:多选弹框隐藏每行前面的复选框 ----
|
||||||
// 用户原话:「弹框选择人员那里,暂存区太小了…前面的复选框就没用了吧,去掉吧。」
|
// 用户原话:「弹框选择人员那里,暂存区太小了…前面的复选框就没用了吧,去掉吧。」
|
||||||
@@ -715,6 +725,30 @@
|
|||||||
Array.prototype.forEach.call(document.querySelectorAll('.pp'), init);
|
Array.prototype.forEach.call(document.querySelectorAll('.pp'), init);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔴🔴 v43(用户 2026-10-11 第 4 条):「在旅程功能里面,编辑一个行程的时候,
|
||||||
|
* 现在"参与的人"添加参与人的那个功能已经不好使了,弹不出选人的框了。」
|
||||||
|
*
|
||||||
|
* 真根因:本组件的绑定只跑一次 —— 脚本加载时 `boot()` 扫描当时的 `.pp`。
|
||||||
|
* 而**详情页的行程弹框**是 AJAX 取片段、`innerHTML` 塞进 DOM 的
|
||||||
|
* ⇒ 那一刻才出现的 `.pp` **从来没被 init 过**
|
||||||
|
* ⇒「+ 添加参与的人」按钮上没有任何 click 监听 ⇒ 点了没反应(弹不出弹框)。
|
||||||
|
* (同一个根因也解释了"暂存区/确定/取消"全都按不动。)
|
||||||
|
*
|
||||||
|
* ⇒ 对外暴露 `window.fbInitPickers(scope)`:任何"动态插入含 `.pp` 的 HTML"之后
|
||||||
|
* 调它一次即可。`init()` 内部有 `data-pp-init` 幂等标记,重复调用安全。
|
||||||
|
*
|
||||||
|
* ⚠️ 本脚本是**按需加载**的(layout.php 里页面上有 `.pp` 才拉),而弹框里的 `.pp`
|
||||||
|
* 是后来才出现的 ⇒ 加载判据可能在脚本拉起时还没看到它。所以宿主调这个钩子时,
|
||||||
|
* 要容忍"函数还不存在"(那时从 `.pp` 到脚本的按需加载链还没走完);
|
||||||
|
* 见 trips/show.php 里那段"等它就绪"的处理。
|
||||||
|
*/
|
||||||
|
window.fbInitPickers = function (scope) {
|
||||||
|
var root = scope || document;
|
||||||
|
Array.prototype.forEach.call(root.querySelectorAll('.pp'), init);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', boot);
|
document.addEventListener('DOMContentLoaded', boot);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2626,13 +2626,24 @@ switch ($r) {
|
|||||||
flash('err', '找不到这条观影记录。');
|
flash('err', '找不到这条观影记录。');
|
||||||
redirect('movies');
|
redirect('movies');
|
||||||
}
|
}
|
||||||
View::render('movies/form', [
|
/* 🔴 v43(用户 2026-10-11 第 6 条):条目详情浮层里点「编辑」时,
|
||||||
'title' => '编辑观影记录',
|
要把它**就地**换成这张编辑表单 —— `?embed=1` 只吐弹框那一块
|
||||||
|
(不套 layout),由浮层塞进正文。与 `trips/edit` 同一套约定。
|
||||||
|
⚠️ 详情页那一路(`movies/show` 内嵌 `$mvModalOnly+$mvModalForce`)
|
||||||
|
继续走原样,不受影响。 */
|
||||||
|
$mvArgs = [
|
||||||
'm' => $m,
|
'm' => $m,
|
||||||
'people' => Person::roster($familyId, (int)Auth::personId(), 'member', ''),
|
'people' => Person::roster($familyId, (int)Auth::personId(), 'member', ''),
|
||||||
'cinemas' => Cinema::all($familyId),
|
'cinemas' => Cinema::all($familyId),
|
||||||
'errors' => [],
|
'errors' => [],
|
||||||
], 'movies');
|
];
|
||||||
|
if ((string)($_GET['embed'] ?? '') === '1') {
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo View::renderPartial('movies/form',
|
||||||
|
$mvArgs + ['mvModalOnly' => true, 'mvModalForce' => true, 'mvOpen' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
View::render('movies/form', ['title' => '编辑观影记录'] + $mvArgs, 'movies');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
/** 收集表单里和观影有关的字段(create / update 共用一份)。 */
|
/** 收集表单里和观影有关的字段(create / update 共用一份)。 */
|
||||||
@@ -4377,8 +4388,23 @@ $want = array_keys($want);
|
|||||||
$pids = Anniversary::peopleOf($editing);
|
$pids = Anniversary::peopleOf($editing);
|
||||||
$editing['person_ids'] = $pids;
|
$editing['person_ids'] = $pids;
|
||||||
}
|
}
|
||||||
View::render('days/form', [
|
/* 🔴🔴 v43(用户 2026-10-11 第 2 条):「新建纪念日的时候,原来的页面还保存着。
|
||||||
'title' => $id > 0 ? '编辑纪念日' : '新建纪念日',
|
但是点击"修改纪念日",怎么好像原来的页面全都没了?就是下面那一层页面没了,
|
||||||
|
这好像不太合理吧?」
|
||||||
|
|
||||||
|
真根因:**「修改」是 `<a href="days/edit">` 整页跳转** —— 它套 layout,
|
||||||
|
渲染出的是"一张只有弹框的空白页",列表页被整个换掉
|
||||||
|
(正是用户说的"下面那一层页面没了")。而「新建」走的是列表页内嵌弹框,
|
||||||
|
两条路观感不一致。
|
||||||
|
|
||||||
|
⇒ 修法:`days/edit` 支持 `?embed=1` —— **只吐弹框那一块**(不套 layout),
|
||||||
|
列表页 JS 拿它替换掉当前 `#anNewCard` 并打开。
|
||||||
|
这样"编辑"与"新建"共用**同一个弹框、同一个底层页面**。
|
||||||
|
(与 `trips/edit` 的 `?embed=1` 完全同一套约定。)
|
||||||
|
⚠️ 内嵌形态标 `$anModalOnly = true`:否则模板会给它打上 `data-an-standalone`,
|
||||||
|
关闭时去 `window.location.href` 跳页 —— 那正是要去掉的行为。
|
||||||
|
⚠️ 非 embed 整页路径**保留**(直接访问 URL / 无 JS 兜底)。 */
|
||||||
|
$anArgs = [
|
||||||
'editing' => $editing,
|
'editing' => $editing,
|
||||||
'kinds' => Anniversary::kinds($familyId),
|
'kinds' => Anniversary::kinds($familyId),
|
||||||
'people' => Person::roster($familyId, $me, 'member', ''),
|
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||||
@@ -4387,7 +4413,15 @@ $want = array_keys($want);
|
|||||||
否则用户看到的是一张空白页(内容全在 hidden 的弹框里)。 */
|
否则用户看到的是一张空白页(内容全在 hidden 的弹框里)。 */
|
||||||
'anModal' => true,
|
'anModal' => true,
|
||||||
'anOpen' => true,
|
'anOpen' => true,
|
||||||
], 'days');
|
];
|
||||||
|
if ((string)($_GET['embed'] ?? '') === '1') {
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo View::renderPartial('days/form',
|
||||||
|
$anArgs + ['anModalOnly' => true, 'anShell' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
View::render('days/form',
|
||||||
|
['title' => $id > 0 ? '编辑纪念日' : '新建纪念日'] + $anArgs, 'days');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'days/save':
|
case 'days/save':
|
||||||
@@ -4912,14 +4946,23 @@ $want = array_keys($want);
|
|||||||
$occ = preg_match('/^\d{4}-\d{2}-\d{2}$/', $occ) ? $occ : '';
|
$occ = preg_match('/^\d{4}-\d{2}-\d{2}$/', $occ) ? $occ : '';
|
||||||
// 编辑页要回填"哪些天被跳过了"(跳过的那次不再出现在日历里)
|
// 编辑页要回填"哪些天被跳过了"(跳过的那次不再出现在日历里)
|
||||||
$ev['skips'] = Calendar::skipsOf((int)$ev['id']);
|
$ev['skips'] = Calendar::skipsOf((int)$ev['id']);
|
||||||
View::render('calendar/event', [
|
/* 🔴 v43(用户 2026-10-11 第 6 条):条目详情浮层里点「编辑」时,
|
||||||
|
`?embed=1` 只吐**编辑表单**(不套 layout、不带页头/删除),
|
||||||
|
由浮层塞进正文。与 days/edit、movies/edit、trips/node_edit 同一套。 */
|
||||||
|
$evArgs = [
|
||||||
'title' => (string)$ev['title'],
|
'title' => (string)$ev['title'],
|
||||||
'ev' => $ev,
|
'ev' => $ev,
|
||||||
'cal' => $cal,
|
'cal' => $cal,
|
||||||
'occ' => $occ,
|
'occ' => $occ,
|
||||||
// ⚠️ 不要再传 'ym':模板自己从 $ev['starts_at'] 算(模板里那行更权威,
|
// ⚠️ 不要再传 'ym':模板自己从 $ev['starts_at'] 算(模板里那行更权威,
|
||||||
// 传进来反而会在"改过日期后"与模板算出的值不一致)。
|
// 传进来反而会在"改过日期后"与模板算出的值不一致)。
|
||||||
], 'calendar');
|
];
|
||||||
|
if ((string)($_GET['embed'] ?? '') === '1') {
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo View::renderPartial('calendar/event', $evArgs + ['evEmbed' => true]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
View::render('calendar/event', $evArgs, 'calendar');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'calendar/event_update':
|
case 'calendar/event_update':
|
||||||
@@ -5557,6 +5600,35 @@ $want = array_keys($want);
|
|||||||
], 'tasks');
|
], 'tasks');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
/* 🔴 v43(用户 2026-10-11 第 6 条):条目详情浮层里点「编辑」→ 取这张表单片段。
|
||||||
|
⚠️ 与 `tasks/show` 共用同一份 `_edit_form.php`(不许各写一张,那必然有一天不同步)。 */
|
||||||
|
case 'tasks/edit':
|
||||||
|
Auth::requireWrite();
|
||||||
|
$familyId = (int)Auth::familyId();
|
||||||
|
$me = (int)Auth::personId();
|
||||||
|
$t = Task::find($familyId, (int)($_GET['id'] ?? 0), $me);
|
||||||
|
if (!$t || !Task::canWrite($t, $me)) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo '这项任务不能改。';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$tkParents = [];
|
||||||
|
foreach (['todo', 'doing', 'done'] as $sk) {
|
||||||
|
foreach (Task::board($familyId, $me)[$sk] as $p) {
|
||||||
|
if ((int)$p['id'] !== (int)$t['id']) { $tkParents[] = $p; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header('Content-Type: text/html; charset=utf-8');
|
||||||
|
echo View::renderPartial('tasks/_edit_form', [
|
||||||
|
't' => $t,
|
||||||
|
'people' => Person::roster($familyId, $me, 'member', ''),
|
||||||
|
'parents' => $tkParents,
|
||||||
|
'boards' => Task::boards($familyId),
|
||||||
|
'categories' => Task::categories($familyId),
|
||||||
|
'tkEditEmbed' => true, // 浮层形态:保存键由浮层底栏给
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
|
||||||
case 'tasks/create':
|
case 'tasks/create':
|
||||||
case 'tasks/update':
|
case 'tasks/update':
|
||||||
$familyId = (int)Auth::familyId();
|
$familyId = (int)Auth::familyId();
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 🔴 v43(用户 2026-10-11 第 6 条)——「条目详情浮层」的**空壳**。
|
||||||
|
*
|
||||||
|
* 用户原话:
|
||||||
|
* 「首页上的日程、首页上的近期任务,以及日程里面的日程,点击它们的时候,
|
||||||
|
* 都会弹出一个框显示更详细的信息。上面有不同的功能按钮,比如说详细信息,
|
||||||
|
* 在日程里面,如果是观影啊、旅程啊,点击详细信息就会进入到观影或者旅程的
|
||||||
|
* 那个相关的页面。如果是日程的话,点击详细信息就进到日程的详细页面。
|
||||||
|
* 当然还可以直接点击编辑,点击编辑不同的来源就进入不同的编辑页面,
|
||||||
|
* 就在当前的页面弹窗,把之前的窗给去掉,然后弹出编辑的那个框来就可以了。」
|
||||||
|
*
|
||||||
|
* ⇒ 三处入口(首页日程 / 首页近期任务 / 日程页条目)都挂同一个浮层,
|
||||||
|
* 内容由 `assets/js/entry_popup.js` 按点击项的 `data-fb-entry` 现填。
|
||||||
|
*
|
||||||
|
* 两种形态(由 JS 切):
|
||||||
|
* ① **详情态**:正文几行「标签:值」;底栏 = 详细信息(跳该来源详情页)+ 编辑
|
||||||
|
* ② **编辑态**:正文换成该来源的编辑表单;底栏 = 返回(回详情态)+ 保存
|
||||||
|
* (保存走**原生提交** —— 四个来源的 update 路由本来就收普通 POST 并跳转,
|
||||||
|
* 不依赖任何页面自带脚本,所以浮层在任何页面都能用)
|
||||||
|
*
|
||||||
|
* ⚠️ 空壳放在 **layout.php** 里(全站一份):三处入口在不同页面,
|
||||||
|
* 放各页面迟早有一个忘了 include —— 那种"某些页面点了没反应"最难查。
|
||||||
|
* ⚠️ 面板里**不放任何服务端内容**:同一页一百条日程也只有一个浮层,开销可忽略。
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
<div class="fb-entry-modal" id="fbEntryModal" role="dialog" aria-modal="true"
|
||||||
|
aria-label="条目详情" hidden>
|
||||||
|
<div class="cal-modal-panel fb-entry-panel" id="fbEntryPanel">
|
||||||
|
<div class="cal-modal-head">
|
||||||
|
<b id="fbEntryTitle">详情</b>
|
||||||
|
<button type="button" class="cal-modal-x" data-fb-entry-close aria-label="关闭">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="cal-modal-body" id="fbEntryBody"></div>
|
||||||
|
<div class="cal-modal-foot" id="fbEntryFoot">
|
||||||
|
<span class="cal-modal-tip" id="fbEntryTip"></span>
|
||||||
|
<?php /* 详情态的两颗 */ ?>
|
||||||
|
<a class="btn btn-ghost btn-sm" id="fbEntryDetail" href="#" hidden>详细信息</a>
|
||||||
|
<button class="btn btn-sm" type="button" id="fbEntryEdit" hidden>编辑</button>
|
||||||
|
<?php /* 编辑态的两颗(默认藏着)—— 「返回」不丢用户填的东西吗?
|
||||||
|
⚠️ 会丢。所以措辞用「返回」,并且在有改动时先问一句(见 JS)。 */ ?>
|
||||||
|
<button class="btn btn-ghost btn-sm" type="button" id="fbEntryBack" hidden>返回</button>
|
||||||
|
<button class="btn btn-sm" type="button" id="fbEntrySave" hidden>保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -17,31 +17,54 @@
|
|||||||
* 用法(include 前设好):
|
* 用法(include 前设好):
|
||||||
* $evFields 正在编辑的日程(新建时传空数组)
|
* $evFields 正在编辑的日程(新建时传空数组)
|
||||||
* $evAllDay 全天的默认值(新建默认**不勾**,用户 2026-10-02 第 6 条)
|
* $evAllDay 全天的默认值(新建默认**不勾**,用户 2026-10-02 第 6 条)
|
||||||
|
* $evMvStyle 🔴 v43:「标签 62px 右对齐 + 冒号 + 控件」的**观影式**排版开关。
|
||||||
|
* 新建日程那个弹框要它(用户:日程新建页排版和观影/行程一样);
|
||||||
|
* 编辑页(`calendar/event.php`)继续用原来的 `.form-grid` 两列。
|
||||||
*/
|
*/
|
||||||
$evF = is_array($evFields ?? null) ? $evFields : [];
|
$evF = is_array($evFields ?? null) ? $evFields : [];
|
||||||
$evAllDayDefault = (bool)($evAllDay ?? false);
|
$evAllDayDefault = (bool)($evAllDay ?? false);
|
||||||
$evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evAllDayDefault;
|
$evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evAllDayDefault;
|
||||||
|
/* 🔴 v43(用户 2026-10-11 第 5 条):观影式排版开关。 */
|
||||||
|
$evMv = !empty($evMvStyle);
|
||||||
|
/* 标签包装:观影式用 `.mv-row` + `.mv-lb`(冒号由 CSS ::after 给);
|
||||||
|
否则用原来的 `.field` + `<label>`。
|
||||||
|
⚠️ 抽成闭包是为了**不在下面每处 if/else 手抄两遍结构** ——
|
||||||
|
抄两遍必然有一天两边不同步(v37 那种"改了 A 忘了 B"的坑)。
|
||||||
|
⚠️ 「必填星号」作为**参数**传(不要用 `use` 捕获 —— 闭包定义时那个变量还没赋值,
|
||||||
|
捕获到的是 null,星号永远不出现)。 */
|
||||||
|
$evRowOpen = static function (string $full = '') use ($evMv): string {
|
||||||
|
return '<div class="' . ($evMv ? 'mv-row' : 'field') . ($full !== '' ? ' ' . $full : '') . '">';
|
||||||
|
};
|
||||||
|
$evLab = static function (string $text, string $for = '', bool $star = false) use ($evMv): string {
|
||||||
|
if ($evMv) {
|
||||||
|
// 观影式:标签是 <span class="mv-lb">,冒号由 CSS ::after 给
|
||||||
|
return '<span class="mv-lb">' . $text . ($star ? '<i>*</i>' : '') . '</span>';
|
||||||
|
}
|
||||||
|
return '<label' . ($for !== '' ? ' for="' . e($for) . '"' : '') . '>' . $text . ($star ? ' *' : '') . '</label>';
|
||||||
|
};
|
||||||
?>
|
?>
|
||||||
<div class="field full">
|
<?php echo $evRowOpen('full'); ?>
|
||||||
<label for="ev-title">标题 *</label>
|
<?= $evLab('标题', 'ev-title', true) ?>
|
||||||
<input class="input" id="ev-title" name="title" required
|
<input class="input" id="ev-title" name="title" required
|
||||||
value="<?= e((string)($evF['title'] ?? '')) ?>" placeholder="如:家长会">
|
value="<?= e((string)($evF['title'] ?? '')) ?>" placeholder="如:家长会">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-location">地点</label>
|
<?= $evLab('地点', 'ev-location') ?>
|
||||||
<input class="input" id="ev-location" name="location"
|
<input class="input" id="ev-location" name="location"
|
||||||
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室">
|
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<?php echo $evRowOpen(); ?>
|
||||||
<label>提醒</label>
|
<?= $evLab('提醒') ?>
|
||||||
<?php /* v24 第 20 批 第 8 条:**多选**(用户原话:「一个日程,我可以选择让它
|
<?php /* v24 第 20 批 第 8 条:**多选**(用户原话:「一个日程,我可以选择让它
|
||||||
5分钟提醒,10分钟提醒等等。」)。
|
5分钟提醒,10分钟提醒等等。」)。
|
||||||
⚠️ 用 checkbox 而不是 `<select multiple>`:后者在手机上要"按住再点",
|
⚠️ 用 checkbox 而不是 `<select multiple>`:后者在手机上要"按住再点",
|
||||||
几乎没法用(任务共享那个 multiple 下拉就是因此被换掉的)。
|
几乎没法用(任务共享那个 multiple 下拉就是因此被换掉的)。
|
||||||
⚠️ 字段名必须是 `reminder_minutes[]` —— Calendar::reminderMinutes() 认得数组。
|
⚠️ 字段名必须是 `reminder_minutes[]` —— Calendar::reminderMinutes() 认得数组。
|
||||||
写成 `reminder_minutes`(不带 [])会让 PHP 只留最后一个勾选。 */
|
写成 `reminder_minutes`(不带 [])会让 PHP 只留最后一个勾选。
|
||||||
|
🔴 v43(用户第 5 条):他要求把这一栏底下那句「多选与不提醒」的说明去掉
|
||||||
|
⇒ 这里删掉那个 `.tip`(多选语义由复选框本身表达)。 */
|
||||||
$curReminders = \App\Calendar::reminderList($evF['reminder_minutes'] ?? null); ?>
|
$curReminders = \App\Calendar::reminderList($evF['reminder_minutes'] ?? null); ?>
|
||||||
<div class="rem-picks">
|
<div class="rem-picks">
|
||||||
<?php foreach (\App\Calendar::REMINDERS as $mins => $word): ?>
|
<?php foreach (\App\Calendar::REMINDERS as $mins => $word): ?>
|
||||||
@@ -52,7 +75,6 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
</label>
|
</label>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
<span class="tip">可以选多个(到点各响一次);一个都不选就是不提醒。</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php /* v24 第 5、6 条:起止各一个「日期 + 时分秒」合在一起的控件(全局统一),
|
<?php /* v24 第 5、6 条:起止各一个「日期 + 时分秒」合在一起的控件(全局统一),
|
||||||
@@ -74,43 +96,58 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
修法:**结束那一端不再默认藏起来**,范围控件一次性就挂上。
|
修法:**结束那一端不再默认藏起来**,范围控件一次性就挂上。
|
||||||
起止成对本来就该一起填。想表达"只有一个时刻"的话,范围控件 clear 一下即可。
|
起止成对本来就该一起填。想表达"只有一个时刻"的话,范围控件 clear 一下即可。
|
||||||
⚠️ 顺带删掉「+ 添加结束时间 / 去掉结束时间」那套开关:
|
⚠️ 顺带删掉「+ 添加结束时间 / 去掉结束时间」那套开关:
|
||||||
它切换显隐会让范围控件反复重挂;范围控件自己就能表达"没有结束"。 */ ?>
|
它切换显隐会让范围控件反复重挂;范围控件自己就能表达"没有结束"。
|
||||||
<div class="dt-range">
|
|
||||||
|
🔴🔴 v43(用户 2026-10-11 第 5 条):「开始和结束时间也没有用我们最新的控件,
|
||||||
|
直接就是四个字"时间范围"。」
|
||||||
|
—— 两处真缺陷一起修:
|
||||||
|
① 这个字段块**没传 `$dtPlugin='antd'`** ⇒ 用户看到的还是年/月/日 + 时/分
|
||||||
|
五个原生下拉,而不是他点名要的 Ant Design 控件;
|
||||||
|
② 标签写的是「时间范围」四个字,没告诉人"这是开始 / 结束"。
|
||||||
|
⇒ `$dtPlugin='antd'`(与观影/行程同一套:面板有可点的时/分列、中文「确定」),
|
||||||
|
标签改「开始 / 结束」(并去掉那颗必填星号的位置歧义 —— 只标在开始上)。
|
||||||
|
⚠️ 契约一字未改:隐藏字段仍是 `*_date/_hour/_minute`;antd 拉不动时
|
||||||
|
自动退回自建下拉(渐进增强)。 */ ?>
|
||||||
|
<div class="dt-range<?= $evMv ? ' mv-row' : '' ?>">
|
||||||
<?php /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
|
<?php /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
|
||||||
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
|
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
|
||||||
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
|
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
|
||||||
<span class="dt-range-label">开始 / 结束 *</span>
|
<?php if ($evMv): ?>
|
||||||
|
<span class="mv-lb dt-range-label">开始 / 结束<i>*</i></span>
|
||||||
|
<?php else: ?>
|
||||||
|
<span class="dt-range-label">开始 / 结束 *</span>
|
||||||
|
<?php endif; ?>
|
||||||
<div class="field ev-dt" data-dt>
|
<div class="field ev-dt" data-dt>
|
||||||
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
||||||
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
|
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
|
||||||
$dtRange = 'start';
|
$dtRange = 'start'; $dtPlugin = 'antd';
|
||||||
require __DIR__ . '/../_datetime.php'; ?>
|
require __DIR__ . '/../_datetime.php'; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="field ev-dt" data-dt id="ev-end-field">
|
<div class="field ev-dt" data-dt id="ev-end-field">
|
||||||
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
|
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
|
||||||
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
|
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
|
||||||
$dtRange = 'end';
|
$dtRange = 'end'; $dtPlugin = 'antd';
|
||||||
require __DIR__ . '/../_datetime.php'; ?>
|
require __DIR__ . '/../_datetime.php'; ?>
|
||||||
</div>
|
</div>
|
||||||
</div><?php /* /dt-range */ ?>
|
</div><?php /* /dt-range */ ?>
|
||||||
<span class="field-hint" style="display:block;margin:-4px 0 10px">
|
<?php /* 🔴 v43(用户第 5 条):他要求把「结束留空即无明确结束」的那条提示去掉
|
||||||
结束留空就表示没有明确的结束(只占开始那一刻/那一天)。
|
⇒ 那句 `.field-hint` 整块删除(不再输出)。 */ ?>
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="field">
|
<?php echo $evRowOpen(); ?>
|
||||||
<label>
|
<label class="chk-inline">
|
||||||
<input type="checkbox" id="ev-allday" name="all_day" value="1"
|
<input type="checkbox" id="ev-allday" name="all_day" value="1"
|
||||||
<?= $evIsAllDay ? 'checked' : '' ?>>
|
<?= $evIsAllDay ? 'checked' : '' ?>>
|
||||||
全天
|
<span>全天</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="field-hint">勾上就不管具体几点,只占这一天</span>
|
<?php /* 🔴 v43(用户第 5 条):他要求把「全天只管日期、不管几点」的说明也去掉
|
||||||
|
⇒ 那句 `.field-hint` 整块删除。 */ ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php /* 用户 2026-10-03 第 5、6 条:重复日程。
|
<?php /* 用户 2026-10-03 第 5、6 条:重复日程。
|
||||||
放在「全天」之后的自然位置:先定"哪一天/几点",再定"要不要重复"。
|
放在「全天」之后的自然位置:先定"哪一天/几点",再定"要不要重复"。
|
||||||
周期用 FREQ 记号,结束条件三档(一直 / N 次 / 到某天),另加"工作日"。 */ ?>
|
周期用 FREQ 记号,结束条件三档(一直 / N 次 / 到某天),另加"工作日"。 */ ?>
|
||||||
<div class="field">
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-freq">重复</label>
|
<?= $evLab('重复', 'ev-freq') ?>
|
||||||
<select class="select" id="ev-freq" name="freq">
|
<select class="select" id="ev-freq" name="freq">
|
||||||
<?php $rp = \App\Repeat::parse((string)($evF['rrule'] ?? '')); ?>
|
<?php $rp = \App\Repeat::parse((string)($evF['rrule'] ?? '')); ?>
|
||||||
<?php foreach (\App\Repeat::FREQS as $k => $w): ?>
|
<?php foreach (\App\Repeat::FREQS as $k => $w): ?>
|
||||||
@@ -119,29 +156,35 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field" id="ev-rep-end-box" hidden>
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-rep-end">结束条件</label>
|
<?= $evLab('结束条件', 'ev-rep-end') ?>
|
||||||
<select class="select" id="ev-rep-end" name="rep_end">
|
<div id="ev-rep-end-box" hidden>
|
||||||
<?php foreach (\App\Repeat::ENDS as $k => $w): ?>
|
<select class="select" id="ev-rep-end" name="rep_end">
|
||||||
<option value="<?= e($k) ?>"<?= $rp['count'] > 0 ? '' : '' ?>><?= e($w) ?></option>
|
<?php foreach (\App\Repeat::ENDS as $k => $w): ?>
|
||||||
<?php endforeach; ?>
|
<option value="<?= e($k) ?>"><?= e($w) ?></option>
|
||||||
</select>
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field" id="ev-rep-count-box" hidden>
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-rep-count">重复次数(含第一次)</label>
|
<?= $evLab('重复次数(含第一次)', 'ev-rep-count') ?>
|
||||||
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
|
<div id="ev-rep-count-box" hidden>
|
||||||
value="<?= (int)($rp['count'] ?: 10) ?>">
|
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
|
||||||
|
value="<?= (int)($rp['count'] ?: 10) ?>">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field" id="ev-rep-until-box" hidden>
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-rep-until">重复到</label>
|
<?= $evLab('重复到', 'ev-rep-until') ?>
|
||||||
<input class="input" type="date" id="ev-rep-until" name="rep_until"
|
<div id="ev-rep-until-box" hidden>
|
||||||
value="<?= e((string)($rp['until'] ?? '')) ?>">
|
<input class="input" type="date" id="ev-rep-until" name="rep_until"
|
||||||
|
value="<?= e((string)($rp['until'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-status">状态</label>
|
<?= $evLab('状态', 'ev-status') ?>
|
||||||
<select class="select" id="ev-status" name="status">
|
<select class="select" id="ev-status" name="status">
|
||||||
<?php $st = (string)($evF['status'] ?? 'confirmed'); ?>
|
<?php $st = (string)($evF['status'] ?? 'confirmed'); ?>
|
||||||
<option value="confirmed"<?= $st === 'confirmed' ? ' selected' : '' ?>>确定</option>
|
<option value="confirmed"<?= $st === 'confirmed' ? ' selected' : '' ?>>确定</option>
|
||||||
@@ -150,8 +193,8 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<?php echo $evRowOpen(); ?>
|
||||||
<label for="ev-transp">这段时间算</label>
|
<?= $evLab('这段时间算', 'ev-transp') ?>
|
||||||
<select class="select" id="ev-transp" name="transp">
|
<select class="select" id="ev-transp" name="transp">
|
||||||
<?php $tp = (int)($evF['transp'] ?? 0); ?>
|
<?php $tp = (int)($evF['transp'] ?? 0); ?>
|
||||||
<option value="0"<?= $tp === 0 ? ' selected' : '' ?>>忙碌</option>
|
<option value="0"<?= $tp === 0 ? ' selected' : '' ?>>忙碌</option>
|
||||||
@@ -159,14 +202,14 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field full">
|
<?php echo $evRowOpen('full'); ?>
|
||||||
<label for="ev-note">备注</label>
|
<?= $evLab('备注', 'ev-note') ?>
|
||||||
<textarea class="input" id="ev-note" name="note" rows="3"
|
<textarea class="input" id="ev-note" name="note" rows="3"
|
||||||
placeholder="要带什么、跟谁一起…"><?= e((string)($evF['note'] ?? '')) ?></textarea>
|
placeholder="要带什么、跟谁一起…"><?= e((string)($evF['note'] ?? '')) ?></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field full">
|
<?php echo $evRowOpen('full'); ?>
|
||||||
<label>附件(可多选)</label>
|
<?= $evLab('附件(可多选)') ?>
|
||||||
<?php /* v24 第 21 批 第 7 条:统一成「点击或拖动进行上传」的框。
|
<?php /* v24 第 21 批 第 7 条:统一成「点击或拖动进行上传」的框。
|
||||||
⚠️ 文件框的 id 保持 `ev-attach`(原来就叫这个)—— e2e 与部署断言里
|
⚠️ 文件框的 id 保持 `ev-attach`(原来就叫这个)—— e2e 与部署断言里
|
||||||
有按 id 定位的写法,改名会把它们静默打成假红。 */
|
有按 id 定位的写法,改名会把它们静默打成假红。 */
|
||||||
@@ -189,7 +232,7 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
sync();
|
sync();
|
||||||
|
|
||||||
/* ⚠️ v24 第 21 批 第 7 条:原来这里有一整段「结束时间显隐 + 跟着开始时间走」的 JS。
|
/* ⚠️ v24 第 21 批 第 7 条:原来这里有一整段「结束时间显隐 + 跟着开始时间走」的 JS。
|
||||||
现在起止两端**始终可见**,并且由 Semi 的范围控件统一接管(一次选完两端),
|
现在起止两端**始终可见**,并且由范围控件统一接管(一次选完两端),
|
||||||
所以那段"同步两端"的逻辑整个删掉 —— 留着不但没用,还会和范围控件抢着改那两个
|
所以那段"同步两端"的逻辑整个删掉 —— 留着不但没用,还会和范围控件抢着改那两个
|
||||||
原生字段(两处都写 → 值互相覆盖)。范围控件选完会自己把两端分别写回原生字段
|
原生字段(两处都写 → 值互相覆盖)。范围控件选完会自己把两端分别写回原生字段
|
||||||
(见 app.js 的 onRange → writeDT),服务端契约不变。 */
|
(见 app.js 的 onRange → writeDT),服务端契约不变。 */
|
||||||
@@ -233,4 +276,5 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?php unset($evF, $evAllDayDefault, $evIsAllDay, $evFields, $evAllDay); ?>
|
<?php unset($evF, $evAllDayDefault, $evIsAllDay, $evFields, $evAllDay, $evMv, $evMvStyle,
|
||||||
|
$evRowOpen, $evLab, $evLabelStar); ?>
|
||||||
|
|||||||
@@ -4,20 +4,31 @@
|
|||||||
*
|
*
|
||||||
* ⚠️ 从月视图点**观影 / 旅程 / 任务**那些条会直接跳到对应模块(它们的"编辑页"在那里),
|
* ⚠️ 从月视图点**观影 / 旅程 / 任务**那些条会直接跳到对应模块(它们的"编辑页"在那里),
|
||||||
* 这里只处理 calendar_events 里真正属于某本日历的自定义日程。
|
* 这里只处理 calendar_events 里真正属于某本日历的自定义日程。
|
||||||
|
*
|
||||||
|
* 🔴 v43(用户 2026-10-11 第 6 条):本模板多了一种用法 —— **`?embed=1` 只吐编辑表单**。
|
||||||
|
* 条目详情浮层里点「编辑」时用它(浮层把正文整块换成这张表单)。
|
||||||
|
* ⇒ `$evEmbed = true` 时:不输出页头、不输出只读 dl、不输出删除表单、
|
||||||
|
* 只保留那张编辑表单(外加已有附件)。
|
||||||
|
* ⚠️ 与 `days/edit` / `trips/node_edit` / `movies/edit` 的 `?embed=1` 同一套约定。
|
||||||
*/
|
*/
|
||||||
$canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
$canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
||||||
|
$evEmbed = !empty($evEmbed);
|
||||||
|
$ym = substr((string)$ev['starts_at'], 0, 7);
|
||||||
?>
|
?>
|
||||||
|
<?php if (!$evEmbed): ?>
|
||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<h1><?= e((string)$ev['title']) ?></h1>
|
<h1><?= e((string)$ev['title']) ?></h1>
|
||||||
<span class="sub"><?= e((string)($ev['starts_at'] ?? '')) ?></span>
|
<span class="sub"><?= e((string)($ev['starts_at'] ?? '')) ?></span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => substr((string)$ev['starts_at'], 0, 7)])) ?>">返回日程</a>
|
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">返回日程</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="card card-pad">
|
<div class="<?= $evEmbed ? '' : 'card card-pad' ?>">
|
||||||
<?php $ym = substr((string)$ev['starts_at'], 0, 7); ?>
|
<?php if ($evEmbed && !$canWrite): ?>
|
||||||
<?php if (!$canWrite): ?>
|
<p class="empty-hint">这本日历你只有只读权限,不能改。</p>
|
||||||
|
<?php elseif (!$canWrite): ?>
|
||||||
<dl class="kv">
|
<dl class="kv">
|
||||||
<div class="kv-row"><dt>开始</dt>
|
<div class="kv-row"><dt>开始</dt>
|
||||||
<dd><?= e(empty($ev['all_day']) ? (string)$ev['starts_at'] : substr((string)$ev['starts_at'], 0, 10)) ?></dd>
|
<dd><?= e(empty($ev['all_day']) ? (string)$ev['starts_at'] : substr((string)$ev['starts_at'], 0, 10)) ?></dd>
|
||||||
@@ -60,7 +71,8 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
|||||||
</dl>
|
</dl>
|
||||||
<p class="empty-hint">这本日历你只有只读权限,看得到但不能改。</p>
|
<p class="empty-hint">这本日历你只有只读权限,看得到但不能改。</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<form method="post" action="<?= e(url('calendar/event_update')) ?>" enctype="multipart/form-data">
|
<form method="post" action="<?= e(url('calendar/event_update')) ?>" enctype="multipart/form-data"
|
||||||
|
id="calEditForm">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
|
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
|
||||||
<input type="hidden" name="ym" value="<?= e($ym) ?>">
|
<input type="hidden" name="ym" value="<?= e($ym) ?>">
|
||||||
@@ -95,12 +107,22 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<?php $evFields = $ev; $evAllDay = false; require __DIR__ . '/_event_fields.php'; ?>
|
<?php /* 🔴 v43(用户第 5 条要求"观影式排版"):浮层里编辑时也用 `.mv-grid`,
|
||||||
|
与"新建日程"那个弹框一致(同一个字段块传 $evMvStyle 即可)。
|
||||||
|
⚠️ 整页形态(老链接直达)保持原来的 `.form-grid` 两列 ——
|
||||||
|
那是"页面表单"的排版习惯,不该跟着浮层一起变。 */
|
||||||
|
$evFields = $ev; $evAllDay = false;
|
||||||
|
if ($evEmbed) { $evMvStyle = true; echo '<div class="mv-grid">'; }
|
||||||
|
require __DIR__ . '/_event_fields.php';
|
||||||
|
if ($evEmbed) { echo '</div>'; unset($evMvStyle); }
|
||||||
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if (!$evEmbed): ?>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn" type="submit">保存</button>
|
<button class="btn" type="submit">保存</button>
|
||||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">取消</a>
|
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">取消</a>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</form>
|
</form>
|
||||||
<?php $as = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
|
<?php $as = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
|
||||||
<?php if ($as): ?>
|
<?php if ($as): ?>
|
||||||
@@ -134,7 +156,9 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
|||||||
*/
|
*/
|
||||||
$evIsRepeat = trim((string)($ev['rrule'] ?? '')) !== '';
|
$evIsRepeat = trim((string)($ev['rrule'] ?? '')) !== '';
|
||||||
?>
|
?>
|
||||||
<?php if ($evIsRepeat && !empty($occ)): ?>
|
<?php if ($evEmbed): ?>
|
||||||
|
<?php /* 浮层形态:删除留给详情页(那里有重复日程的三档删除),这里不出。 */ ?>
|
||||||
|
<?php elseif ($evIsRepeat && !empty($occ)): ?>
|
||||||
<form method="post" action="<?= e(url('calendar/event_del')) ?>" class="ev-del ev-del-scope"
|
<form method="post" action="<?= e(url('calendar/event_del')) ?>" class="ev-del ev-del-scope"
|
||||||
onsubmit="return confirm('按上面选的范围删除?')">
|
onsubmit="return confirm('按上面选的范围删除?')">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
@@ -171,3 +195,4 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php unset($evEmbed, $evIsRepeat, $evIsAllDay, $evAllDay, $evFields, $evMvStyle, $ym, $occ); ?>
|
||||||
|
|||||||
@@ -216,7 +216,13 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
</div>
|
</div>
|
||||||
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
|
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
|
||||||
新建时「全天」默认**不勾**(用户原话)。开始时间默认:
|
新建时「全天」默认**不勾**(用户原话)。开始时间默认:
|
||||||
带了 new_at 就用它(点日历的那一格),否则今天 09:00。 */ ?>
|
带了 new_at 就用它(点日历的那一格),否则今天 09:00。
|
||||||
|
🔴 v43(用户 2026-10-11 第 5 条):「新建日程页面的排版还是不对,
|
||||||
|
也是一样的:把标题放在前面,后面加冒号,再跟上输入框。」
|
||||||
|
⇒ 用观影/行程那套 `.mv-grid`(标签 62px 右对齐 + CSS ::after 冒号 +
|
||||||
|
控件占满剩余宽度)包住字段块。
|
||||||
|
`$evMvStyle = true` 让字段块输出 `.mv-row` / `.mv-lb`
|
||||||
|
而不是 `.field` / `<label>`;编辑页不传,保持原两列。 */ ?>
|
||||||
<?php
|
<?php
|
||||||
// 点的位置优先;没点就"月视图当月 → 该月 1 号 / 本月 → 今天"的 09:00
|
// 点的位置优先;没点就"月视图当月 → 该月 1 号 / 本月 → 今天"的 09:00
|
||||||
$defDay = $newAt !== '' ? substr($newAt, 0, 10)
|
$defDay = $newAt !== '' ? substr($newAt, 0, 10)
|
||||||
@@ -227,7 +233,11 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
|
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
|
||||||
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
|
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
|
||||||
$evAllDay = false;
|
$evAllDay = false;
|
||||||
|
$evMvStyle = true; // 观影式排版(用户第 5 条)
|
||||||
|
echo '<div class="mv-grid">';
|
||||||
require __DIR__ . '/_event_fields.php';
|
require __DIR__ . '/_event_fields.php';
|
||||||
|
echo '</div>';
|
||||||
|
unset($evMvStyle);
|
||||||
?>
|
?>
|
||||||
</div><?php /* /cal-modal-body */ ?>
|
</div><?php /* /cal-modal-body */ ?>
|
||||||
<div class="cal-modal-foot">
|
<div class="cal-modal-foot">
|
||||||
@@ -273,7 +283,17 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
<?php foreach (($cols ?? []) as $i => $c): ?>
|
<?php foreach (($cols ?? []) as $i => $c): ?>
|
||||||
<div class="tg-adcell">
|
<div class="tg-adcell">
|
||||||
<?php foreach (($tgAll[$i] ?? []) as $ev): ?>
|
<?php foreach (($tgAll[$i] ?? []) as $ev): ?>
|
||||||
|
<?php
|
||||||
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||||
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||||
|
$evEdit = '';
|
||||||
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||||
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
?>
|
||||||
<a class="cal-ev is-allday" data-cal="<?= (int)$ev['cal'] ?>"
|
<a class="cal-ev is-allday" data-cal="<?= (int)$ev['cal'] ?>"
|
||||||
|
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||||
style="background: <?= e((string)$ev['color']) ?>;"
|
style="background: <?= e((string)$ev['color']) ?>;"
|
||||||
href="<?= e((string)$ev['href']) ?>"
|
href="<?= e((string)$ev['href']) ?>"
|
||||||
title="<?= e((string)$ev['title']) ?>"><?= e((string)$ev['title']) ?></a>
|
title="<?= e((string)$ev['title']) ?>"><?= e((string)$ev['title']) ?></a>
|
||||||
@@ -322,7 +342,23 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
列数 1/2/3/4 是运行时才知道的,类名会爆炸;
|
列数 1/2/3/4 是运行时才知道的,类名会爆炸;
|
||||||
而且模板已经在内联 top/height 了,同一套坐标系最不容易错。 */ ?>
|
而且模板已经在内联 top/height 了,同一套坐标系最不容易错。 */ ?>
|
||||||
<?php foreach (($tgrid['timed'][$i] ?? []) as $ev): ?>
|
<?php foreach (($tgrid['timed'][$i] ?? []) as $ev): ?>
|
||||||
|
<?php /* 🔴 v43(用户第 6 条):点条目弹详情浮层(不再直接跳页)。
|
||||||
|
数据由 fb_entry_json() 统一编(三个入口共用一个函数)。
|
||||||
|
「详细信息」跳该来源的详情页;「编辑」取该来源的编辑表单片段。
|
||||||
|
⚠️ 编辑地址一律带可写判断:只读的(农历/节假日)不给「编辑」。 */ ?>
|
||||||
|
<?php
|
||||||
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||||
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||||
|
$evDet = (string)($ev['href'] ?? '');
|
||||||
|
$evEdit = '';
|
||||||
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||||
|
// trip 的条目指向"某个旅程里的某段行程",编辑器在旅程详情页里 —— 这里不给「编辑」
|
||||||
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
?>
|
||||||
<a class="tg-block" data-cal="<?= (int)$ev['cal'] ?>"
|
<a class="tg-block" data-cal="<?= (int)$ev['cal'] ?>"
|
||||||
|
data-fb-entry='<?= fb_entry_json($ev, $evDet, $evEdit) ?>'
|
||||||
style="background: <?= e((string)$ev['color']) ?>;
|
style="background: <?= e((string)$ev['color']) ?>;
|
||||||
top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;
|
top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;
|
||||||
left: calc(2px + <?= (float)($ev['left'] ?? 0) ?>%);
|
left: calc(2px + <?= (float)($ev['left'] ?? 0) ?>%);
|
||||||
@@ -357,8 +393,18 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
<div class="cal-evs">
|
<div class="cal-evs">
|
||||||
<?php $n = 0; foreach ($cell['events'] as $ev): $n++; ?>
|
<?php $n = 0; foreach ($cell['events'] as $ev): $n++; ?>
|
||||||
<?php if ($n <= 3): ?>
|
<?php if ($n <= 3): ?>
|
||||||
|
<?php
|
||||||
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||||
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||||
|
$evEdit = '';
|
||||||
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||||
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
?>
|
||||||
<a class="cal-ev<?= empty($ev['all_day']) ? '' : ' is-allday' ?>"
|
<a class="cal-ev<?= empty($ev['all_day']) ? '' : ' is-allday' ?>"
|
||||||
data-cal="<?= (int)$ev['cal'] ?>"
|
data-cal="<?= (int)$ev['cal'] ?>"
|
||||||
|
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||||
style="background: <?= e((string)$ev['color']) ?>;"
|
style="background: <?= e((string)$ev['color']) ?>;"
|
||||||
href="<?= e((string)$ev['href']) ?>"
|
href="<?= e((string)$ev['href']) ?>"
|
||||||
title="<?= e((string)$ev['title']) ?>">
|
title="<?= e((string)$ev['title']) ?>">
|
||||||
@@ -409,7 +455,7 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
|||||||
月视图:一格 = 一天 → 开始时间用那天 09:00("当日默认"比 00:00 更像个日程)。
|
月视图:一格 = 一天 → 开始时间用那天 09:00("当日默认"比 00:00 更像个日程)。
|
||||||
周/日视图:按鼠标在**那一列里的纵向位置**反算时刻,最接近的半小时 ——
|
周/日视图:按鼠标在**那一列里的纵向位置**反算时刻,最接近的半小时 ——
|
||||||
点在"下午三点那一段"就该得到 15:00,这是"点哪儿建哪儿"的全部意义。 */
|
点在"下午三点那一段"就该得到 15:00,这是"点哪儿建哪儿"的全部意义。 */
|
||||||
var startField = document.querySelector('.field.ev-dt[data-dt]');
|
var startField = document.querySelector('.ev-dt[data-dt]');
|
||||||
var startDate = startField ? startField.querySelector('input.dt-date') : null;
|
var startDate = startField ? startField.querySelector('input.dt-date') : null;
|
||||||
var startHour = startField ? startField.querySelector('select.dt-hour') : null;
|
var startHour = startField ? startField.querySelector('select.dt-hour') : null;
|
||||||
var startMin = startField ? startField.querySelector('select.dt-minute') : null;
|
var startMin = startField ? startField.querySelector('select.dt-minute') : null;
|
||||||
|
|||||||
+18
-3
@@ -127,9 +127,24 @@ $backUrl = url('days');
|
|||||||
<div class="mv-row">
|
<div class="mv-row">
|
||||||
<span class="mv-lb">日期<i>*</i></span>
|
<span class="mv-lb">日期<i>*</i></span>
|
||||||
<div class="dt-input dt-antd-single" data-antd-date="1" id="an-date-wrap">
|
<div class="dt-input dt-antd-single" data-antd-date="1" id="an-date-wrap">
|
||||||
<input type="date" class="dt-date" id="an-date" name="an_date"
|
<?php /* 🔴🔴 v43(用户 2026-10-11 第 1 条):「日期那里多了一个控件,
|
||||||
value="<?= e(sprintf('%04d-%02d-%02d', (int)$v('year', (int)date('Y')), (int)$v('month', 1), (int)$v('day', 1))) ?>"
|
只保留我们最新的控件就行,之前那种控件删除。」
|
||||||
aria-label="日期">
|
|
||||||
|
真根因:v42 加的那条 CSS 藏原生框的规则写的是
|
||||||
|
`.dt-input.is-antd > .dt-native { …clip… }`
|
||||||
|
—— 它认的是 `.dt-native` 这一层包装。而这里当初**没包这一层**,
|
||||||
|
原生 `<input type="date">` 是 `.dt-input` 的**直接子元素**
|
||||||
|
⇒ 选择器不命中 ⇒ AntD 挂上后,**原生日期框与 AntD 控件并排都在**
|
||||||
|
(用户看到的"多了一个控件")。
|
||||||
|
|
||||||
|
⇒ 修法:补上 `.dt-native` 包装(与 `_datetime.php` 的结构一致),
|
||||||
|
那条 CSS 立刻生效。顺带把原生框**视觉收起**(值载体留着),
|
||||||
|
并在下面补一条**兜底 CSS**(万一哪天结构又变,直接子元素也能收)。 */ ?>
|
||||||
|
<span class="dt-native">
|
||||||
|
<input type="date" class="dt-date" id="an-date" name="an_date"
|
||||||
|
value="<?= e(sprintf('%04d-%02d-%02d', (int)$v('year', (int)date('Y')), (int)$v('month', 1), (int)$v('day', 1))) ?>"
|
||||||
|
aria-label="日期">
|
||||||
|
</span>
|
||||||
<input type="hidden" id="an-month" name="month" value="<?= (int)$v('month', 1) ?>">
|
<input type="hidden" id="an-month" name="month" value="<?= (int)$v('month', 1) ?>">
|
||||||
<input type="hidden" id="an-day" name="day" value="<?= (int)$v('day', 1) ?>">
|
<input type="hidden" id="an-day" name="day" value="<?= (int)$v('day', 1) ?>">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -98,7 +98,17 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
|||||||
→ 改 = `.btn`(主按钮的**蓝**),删 = `.btn-warn`(**黄**实心)。
|
→ 改 = `.btn`(主按钮的**蓝**),删 = `.btn-warn`(**黄**实心)。
|
||||||
⚠️ 原来用的是 `.link-quiet`(纯文字链接)+ `.btn-ghost`(白底描边),
|
⚠️ 原来用的是 `.link-quiet`(纯文字链接)+ `.btn-ghost`(白底描边),
|
||||||
两者都"看不出来是什么动作",与观影/旅程行上那套不一致。 */ ?>
|
两者都"看不出来是什么动作",与观影/旅程行上那套不一致。 */ ?>
|
||||||
<a class="btn btn-sm" href="<?= e(url('days/edit', ['id' => (int)$r['id']])) ?>">修改</a>
|
<?php /* 🔴 v43(用户 2026-10-11 第 2 条):「点击"修改纪念日",
|
||||||
|
怎么好像原来的页面全没了?就是下面那一层页面没了,
|
||||||
|
这好像不太合理吧?」
|
||||||
|
⇒ 「修改」不再整页跳 `days/edit`,改成**在列表页之上开同一个弹框**
|
||||||
|
(与「新建」完全一致的观感)。
|
||||||
|
⚠️ 保留 `data-href` + `href`:JS 没跑时它仍是可直达的链接
|
||||||
|
(无 JS 兜底,行为等同旧版整页编辑)。 */
|
||||||
|
?>
|
||||||
|
<a class="btn btn-sm" data-an-edit
|
||||||
|
data-href="<?= e(url('days/edit', ['id' => (int)$r['id'], 'embed' => 1])) ?>"
|
||||||
|
href="<?= e(url('days/edit', ['id' => (int)$r['id']])) ?>">修改</a>
|
||||||
<form method="post" action="<?= e(url('days/destroy')) ?>" class="res-inline">
|
<form method="post" action="<?= e(url('days/destroy')) ?>" class="res-inline">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
<input type="hidden" name="id" value="<?= (int)$r['id'] ?>">
|
||||||
@@ -169,10 +179,54 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
|||||||
if (t) { t.focus(); }
|
if (t) { t.focus(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* 🔴 v43(用户 2026-10-11 第 2 条):编辑也走这个弹框。
|
||||||
|
把 `?embed=1` 吐回来的那块(**含弹框壳**)替换掉当前 `#anNewCard`,
|
||||||
|
于是"编辑"与"新建"在视觉与交互上是同一个弹框,底层列表页不动。
|
||||||
|
⚠️ 替换后必须**重跑片段自带的内联脚本** —— `innerHTML` 不执行脚本
|
||||||
|
(v42 踩过的坑,`<script>` 既不执行也连标签都不保留)。
|
||||||
|
那两段(关闭自身 / 日期拆分)都要重跑;它们各自有幂等判断,
|
||||||
|
重跑不会重复绑定。 */
|
||||||
|
function runInlineScripts(container, html) {
|
||||||
|
var re = /<script\b[^>]*>([\s\S]*?)<\/script>/gi, m, code = '';
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
if (!/\bsrc\s*=/i.test(m[0].split('>')[0])) { code += m[1] + '\n;\n'; }
|
||||||
|
}
|
||||||
|
if (!code) { return; }
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.textContent = code;
|
||||||
|
(container || document.body).appendChild(s);
|
||||||
|
s.parentNode.removeChild(s);
|
||||||
|
}
|
||||||
|
function loadEdit(url) {
|
||||||
|
var c = card();
|
||||||
|
if (!c || !window.fetch) { if (url) { window.location.href = url; } return; }
|
||||||
|
c.hidden = false; // 先打开(显示"载入中"占位)
|
||||||
|
var lb = c.querySelector('#anNewCardTitle, .cal-modal-head b');
|
||||||
|
if (lb) { lb.textContent = '编辑纪念日'; }
|
||||||
|
fetch(url, { credentials: 'same-origin',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
|
||||||
|
.then(function (r) { return r.text(); })
|
||||||
|
.then(function (html) {
|
||||||
|
var tmp = document.createElement('div');
|
||||||
|
tmp.innerHTML = html;
|
||||||
|
var fresh = tmp.querySelector('#anNewCard');
|
||||||
|
if (!fresh) { window.location.href = url.replace(/[?&]embed=1/, ''); return; }
|
||||||
|
c.parentNode.replaceChild(fresh, c);
|
||||||
|
runInlineScripts(fresh, html);
|
||||||
|
setOpen(true);
|
||||||
|
})
|
||||||
|
.catch(function () { window.location.href = url.replace(/[?&]embed=1/, ''); });
|
||||||
|
}
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
var t = e.target;
|
var t = e.target;
|
||||||
if (!t || !t.closest) { return; }
|
if (!t || !t.closest) { return; }
|
||||||
if (t.closest('#anNewBtn')) { e.preventDefault(); setOpen(true); return; }
|
if (t.closest('#anNewBtn')) { e.preventDefault(); setOpen(true); return; }
|
||||||
|
var ed = t.closest('[data-an-edit]');
|
||||||
|
if (ed) {
|
||||||
|
e.preventDefault();
|
||||||
|
loadEdit(ed.getAttribute('data-href') || '');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (t.closest('#anNewX') || t.closest('#anNewCancel')) { setOpen(false); return; }
|
if (t.closest('#anNewX') || t.closest('#anNewCancel')) { setOpen(false); return; }
|
||||||
var c = card();
|
var c = card();
|
||||||
if (c && t === c) { setOpen(false); }
|
if (c && t === c) { setOpen(false); }
|
||||||
|
|||||||
+34
-1
@@ -275,7 +275,18 @@ unset($evRef);
|
|||||||
<?php if ($allDay): ?>
|
<?php if ($allDay): ?>
|
||||||
<div class="ag-allday">
|
<div class="ag-allday">
|
||||||
<?php foreach ($allDay as $ev): ?>
|
<?php foreach ($allDay as $ev): ?>
|
||||||
<a class="ag-allday-item" href="<?= e((string)$ev['href']) ?>">
|
<?php /* 🔴 v43(用户第 6 条):点条目弹详情浮层(不直接跳页)。 */ ?>
|
||||||
|
<?php
|
||||||
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||||
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||||
|
$evEdit = '';
|
||||||
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||||
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
?>
|
||||||
|
<a class="ag-allday-item" href="<?= e((string)$ev['href']) ?>"
|
||||||
|
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'>
|
||||||
<span class="dash-dot" style="background:<?= e((string)$ev['color']) ?>;"></span>
|
<span class="dash-dot" style="background:<?= e((string)$ev['color']) ?>;"></span>
|
||||||
<span class="ag-ad-t">全天</span>
|
<span class="ag-ad-t">全天</span>
|
||||||
<span class="ag-ad-w"><?= e((string)$ev['title']) ?></span>
|
<span class="ag-ad-w"><?= e((string)$ev['title']) ?></span>
|
||||||
@@ -329,8 +340,18 @@ unset($evRef);
|
|||||||
$endMin = min($endMin, $endH * 60);
|
$endMin = min($endMin, $endH * 60);
|
||||||
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
|
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
|
||||||
$hPct = max(2.2, $hPct); // 至少能塞下一行
|
$hPct = max(2.2, $hPct); // 至少能塞下一行
|
||||||
|
?>
|
||||||
|
<?php
|
||||||
|
$evKind = (string)($ev['kind'] ?? 'custom');
|
||||||
|
$evRef = (int)($ev['ref_id'] ?? 0);
|
||||||
|
$evEdit = '';
|
||||||
|
if ($evKind === 'custom' && $evRef > 0) { $evEdit = url('calendar/event', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'movie' && $evRef > 0) { $evEdit = url('movies/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
|
elseif ($evKind === 'task' && $evRef > 0) { $evEdit = url('tasks/edit', ['id' => $evRef]); }
|
||||||
|
elseif ($evKind === 'anniv' && $evRef > 0) { $evEdit = url('days/edit', ['id' => $evRef, 'embed' => 1]); }
|
||||||
?>
|
?>
|
||||||
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
|
<a class="ag-ev" href="<?= e((string)$ev['href']) ?>"
|
||||||
|
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||||
style="background: <?= e((string)$ev['color']) ?>;
|
style="background: <?= e((string)$ev['color']) ?>;
|
||||||
top: <?= number_format($topPct, 2) ?>%;
|
top: <?= number_format($topPct, 2) ?>%;
|
||||||
height: <?= number_format($hPct, 2) ?>%;
|
height: <?= number_format($hPct, 2) ?>%;
|
||||||
@@ -416,7 +437,19 @@ unset($evRef);
|
|||||||
require __DIR__ . '/_task_check.php';
|
require __DIR__ . '/_task_check.php';
|
||||||
?>
|
?>
|
||||||
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--tk-c)') ?>;"></span>
|
<span class="dash-dot" style="background: <?= e($late ? 'var(--red)' : 'var(--tk-c)') ?>;"></span>
|
||||||
|
<?php /* 🔴 v43(用户第 6 条):点任务标题也弹详情浮层(不直接跳页)。
|
||||||
|
要点在**标题那个 <a>** 上而不是整行 ——
|
||||||
|
行里还有复选框(勾一下完成)与其它链接,
|
||||||
|
整行可点会让"勾选"和"打开"互相抢。 */ ?>
|
||||||
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
<a class="dash-what" href="<?= e(url('tasks/show', ['id' => (int)$t['id']])) ?>"
|
||||||
|
data-fb-entry='<?= fb_entry_json([
|
||||||
|
'title' => (string)$t['title'],
|
||||||
|
'status_word' => \App\Task::statusWord((string)($t['status'] ?? 'todo')),
|
||||||
|
'note' => (string)($t['note'] ?? ''),
|
||||||
|
], url('tasks/show', ['id' => (int)$t['id']]),
|
||||||
|
\App\Auth::canWrite() ? url('tasks/edit', ['id' => (int)$t['id']]) : '',
|
||||||
|
[['截止', ($t['due_at'] ?? '') !== '' ? substr((string)$t['due_at'], 0, 16) : ''],
|
||||||
|
['看板', trim((string)($t['board_name'] ?? ''))]]) ?>'
|
||||||
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
style="color: <?= e($late ? 'var(--red)' : $tkColor) ?>;">
|
||||||
<?= e((string)$t['title']) ?>
|
<?= e((string)$t['title']) ?>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
+40
-6
@@ -207,7 +207,19 @@ $isLoggedIn = \App\Auth::check();
|
|||||||
<button type="button" class="pv-nav pv-next" aria-label="下一张" hidden>›</button>
|
<button type="button" class="pv-nav pv-next" aria-label="下一张" hidden>›</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php /* 🔴 v43(用户 2026-10-11 第 6 条):**条目详情浮层**(全站一份)。
|
||||||
|
首页的日程 / 首页的近期任务 / 日程页的条目 —— 点它们不再直接跳页,
|
||||||
|
而是弹这一个浮层(详情 + 「详细信息」跳转 + 「编辑」就地换编辑表单)。
|
||||||
|
⚠️ 空壳放这里(不放各页面):「三处入口都在不同页面」,
|
||||||
|
放页面里迟早有一个忘了 include —— 那种"某些页面点了没反应"最难查。
|
||||||
|
⚠️ 内容全是 JS 按点击项的 `data-fb-entry` 现填,同一页一百条也只有一个浮层。 */
|
||||||
|
require __DIR__ . '/_entry_popup.php'; ?>
|
||||||
|
|
||||||
<script src="<?= e(asset('/assets/js/app.js')) ?>"></script>
|
<script src="<?= e(asset('/assets/js/app.js')) ?>"></script>
|
||||||
|
<?php /* 🔴 v43:条目详情浮层的脚本。**全站加载** ——
|
||||||
|
三处入口分散在首页/日程页,按需加载的判据会在这些页面之间来回漏;
|
||||||
|
它只有几 KB,不值得为省这点流量赌"某个页面漏加载"。 */ ?>
|
||||||
|
<script src="<?= e(asset('/assets/js/entry_popup.js')) ?>" defer></script>
|
||||||
<?php /* 🔴 v32 第 3 条:浏览器通知(站内消息 + 日程到期提醒)。
|
<?php /* 🔴 v32 第 3 条:浏览器通知(站内消息 + 日程到期提醒)。
|
||||||
⚠️ **只在已登录时加载** —— 登录页轮询会一直 302 去登录页,白跑。
|
⚠️ **只在已登录时加载** —— 登录页轮询会一直 302 去登录页,白跑。
|
||||||
同理也不在"免登录的导航展示页"上跑(那页没有会话)。
|
同理也不在"免登录的导航展示页"上跑(那页没有会话)。
|
||||||
@@ -230,14 +242,36 @@ window.FB_NOTIFY = {
|
|||||||
**不是**含糊地找 "pp" 这两个字母(页面里 `pp` 太容易撞:分页、其它类名…)。
|
**不是**含糊地找 "pp" 这两个字母(页面里 `pp` 太容易撞:分页、其它类名…)。
|
||||||
⚠️ DOM 必须已经解析到组件才判断 → 脚本放在 `</body>` 前(本文件本来就是),
|
⚠️ DOM 必须已经解析到组件才判断 → 脚本放在 `</body>` 前(本文件本来就是),
|
||||||
所以这里能直接查;不需要 DOMContentLoaded 包一层。
|
所以这里能直接查;不需要 DOMContentLoaded 包一层。
|
||||||
⚠️ 无 JS 兜底不受影响:组件的 `<noscript>` 块本来就是给"脚本没跑"准备的。 */ ?>
|
⚠️ 无 JS 兜底不受影响:组件的 `<noscript>` 块本来就是给"脚本没跑"准备的。
|
||||||
|
|
||||||
|
🔴🔴 v43(用户 2026-10-11 第 4 条):**上面那条"页面加载时扫一次"的判据不够了**。
|
||||||
|
行程详情页的编辑弹框是 AJAX 取片段、后来才把 `.pp` 塞进 DOM 的 ——
|
||||||
|
脚本那一刻**根本没被加载过**,之后也没人去补 ⇒ 选人按钮永远点不动
|
||||||
|
(「编辑一个行程的时候,参与的人…弹不出选人的框了」)。
|
||||||
|
⇒ 额外暴露一个 `window.fbLoadPicker()`:任何"动态插入含 `.pp` 的 HTML"之后
|
||||||
|
调它一次,脚本会被**按需拉起**(幂等:第二次调用直接返回同一个 Promise)。
|
||||||
|
配套的是 person_picker.js 里的 `window.fbInitPickers(scope)`(真正去绑定)。 */ ?>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
if (!document.querySelector('.pp[data-pp]')) { return; }
|
var url = <?= json_encode(asset('/assets/js/person_picker.js')) ?>;
|
||||||
var s = document.createElement('script');
|
var loading = null;
|
||||||
s.src = <?= json_encode(asset('/assets/js/person_picker.js')) ?>;
|
function load() {
|
||||||
s.defer = true;
|
if (window.fbInitPickers) { return Promise.resolve(true); }
|
||||||
document.head.appendChild(s);
|
if (loading) { return loading; }
|
||||||
|
loading = new Promise(function (res) {
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = url;
|
||||||
|
s.defer = true;
|
||||||
|
s.onload = function () { res(true); };
|
||||||
|
s.onerror = function () { loading = null; res(false); };
|
||||||
|
document.head.appendChild(s);
|
||||||
|
});
|
||||||
|
return loading;
|
||||||
|
}
|
||||||
|
// 供动态内容(AJAX 片段)按需拉起
|
||||||
|
window.fbLoadPicker = load;
|
||||||
|
// 首屏就有的(绝大多数页面):立刻拉
|
||||||
|
if (document.querySelector('.pp[data-pp]')) { load(); }
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<?php /* v27:笔记的分块编辑器同样**按需加载**(页面上真有 `#noteForm` 才拉)。
|
<?php /* v27:笔记的分块编辑器同样**按需加载**(页面上真有 `#noteForm` 才拉)。
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 🔴 v43(用户 2026-10-11 第 6 条)—— 任务的「编辑」表单,**独立一份**。
|
||||||
|
*
|
||||||
|
* 为什么要独立出来:
|
||||||
|
* 原来它内联在 `tasks/show.php` 的 `<details>` 里。而条目详情浮层点「编辑」时,
|
||||||
|
* 需要**单独取这份表单**(`tasks/edit?embed=1`)—— 不可能去详情页里抠。
|
||||||
|
* 抽成一份后,详情页与浮层用的是**同一张表单**,不会出现"两处字段不一致"。
|
||||||
|
*
|
||||||
|
* ⚠️ 字段名 / 服务端契约与原来**一字未改**(tasks/update 收的就是这些)。
|
||||||
|
* ⚠️ 「状态」不在这里改:状态由专门的 `tasks/status` 路由管(它还要处理循环任务),
|
||||||
|
* 文案里也说明了这一点(与 v24 第 14 批的口径一致)。
|
||||||
|
*
|
||||||
|
* 🔴🔴 **本片段一律用带前缀的局部变量(`$tk*`),绝不碰入参的名字。**
|
||||||
|
* 第一版写的是 `$t = is_array($t ?? null) ? $t : [];` —— 看似无害,其实是在
|
||||||
|
* **覆盖宿主模板的 `$t`**(那是"这条任务"本身)。只读一次没问题,
|
||||||
|
* 但下一个人只要把某个 `$tk*` 改回 `$t`(很自然)就会让详情页后半段的
|
||||||
|
* `$t['children']` / 评论区全是 null —— **而且是静默的**。
|
||||||
|
* 项目的"片段体检"(`_fb_frag_guard`)当场抓到了这处裸赋值,是对的。
|
||||||
|
* ⇒ 入参先**收进 `$tkT`**,后面全用它。
|
||||||
|
*
|
||||||
|
* 用法(include 前设好):
|
||||||
|
* $t 任务(Task::find 的形状)
|
||||||
|
* $people 名册
|
||||||
|
* $parents 可选父任务候选
|
||||||
|
* $categories / $boards 分类与看板
|
||||||
|
* $tkEditEmbed 是否"浮层形态"(true = 不输出自己的「保存」,由浮层底栏给)
|
||||||
|
*/
|
||||||
|
$tkT = is_array($t ?? null) ? $t : [];
|
||||||
|
$tkPeople = (array)($people ?? []);
|
||||||
|
$tkParents = (array)($parents ?? []);
|
||||||
|
$tkCats = (array)($categories ?? []);
|
||||||
|
$tkBoards = (array)($boards ?? []);
|
||||||
|
$tkEmbed = !empty($tkEditEmbed);
|
||||||
|
?>
|
||||||
|
<form method="post" action="<?= e(url('tasks/update')) ?>" class="node-form" id="tkEditForm">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="id" value="<?= (int)($tkT['id'] ?? 0) ?>">
|
||||||
|
<div class="nf-grid">
|
||||||
|
<div class="field full">
|
||||||
|
<label for="tk-title">标题</label>
|
||||||
|
<input class="input" id="tk-title" name="title" required value="<?= e((string)($tkT['title'] ?? '')) ?>">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>截止时间(留空=不上日历)</label>
|
||||||
|
<?php $dtName = 'due_at'; $dtValue = $tkT['due_at'] ?? null;
|
||||||
|
$dtRequired = false;
|
||||||
|
require __DIR__ . '/../_datetime.php'; ?>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>负责人</label>
|
||||||
|
<?php /* 第 8 条同一诉求:负责人也是「选人」,人多时用弹窗 + 首字母更快。
|
||||||
|
保留原下拉的「不指定」(pp-item-none + value=0)。 */ ?>
|
||||||
|
<?php
|
||||||
|
$ppName = 'assignee_id';
|
||||||
|
$ppPeople = $tkPeople;
|
||||||
|
$ppSelected = [(int)($tkT['assignee_id'] ?? 0)];
|
||||||
|
$ppMode = 'single';
|
||||||
|
$ppOpenBtn = '更换负责人';
|
||||||
|
$ppTitle = '谁负责这件事';
|
||||||
|
$ppEmptyText = '不指定';
|
||||||
|
$ppAllowEmpty = true;
|
||||||
|
require __DIR__ . '/../_person_picker.php';
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>循环</label>
|
||||||
|
<select class="select" name="repeat_rule">
|
||||||
|
<?php foreach (\App\Task::REPEAT as $rk => $rv): ?>
|
||||||
|
<option value="<?= e($rk) ?>"<?= (string)($tkT['repeat_rule'] ?? '') === (string)$rk ? ' selected' : '' ?>>
|
||||||
|
<?= e($rv) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>挂到哪个父任务下</label>
|
||||||
|
<select class="select" name="parent_id">
|
||||||
|
<option value="0">(这是一项独立的任务)</option>
|
||||||
|
<?php foreach ($tkParents as $p): ?>
|
||||||
|
<option value="<?= (int)$p['id'] ?>"<?= (int)($tkT['parent_id'] ?? 0) === (int)$p['id'] ? ' selected' : '' ?>>
|
||||||
|
<?= e((string)$p['title']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<?php /* v24 第 9、10 条:分类与看板(看板把任务放到哪一块,分类是标签) */ ?>
|
||||||
|
<div class="field">
|
||||||
|
<label>分类</label>
|
||||||
|
<select class="select" name="category_id">
|
||||||
|
<option value="0">未分类</option>
|
||||||
|
<?php foreach ($tkCats as $c): ?>
|
||||||
|
<option value="<?= (int)$c['id'] ?>"<?= (int)($tkT['category_id'] ?? 0) === (int)$c['id'] ? ' selected' : '' ?>>
|
||||||
|
<?= e((string)$c['name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>看板</label>
|
||||||
|
<select class="select" name="board_id">
|
||||||
|
<?php foreach ($tkBoards as $b): ?>
|
||||||
|
<option value="<?= (int)$b['id'] ?>"<?= (int)($tkT['board_id'] ?? 0) === (int)$b['id'] ? ' selected' : '' ?>>
|
||||||
|
<?= e((string)$b['name']) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<label for="tk-note">备注</label>
|
||||||
|
<textarea class="input" id="tk-note" name="note" rows="3"><?= e((string)($tkT['note'] ?? '')) ?></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php /* ⚠️ 「状态」不在这里改(由 tasks/status 路由管,它还要处理循环任务)。
|
||||||
|
浮层形态下由它的底栏给「保存」;整页详情页里带自己的按钮。 */ ?>
|
||||||
|
<?php if (!$tkEmbed): ?>
|
||||||
|
<div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
<?php /* 🔴🔴 —— **千万不要 unset 入参变量**!
|
||||||
|
这几种是宿主模板的"大局变量"(`$t` = 这条任务、`$people` = 名册…),
|
||||||
|
include 之后宿主还要继续用它们(`$t['children']`、评论、子任务…)。
|
||||||
|
我第一版照抄了 `_datetime.php` 那种"用完即清"的写法,把 `$t` 一并 unset ——
|
||||||
|
结果 `tasks/show.php` 后半段全部拿不到 `$t`,**子任务列表与评论区静默消失**
|
||||||
|
(e2e 当场抓到 2 条:子任务没了、@高亮没了)。
|
||||||
|
⇒ 这里只清**本片段自己造的**临时变量(`$tk*` 与 `$dt*` / `$pp*` 那几组)。 */
|
||||||
|
unset($tkT, $tkPeople, $tkParents, $tkCats, $tkBoards, $tkEmbed,
|
||||||
|
$dtName, $dtValue, $dtRequired,
|
||||||
|
$ppName, $ppPeople, $ppSelected, $ppMode, $ppOpenBtn, $ppTitle,
|
||||||
|
$ppEmptyText, $ppAllowEmpty); ?>
|
||||||
@@ -112,86 +112,13 @@ $isChild = !empty($t['parent_id']);
|
|||||||
<?php if ($canWrite): ?>
|
<?php if ($canWrite): ?>
|
||||||
<details class="node-add">
|
<details class="node-add">
|
||||||
<summary class="link-quiet">编辑这项任务</summary>
|
<summary class="link-quiet">编辑这项任务</summary>
|
||||||
<form method="post" action="<?= e(url('tasks/update')) ?>" class="node-form">
|
<?php /* 🔴 v43(用户第 6 条):表单**抽成独立一份** `_edit_form.php` ——
|
||||||
<?= csrf_field() ?>
|
条目详情浮层点「编辑」时要单独取它(`tasks/edit?embed=1`),
|
||||||
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
不可能去详情页里抠。抽出来后两处用的是同一张表单,不会不同步。
|
||||||
<div class="nf-grid">
|
⚠️ 片段内部用 `$tk*` 前缀的局部变量,**不碰宿主的 `$t`/`$parents`**。 */
|
||||||
<div class="field full">
|
$tkEditEmbed = false; // 整页形态:带上自己的「保存」按钮
|
||||||
<label>标题</label>
|
require __DIR__ . '/_edit_form.php';
|
||||||
<input class="input" name="title" required value="<?= e((string)$t['title']) ?>">
|
?>
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>截止时间(留空=不上日历)</label>
|
|
||||||
<?php $dtName = 'due_at'; $dtValue = $t['due_at'] ?? null;
|
|
||||||
$dtRequired = false;
|
|
||||||
require __DIR__ . '/../_datetime.php'; ?>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>负责人</label>
|
|
||||||
<?php /* 第 8 条同一诉求:负责人也是「选人」,人多时用弹窗 + 首字母更快。
|
|
||||||
保留原下拉的「不指定」(pp-item-none + value=0)。 */ ?>
|
|
||||||
<?php
|
|
||||||
$ppName = 'assignee_id';
|
|
||||||
$ppPeople = $people;
|
|
||||||
$ppSelected = [(int)($t['assignee_id'] ?? 0)];
|
|
||||||
$ppMode = 'single';
|
|
||||||
$ppOpenBtn = '更换负责人';
|
|
||||||
$ppTitle = '谁负责这件事';
|
|
||||||
$ppEmptyText = '不指定';
|
|
||||||
$ppAllowEmpty = true;
|
|
||||||
require __DIR__ . '/../_person_picker.php';
|
|
||||||
?>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>循环</label>
|
|
||||||
<select class="select" name="repeat_rule">
|
|
||||||
<?php foreach (\App\Task::REPEAT as $rk => $rv): ?>
|
|
||||||
<option value="<?= e($rk) ?>"<?= (string)($t['repeat_rule'] ?? '') === (string)$rk ? ' selected' : '' ?>>
|
|
||||||
<?= e($rv) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>挂到哪个父任务下</label>
|
|
||||||
<select class="select" name="parent_id">
|
|
||||||
<option value="0">(这是一项独立的任务)</option>
|
|
||||||
<?php foreach ($parents as $p): ?>
|
|
||||||
<option value="<?= (int)$p['id'] ?>"<?= (int)($t['parent_id'] ?? 0) === (int)$p['id'] ? ' selected' : '' ?>>
|
|
||||||
<?= e((string)$p['title']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<?php /* v24 第 9、10 条:分类与看板(看板把任务放到哪一块,分类是标签) */ ?>
|
|
||||||
<div class="field">
|
|
||||||
<label>分类</label>
|
|
||||||
<select class="select" name="category_id">
|
|
||||||
<option value="0">未分类</option>
|
|
||||||
<?php foreach (($categories ?? []) as $c): ?>
|
|
||||||
<option value="<?= (int)$c['id'] ?>"<?= (int)($t['category_id'] ?? 0) === (int)$c['id'] ? ' selected' : '' ?>>
|
|
||||||
<?= e((string)$c['name']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label>看板</label>
|
|
||||||
<select class="select" name="board_id">
|
|
||||||
<?php foreach (($boards ?? []) as $b): ?>
|
|
||||||
<option value="<?= (int)$b['id'] ?>"<?= (int)($t['board_id'] ?? 0) === (int)$b['id'] ? ' selected' : '' ?>>
|
|
||||||
<?= e((string)$b['name']) ?>
|
|
||||||
</option>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field full">
|
|
||||||
<label>备注</label>
|
|
||||||
<textarea class="input" name="note" rows="2"><?= e((string)($t['note'] ?? '')) ?></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="btn-row"><button class="btn btn-sm" type="submit">保存</button></div>
|
|
||||||
</form>
|
|
||||||
</details>
|
</details>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -570,6 +570,34 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
|||||||
s.parentNode.removeChild(s); // 执行完就摘掉,别留在 DOM 里
|
s.parentNode.removeChild(s); // 执行完就摘掉,别留在 DOM 里
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🔴🔴 v43(用户 2026-10-11 第 4 条):「编辑一个行程的时候,"参与的人"
|
||||||
|
添加参与人的那个功能已经不好使了,弹不出选人的框了。」
|
||||||
|
|
||||||
|
真根因:选人组件(`person_picker.js`)的绑定**只跑一次**(脚本加载时扫一遍 `.pp`)。
|
||||||
|
而这一段表单是 AJAX 取回来、`innerHTML` 塞进 DOM 的 ⇒ 它里面的 `.pp`
|
||||||
|
**从来没被 init 过** ⇒「+ 添加参与的人」按钮上没有任何 click 监听
|
||||||
|
⇒ 点了没反应(弹框弹不出来)。暂存区/确定/取消同理全按不动。
|
||||||
|
|
||||||
|
⇒ 塞完片段后补调 `window.fbInitPickers(scope)`(person_picker.js 暴露,
|
||||||
|
内部幂等)。⚠️ 该脚本是**按需加载**的(layout.php:页面上有 `.pp` 才拉),
|
||||||
|
而弹框里的 `.pp` 是后来才出现的 —— 脚本此刻可能还没加载完。
|
||||||
|
所以做"等它就绪":轮询若干次,出现即调;始终不来也无害(下次打开再试)。
|
||||||
|
⚠️ 只重试有限次(约 3 秒),不做无限轮询。 */
|
||||||
|
function initPickersWhenReady(scope, tries) {
|
||||||
|
tries = (tries === undefined) ? 25 : tries;
|
||||||
|
/* 先确保脚本被拉起(详情页首屏没有 `.pp`,layout 那条"首屏就拉"的判据不会触发它)。
|
||||||
|
`fbLoadPicker` 是幂等的,重复调用只是拿到同一个 Promise。 */
|
||||||
|
if (!window.fbInitPickers && typeof window.fbLoadPicker === 'function') {
|
||||||
|
try { window.fbLoadPicker(); } catch (e) { /* 忽略 */ }
|
||||||
|
}
|
||||||
|
if (typeof window.fbInitPickers === 'function') {
|
||||||
|
try { window.fbInitPickers(scope); } catch (e) { /* 不阻塞表单其它初始化 */ }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (tries <= 0) { return; }
|
||||||
|
setTimeout(function () { initPickersWhenReady(scope, tries - 1); }, 150);
|
||||||
|
}
|
||||||
|
|
||||||
function loadInto(url, title) {
|
function loadInto(url, title) {
|
||||||
if (titleEl()) { titleEl().textContent = title || '行程'; }
|
if (titleEl()) { titleEl().textContent = title || '行程'; }
|
||||||
if (body()) { body().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
if (body()) { body().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
||||||
@@ -582,10 +610,21 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
|||||||
body().innerHTML = html;
|
body().innerHTML = html;
|
||||||
/* ① 先执行片段自带的内联脚本(它会定义/调用自己的初始化) */
|
/* ① 先执行片段自带的内联脚本(它会定义/调用自己的初始化) */
|
||||||
runInlineScripts(body(), html);
|
runInlineScripts(body(), html);
|
||||||
/* ② 再补跑宿主这侧的通用升级(日期/antd/行程表单),按依赖顺序 */
|
/* ② 再补跑宿主这侧的通用升级(日期/antd/行程表单/选人组件),按依赖顺序 */
|
||||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||||
if (window.fbNodeForm) { window.fbNodeForm(body()); }
|
if (window.fbNodeForm) { window.fbNodeForm(body()); }
|
||||||
|
/* 🔴🔴 v43(用户 2026-10-11 第 4 条):「编辑一个行程的时候,"参与的人"
|
||||||
|
添加参与人的那个功能已经不好使了,弹不出选人的框了。」
|
||||||
|
|
||||||
|
真根因:选人组件(`person_picker.js`)的绑定**只跑一次**(脚本加载时扫一遍
|
||||||
|
`.pp`)。而这一段表单是 AJAX 取回来、`innerHTML` 塞进 DOM 的 ⇒
|
||||||
|
它里面的 `.pp` **从来没被 init 过** ⇒「+ 添加参与的人」按钮上没有 click
|
||||||
|
监听 ⇒ 点了没反应。⇒ 这里塞完片段**补调一次**初始化。
|
||||||
|
⚠️ 该脚本是**按需加载**的(layout.php:页面上有 `.pp` 才拉),而弹框里的
|
||||||
|
`.pp` 是后来才出现的 —— 脚本可能还没加载完(或都还没开始加载)。
|
||||||
|
所以这里做"等它就绪":轮询若干次,出现即调;全程没出现也无害。 */
|
||||||
|
initPickersWhenReady(body());
|
||||||
var f = document.getElementById('nf-title');
|
var f = document.getElementById('nf-title');
|
||||||
if (f) { f.focus(); }
|
if (f) { f.focus(); }
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user