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:
@@ -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 前设好):
|
||||
* $evFields 正在编辑的日程(新建时传空数组)
|
||||
* $evAllDay 全天的默认值(新建默认**不勾**,用户 2026-10-02 第 6 条)
|
||||
* $evMvStyle 🔴 v43:「标签 62px 右对齐 + 冒号 + 控件」的**观影式**排版开关。
|
||||
* 新建日程那个弹框要它(用户:日程新建页排版和观影/行程一样);
|
||||
* 编辑页(`calendar/event.php`)继续用原来的 `.form-grid` 两列。
|
||||
*/
|
||||
$evF = is_array($evFields ?? null) ? $evFields : [];
|
||||
$evAllDayDefault = (bool)($evAllDay ?? false);
|
||||
$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">
|
||||
<label for="ev-title">标题 *</label>
|
||||
<?php echo $evRowOpen('full'); ?>
|
||||
<?= $evLab('标题', 'ev-title', true) ?>
|
||||
<input class="input" id="ev-title" name="title" required
|
||||
value="<?= e((string)($evF['title'] ?? '')) ?>" placeholder="如:家长会">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="ev-location">地点</label>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('地点', 'ev-location') ?>
|
||||
<input class="input" id="ev-location" name="location"
|
||||
value="<?= e((string)($evF['location'] ?? '')) ?>" placeholder="如:学校三楼会议室">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>提醒</label>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('提醒') ?>
|
||||
<?php /* v24 第 20 批 第 8 条:**多选**(用户原话:「一个日程,我可以选择让它
|
||||
5分钟提醒,10分钟提醒等等。」)。
|
||||
⚠️ 用 checkbox 而不是 `<select multiple>`:后者在手机上要"按住再点",
|
||||
几乎没法用(任务共享那个 multiple 下拉就是因此被换掉的)。
|
||||
⚠️ 字段名必须是 `reminder_minutes[]` —— Calendar::reminderMinutes() 认得数组。
|
||||
写成 `reminder_minutes`(不带 [])会让 PHP 只留最后一个勾选。 */
|
||||
写成 `reminder_minutes`(不带 [])会让 PHP 只留最后一个勾选。
|
||||
🔴 v43(用户第 5 条):他要求把这一栏底下那句「多选与不提醒」的说明去掉
|
||||
⇒ 这里删掉那个 `.tip`(多选语义由复选框本身表达)。 */
|
||||
$curReminders = \App\Calendar::reminderList($evF['reminder_minutes'] ?? null); ?>
|
||||
<div class="rem-picks">
|
||||
<?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>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<span class="tip">可以选多个(到点各响一次);一个都不选就是不提醒。</span>
|
||||
</div>
|
||||
|
||||
<?php /* v24 第 5、6 条:起止各一个「日期 + 时分秒」合在一起的控件(全局统一),
|
||||
@@ -74,43 +96,58 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
||||
修法:**结束那一端不再默认藏起来**,范围控件一次性就挂上。
|
||||
起止成对本来就该一起填。想表达"只有一个时刻"的话,范围控件 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 /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
|
||||
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
|
||||
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
|
||||
<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>
|
||||
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
||||
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
|
||||
$dtRange = 'start';
|
||||
$dtRange = 'start'; $dtPlugin = 'antd';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<div class="field ev-dt" data-dt id="ev-end-field">
|
||||
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
|
||||
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
|
||||
$dtRange = 'end';
|
||||
$dtRange = 'end'; $dtPlugin = 'antd';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
</div><?php /* /dt-range */ ?>
|
||||
<span class="field-hint" style="display:block;margin:-4px 0 10px">
|
||||
结束留空就表示没有明确的结束(只占开始那一刻/那一天)。
|
||||
</span>
|
||||
<?php /* 🔴 v43(用户第 5 条):他要求把「结束留空即无明确结束」的那条提示去掉
|
||||
⇒ 那句 `.field-hint` 整块删除(不再输出)。 */ ?>
|
||||
|
||||
<div class="field">
|
||||
<label>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<label class="chk-inline">
|
||||
<input type="checkbox" id="ev-allday" name="all_day" value="1"
|
||||
<?= $evIsAllDay ? 'checked' : '' ?>>
|
||||
全天
|
||||
<span>全天</span>
|
||||
</label>
|
||||
<span class="field-hint">勾上就不管具体几点,只占这一天</span>
|
||||
<?php /* 🔴 v43(用户第 5 条):他要求把「全天只管日期、不管几点」的说明也去掉
|
||||
⇒ 那句 `.field-hint` 整块删除。 */ ?>
|
||||
</div>
|
||||
|
||||
<?php /* 用户 2026-10-03 第 5、6 条:重复日程。
|
||||
放在「全天」之后的自然位置:先定"哪一天/几点",再定"要不要重复"。
|
||||
周期用 FREQ 记号,结束条件三档(一直 / N 次 / 到某天),另加"工作日"。 */ ?>
|
||||
<div class="field">
|
||||
<label for="ev-freq">重复</label>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('重复', 'ev-freq') ?>
|
||||
<select class="select" id="ev-freq" name="freq">
|
||||
<?php $rp = \App\Repeat::parse((string)($evF['rrule'] ?? '')); ?>
|
||||
<?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>
|
||||
</div>
|
||||
|
||||
<div class="field" id="ev-rep-end-box" hidden>
|
||||
<label for="ev-rep-end">结束条件</label>
|
||||
<select class="select" id="ev-rep-end" name="rep_end">
|
||||
<?php foreach (\App\Repeat::ENDS as $k => $w): ?>
|
||||
<option value="<?= e($k) ?>"<?= $rp['count'] > 0 ? '' : '' ?>><?= e($w) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('结束条件', 'ev-rep-end') ?>
|
||||
<div id="ev-rep-end-box" hidden>
|
||||
<select class="select" id="ev-rep-end" name="rep_end">
|
||||
<?php foreach (\App\Repeat::ENDS as $k => $w): ?>
|
||||
<option value="<?= e($k) ?>"><?= e($w) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" id="ev-rep-count-box" hidden>
|
||||
<label for="ev-rep-count">重复次数(含第一次)</label>
|
||||
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
|
||||
value="<?= (int)($rp['count'] ?: 10) ?>">
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('重复次数(含第一次)', 'ev-rep-count') ?>
|
||||
<div id="ev-rep-count-box" hidden>
|
||||
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
|
||||
value="<?= (int)($rp['count'] ?: 10) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" id="ev-rep-until-box" hidden>
|
||||
<label for="ev-rep-until">重复到</label>
|
||||
<input class="input" type="date" id="ev-rep-until" name="rep_until"
|
||||
value="<?= e((string)($rp['until'] ?? '')) ?>">
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('重复到', 'ev-rep-until') ?>
|
||||
<div id="ev-rep-until-box" hidden>
|
||||
<input class="input" type="date" id="ev-rep-until" name="rep_until"
|
||||
value="<?= e((string)($rp['until'] ?? '')) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="ev-status">状态</label>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('状态', 'ev-status') ?>
|
||||
<select class="select" id="ev-status" name="status">
|
||||
<?php $st = (string)($evF['status'] ?? 'confirmed'); ?>
|
||||
<option value="confirmed"<?= $st === 'confirmed' ? ' selected' : '' ?>>确定</option>
|
||||
@@ -150,8 +193,8 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="ev-transp">这段时间算</label>
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('这段时间算', 'ev-transp') ?>
|
||||
<select class="select" id="ev-transp" name="transp">
|
||||
<?php $tp = (int)($evF['transp'] ?? 0); ?>
|
||||
<option value="0"<?= $tp === 0 ? ' selected' : '' ?>>忙碌</option>
|
||||
@@ -159,14 +202,14 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label for="ev-note">备注</label>
|
||||
<?php echo $evRowOpen('full'); ?>
|
||||
<?= $evLab('备注', 'ev-note') ?>
|
||||
<textarea class="input" id="ev-note" name="note" rows="3"
|
||||
placeholder="要带什么、跟谁一起…"><?= e((string)($evF['note'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field full">
|
||||
<label>附件(可多选)</label>
|
||||
<?php echo $evRowOpen('full'); ?>
|
||||
<?= $evLab('附件(可多选)') ?>
|
||||
<?php /* v24 第 21 批 第 7 条:统一成「点击或拖动进行上传」的框。
|
||||
⚠️ 文件框的 id 保持 `ev-attach`(原来就叫这个)—— e2e 与部署断言里
|
||||
有按 id 定位的写法,改名会把它们静默打成假红。 */
|
||||
@@ -189,7 +232,7 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
||||
sync();
|
||||
|
||||
/* ⚠️ v24 第 21 批 第 7 条:原来这里有一整段「结束时间显隐 + 跟着开始时间走」的 JS。
|
||||
现在起止两端**始终可见**,并且由 Semi 的范围控件统一接管(一次选完两端),
|
||||
现在起止两端**始终可见**,并且由范围控件统一接管(一次选完两端),
|
||||
所以那段"同步两端"的逻辑整个删掉 —— 留着不但没用,还会和范围控件抢着改那两个
|
||||
原生字段(两处都写 → 值互相覆盖)。范围控件选完会自己把两端分别写回原生字段
|
||||
(见 app.js 的 onRange → writeDT),服务端契约不变。 */
|
||||
@@ -233,4 +276,5 @@ $evIsAllDay = array_key_exists('all_day', $evF) ? !empty($evF['all_day']) : $evA
|
||||
}
|
||||
})();
|
||||
</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 里真正属于某本日历的自定义日程。
|
||||
*
|
||||
* 🔴 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']);
|
||||
$evEmbed = !empty($evEmbed);
|
||||
$ym = substr((string)$ev['starts_at'], 0, 7);
|
||||
?>
|
||||
<?php if (!$evEmbed): ?>
|
||||
<div class="page-head">
|
||||
<h1><?= e((string)$ev['title']) ?></h1>
|
||||
<span class="sub"><?= e((string)($ev['starts_at'] ?? '')) ?></span>
|
||||
<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>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card card-pad">
|
||||
<?php $ym = substr((string)$ev['starts_at'], 0, 7); ?>
|
||||
<?php if (!$canWrite): ?>
|
||||
<div class="<?= $evEmbed ? '' : 'card card-pad' ?>">
|
||||
<?php if ($evEmbed && !$canWrite): ?>
|
||||
<p class="empty-hint">这本日历你只有只读权限,不能改。</p>
|
||||
<?php elseif (!$canWrite): ?>
|
||||
<dl class="kv">
|
||||
<div class="kv-row"><dt>开始</dt>
|
||||
<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>
|
||||
<p class="empty-hint">这本日历你只有只读权限,看得到但不能改。</p>
|
||||
<?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() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$ev['id'] ?>">
|
||||
<input type="hidden" name="ym" value="<?= e($ym) ?>">
|
||||
@@ -95,12 +107,22 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
||||
<?php endif; ?>
|
||||
|
||||
<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>
|
||||
<?php if (!$evEmbed): ?>
|
||||
<div class="btn-row">
|
||||
<button class="btn" type="submit">保存</button>
|
||||
<a class="btn btn-ghost" href="<?= e(url('calendar', ['ym' => $ym])) ?>">取消</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<?php $as = \App\Attachment::list(\App\Calendar::ATTACH, (int)$ev['id']); ?>
|
||||
<?php if ($as): ?>
|
||||
@@ -134,7 +156,9 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
||||
*/
|
||||
$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"
|
||||
onsubmit="return confirm('按上面选的范围删除?')">
|
||||
<?= csrf_field() ?>
|
||||
@@ -171,3 +195,4 @@ $canWrite = \App\Auth::canWrite() && !empty($ev['_can_write']);
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php unset($evEmbed, $evIsRepeat, $evIsAllDay, $evAllDay, $evFields, $evMvStyle, $ym, $occ); ?>
|
||||
|
||||
@@ -216,7 +216,13 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
</div>
|
||||
<?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
|
||||
// 点的位置优先;没点就"月视图当月 → 该月 1 号 / 本月 → 今天"的 09:00
|
||||
$defDay = $newAt !== '' ? substr($newAt, 0, 10)
|
||||
@@ -227,7 +233,11 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
|
||||
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
|
||||
$evAllDay = false;
|
||||
$evMvStyle = true; // 观影式排版(用户第 5 条)
|
||||
echo '<div class="mv-grid">';
|
||||
require __DIR__ . '/_event_fields.php';
|
||||
echo '</div>';
|
||||
unset($evMvStyle);
|
||||
?>
|
||||
</div><?php /* /cal-modal-body */ ?>
|
||||
<div class="cal-modal-foot">
|
||||
@@ -273,7 +283,17 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
<?php foreach (($cols ?? []) as $i => $c): ?>
|
||||
<div class="tg-adcell">
|
||||
<?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'] ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||
style="background: <?= e((string)$ev['color']) ?>;"
|
||||
href="<?= e((string)$ev['href']) ?>"
|
||||
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 是运行时才知道的,类名会爆炸;
|
||||
而且模板已经在内联 top/height 了,同一套坐标系最不容易错。 */ ?>
|
||||
<?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'] ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, $evDet, $evEdit) ?>'
|
||||
style="background: <?= e((string)$ev['color']) ?>;
|
||||
top: <?= (float)$ev['top'] ?>%; height: <?= (float)$ev['height'] ?>%;
|
||||
left: calc(2px + <?= (float)($ev['left'] ?? 0) ?>%);
|
||||
@@ -357,8 +393,18 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
<div class="cal-evs">
|
||||
<?php $n = 0; foreach ($cell['events'] as $ev): $n++; ?>
|
||||
<?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' ?>"
|
||||
data-cal="<?= (int)$ev['cal'] ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||
style="background: <?= e((string)$ev['color']) ?>;"
|
||||
href="<?= e((string)$ev['href']) ?>"
|
||||
title="<?= e((string)$ev['title']) ?>">
|
||||
@@ -409,7 +455,7 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
月视图:一格 = 一天 → 开始时间用那天 09:00("当日默认"比 00: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 startHour = startField ? startField.querySelector('select.dt-hour') : null;
|
||||
var startMin = startField ? startField.querySelector('select.dt-minute') : null;
|
||||
|
||||
+18
-3
@@ -127,9 +127,24 @@ $backUrl = url('days');
|
||||
<div class="mv-row">
|
||||
<span class="mv-lb">日期<i>*</i></span>
|
||||
<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"
|
||||
value="<?= e(sprintf('%04d-%02d-%02d', (int)$v('year', (int)date('Y')), (int)$v('month', 1), (int)$v('day', 1))) ?>"
|
||||
aria-label="日期">
|
||||
<?php /* 🔴🔴 v43(用户 2026-10-11 第 1 条):「日期那里多了一个控件,
|
||||
只保留我们最新的控件就行,之前那种控件删除。」
|
||||
|
||||
真根因: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-day" name="day" value="<?= (int)$v('day', 1) ?>">
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,17 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
|
||||
→ 改 = `.btn`(主按钮的**蓝**),删 = `.btn-warn`(**黄**实心)。
|
||||
⚠️ 原来用的是 `.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">
|
||||
<?= csrf_field() ?>
|
||||
<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(); }
|
||||
}
|
||||
}
|
||||
/* 🔴 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) {
|
||||
var t = e.target;
|
||||
if (!t || !t.closest) { 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; }
|
||||
var c = card();
|
||||
if (c && t === c) { setOpen(false); }
|
||||
|
||||
+34
-1
@@ -275,7 +275,18 @@ unset($evRef);
|
||||
<?php if ($allDay): ?>
|
||||
<div class="ag-allday">
|
||||
<?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="ag-ad-t">全天</span>
|
||||
<span class="ag-ad-w"><?= e((string)$ev['title']) ?></span>
|
||||
@@ -329,8 +340,18 @@ unset($evRef);
|
||||
$endMin = min($endMin, $endH * 60);
|
||||
$hPct = ($endMin / 60 - $startH) / max(1, ($endH - $startH)) * 100 - $topPct;
|
||||
$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']) ?>"
|
||||
data-fb-entry='<?= fb_entry_json($ev, (string)($ev['href'] ?? ''), $evEdit) ?>'
|
||||
style="background: <?= e((string)$ev['color']) ?>;
|
||||
top: <?= number_format($topPct, 2) ?>%;
|
||||
height: <?= number_format($hPct, 2) ?>%;
|
||||
@@ -416,7 +437,19 @@ unset($evRef);
|
||||
require __DIR__ . '/_task_check.php';
|
||||
?>
|
||||
<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']])) ?>"
|
||||
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) ?>;">
|
||||
<?= e((string)$t['title']) ?>
|
||||
</a>
|
||||
|
||||
+40
-6
@@ -207,7 +207,19 @@ $isLoggedIn = \App\Auth::check();
|
||||
<button type="button" class="pv-nav pv-next" aria-label="下一张" hidden>›</button>
|
||||
</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>
|
||||
<?php /* 🔴 v43:条目详情浮层的脚本。**全站加载** ——
|
||||
三处入口分散在首页/日程页,按需加载的判据会在这些页面之间来回漏;
|
||||
它只有几 KB,不值得为省这点流量赌"某个页面漏加载"。 */ ?>
|
||||
<script src="<?= e(asset('/assets/js/entry_popup.js')) ?>" defer></script>
|
||||
<?php /* 🔴 v32 第 3 条:浏览器通知(站内消息 + 日程到期提醒)。
|
||||
⚠️ **只在已登录时加载** —— 登录页轮询会一直 302 去登录页,白跑。
|
||||
同理也不在"免登录的导航展示页"上跑(那页没有会话)。
|
||||
@@ -230,14 +242,36 @@ window.FB_NOTIFY = {
|
||||
**不是**含糊地找 "pp" 这两个字母(页面里 `pp` 太容易撞:分页、其它类名…)。
|
||||
⚠️ DOM 必须已经解析到组件才判断 → 脚本放在 `</body>` 前(本文件本来就是),
|
||||
所以这里能直接查;不需要 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>
|
||||
(function () {
|
||||
if (!document.querySelector('.pp[data-pp]')) { return; }
|
||||
var s = document.createElement('script');
|
||||
s.src = <?= json_encode(asset('/assets/js/person_picker.js')) ?>;
|
||||
s.defer = true;
|
||||
document.head.appendChild(s);
|
||||
var url = <?= json_encode(asset('/assets/js/person_picker.js')) ?>;
|
||||
var loading = null;
|
||||
function load() {
|
||||
if (window.fbInitPickers) { return Promise.resolve(true); }
|
||||
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>
|
||||
<?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): ?>
|
||||
<details class="node-add">
|
||||
<summary class="link-quiet">编辑这项任务</summary>
|
||||
<form method="post" action="<?= e(url('tasks/update')) ?>" class="node-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="id" value="<?= (int)$t['id'] ?>">
|
||||
<div class="nf-grid">
|
||||
<div class="field full">
|
||||
<label>标题</label>
|
||||
<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>
|
||||
<?php /* 🔴 v43(用户第 6 条):表单**抽成独立一份** `_edit_form.php` ——
|
||||
条目详情浮层点「编辑」时要单独取它(`tasks/edit?embed=1`),
|
||||
不可能去详情页里抠。抽出来后两处用的是同一张表单,不会不同步。
|
||||
⚠️ 片段内部用 `$tk*` 前缀的局部变量,**不碰宿主的 `$t`/`$parents`**。 */
|
||||
$tkEditEmbed = false; // 整页形态:带上自己的「保存」按钮
|
||||
require __DIR__ . '/_edit_form.php';
|
||||
?>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -570,6 +570,34 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
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) {
|
||||
if (titleEl()) { titleEl().textContent = title || '行程'; }
|
||||
if (body()) { body().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
||||
@@ -582,10 +610,21 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
body().innerHTML = html;
|
||||
/* ① 先执行片段自带的内联脚本(它会定义/调用自己的初始化) */
|
||||
runInlineScripts(body(), html);
|
||||
/* ② 再补跑宿主这侧的通用升级(日期/antd/行程表单),按依赖顺序 */
|
||||
/* ② 再补跑宿主这侧的通用升级(日期/antd/行程表单/选人组件),按依赖顺序 */
|
||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||
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');
|
||||
if (f) { f.focus(); }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user