v44 第 44 批:界面 5 条 + 移动端 API v1 全量
界面(用户 2026-10-12 第 1~5 条) 1. 新建日程「日历本」并入 .cs 自绘下拉,颜色画进候选项,三字与下拉同行。 2. 新建日程「开始/结束」改「日期范围:」,删旧日期控件; 重复相关三行仅选重复时出现;删「(含第一次)」与「这段时间算忙碌」。 3. 条目浮层加「地点」、去掉「状态」行;首页红线按分钟定位(原只取整小时); 去掉左侧红点只留红线。 4. 任务页删掉状态说明提示(STATUS_HINT 三值清空)。 5. 新建任务:标题在前树框在后;截止时间改最新日历控件;负责人默认自己; 删冗余提示;「上面要做什么」→「任务名称」;清理括号说明。 移动端 API(用户第 6 条:导航模块除外,其余全量) - 新增 Bearer 令牌鉴权(api_tokens 表,Schema v32),与登录口令 / DAV 口令三套分立。 - Api.php 内核(鉴权/取参/信封/分页/分发 + _method 覆写)+ ApiHandler.php 17 条资源 + ApiToken.php + api.php 入口。 - 路由:router.php / .htaccess / index.php 三处 /api/ 兜底(部署脚本自动追加线上规则)。 - 个人设置新增「App 接口令牌」卡;docs/API.md 接口文档; docs/API-QUESTION-LIST.md 自主决策与待确认缺口清单。 验证:本地 e2e 1510/0、v43 真机 33/0、v43 反向 9/0、 v44 真机 41/0、v44 API 63/0、v44 反向 19/0;全流水线 PUSH_RC=0。
This commit is contained in:
@@ -32,9 +32,15 @@ $evMv = !empty($evMvStyle);
|
||||
抄两遍必然有一天两边不同步(v37 那种"改了 A 忘了 B"的坑)。
|
||||
⚠️ 「必填星号」作为**参数**传(不要用 `use` 捕获 —— 闭包定义时那个变量还没赋值,
|
||||
捕获到的是 null,星号永远不出现)。 */
|
||||
$evRowOpen = static function (string $full = '') use ($evMv): string {
|
||||
return '<div class="' . ($evMv ? 'mv-row' : 'field') . ($full !== '' ? ' ' . $full : '') . '">';
|
||||
$evRowOpen = static function (string $full = '', bool $hidden = false) use ($evMv): string {
|
||||
return '<div class="' . ($evMv ? 'mv-row' : 'field') . ($full !== '' ? ' ' . $full : '') . '"'
|
||||
. ($hidden ? ' hidden' : '') . '>';
|
||||
};
|
||||
/* 重复规则**先算好**:下面"结束条件 / 重复次数 / 重复到"三行要不要默认藏起来,
|
||||
取决于它(见 v44 第 2 条)。原来 `$rp` 是在「重复」那一行才 parse 的,
|
||||
那三行排在它后面 ⇒ 拿不到。提到最前面,只算一次。 */
|
||||
$evRp = \App\Repeat::parse((string)($evF['rrule'] ?? ''));
|
||||
$evRepeating = (string)($evRp['freq'] ?? '') !== '';
|
||||
$evLab = static function (string $text, string $for = '', bool $star = false) use ($evMv): string {
|
||||
if ($evMv) {
|
||||
// 观影式:标签是 <span class="mv-lb">,冒号由 CSS ::after 给
|
||||
@@ -107,28 +113,50 @@ $evLab = static function (string $text, string $for = '', bool $star = false) us
|
||||
⇒ `$dtPlugin='antd'`(与观影/行程同一套:面板有可点的时/分列、中文「确定」),
|
||||
标签改「开始 / 结束」(并去掉那颗必填星号的位置歧义 —— 只标在开始上)。
|
||||
⚠️ 契约一字未改:隐藏字段仍是 `*_date/_hour/_minute`;antd 拉不动时
|
||||
自动退回自建下拉(渐进增强)。 */ ?>
|
||||
<div class="dt-range<?= $evMv ? ' mv-row' : '' ?>">
|
||||
<?php /* ⚠️ 标签只有**一个**,挂在范围容器上(不是各挂各的):
|
||||
自动退回自建下拉(渐进增强)。
|
||||
|
||||
🔴🔴 v44(用户 2026-10-12 第 2 条):「开始/结束,改为:日期范围:。
|
||||
之前的日期选择控件还有,需要删除。」
|
||||
—— 用户要三种东西:① 标签「日期范围」;② 标签与**最新控件同行**;
|
||||
③ 原生那套(年/月/日 + 时/分 五个下拉)**真的消失**。
|
||||
|
||||
🔴🔴 **"还在"的真根因(结构性,不是样式没写)**:
|
||||
观影那对是 `‹div.mv-row.dt-range› ‹div.dt-input›`(**直接子**),
|
||||
而日程这对是 `‹div.dt-range› ‹div.field.ev-dt› ‹div.dt-input›`
|
||||
—— `.dt-input` 是**孙子**。
|
||||
于是那条最常见的收起规则 `.dt-range.is-antd > .dt-input`(**直接子**选择器)
|
||||
**在日程上根本不命中** ⇒ antd 挂上了、原生五个下拉照样显示
|
||||
(用户看到两个控件并排,"之前的日期选择控件还有")。
|
||||
⚠️ 与 v43 纪念日那次是**同一个家族**:结构一变,直接子选择器就静默失效。
|
||||
⇒ 两手都做:① 把日程的结构改成与观影**一致**(`.dt-input` 直接子);
|
||||
② CSS 里把 `>` 放宽成后代选择器(0,2,0 → 仍高于基类),
|
||||
将来结构再变也不会又冒出第二个控件。
|
||||
|
||||
⇒ 形态:由 `_datetime.php` 的 `$dtLabel` 把「日期范围:」
|
||||
**内联在控件那一行**(不靠宿主的行结构),一个 `.mv-lb` 挂在
|
||||
`.dt-input` 里 → 三样天然同行,宽度不够时整体换行。 */ ?>
|
||||
|
||||
<div class="dt-range<?= $evMv ? ' mv-row is-tagged' : '' ?>">
|
||||
<?php /* ⚠️ 标签只有**一个**,挂在**第一个端点**上(不是各挂各的):
|
||||
升级成一个范围控件之后,"开始"与"结束"就是同一个控件的两个端点,
|
||||
两个标签会变成"看着像两个字段、其实是一个"的错觉。 */ ?>
|
||||
<?php if ($evMv): ?>
|
||||
<span class="mv-lb dt-range-label">开始 / 结束<i>*</i></span>
|
||||
<?php else: ?>
|
||||
<span class="dt-range-label">开始 / 结束 *</span>
|
||||
两个标签会变成"看着像两个字段、其实是一个"的错觉。
|
||||
⚠️ 观影式:标签由 `_datetime.php` 内联(`$dtLabel`)—— 要的是与控件同行;
|
||||
两列式(编辑整页):标签仍是 `.dt-range-label`,在两端之上。 */ ?>
|
||||
<?php if (!$evMv): ?>
|
||||
<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'; $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'; $dtPlugin = 'antd';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div>
|
||||
<?php $dtName = 'starts_at'; $dtId = 'ev-start';
|
||||
$dtValue = $evF['starts_at'] ?? null; $dtRequired = true;
|
||||
$dtRange = 'start'; $dtPlugin = 'antd'; $dtClass = 'field ev-dt'; $dtFieldId = '';
|
||||
if ($evMv) { $dtLabel = '日期范围'; $dtLabelStar = true; }
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
<?php $dtName = 'ends_at'; $dtId = 'ev-end';
|
||||
$dtValue = $evF['ends_at'] ?? null; $dtRequired = false;
|
||||
$dtRange = 'end'; $dtPlugin = 'antd';
|
||||
$dtClass = 'field ev-dt'; $dtFieldId = 'ev-end-field';
|
||||
/* 结束端点不重复标签(同一个范围控件,标签已经挂在开始那一端上) */
|
||||
$dtLabel = ''; $dtLabelStar = false;
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
</div><?php /* /dt-range */ ?>
|
||||
<?php /* 🔴 v43(用户第 5 条):他要求把「结束留空即无明确结束」的那条提示去掉
|
||||
⇒ 那句 `.field-hint` 整块删除(不再输出)。 */ ?>
|
||||
@@ -149,16 +177,23 @@ $evLab = static function (string $text, string $for = '', bool $star = false) us
|
||||
<?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): ?>
|
||||
<option value="<?= e($k) ?>"<?= (string)$rp['freq'] === (string)$k ? ' selected' : '' ?>><?= e($w) ?></option>
|
||||
<option value="<?= e($k) ?>"<?= (string)$evRp['freq'] === (string)$k ? ' selected' : '' ?>><?= e($w) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?php /* 🔴 v44(用户 2026-10-12 第 2 条):「重复这里,只有选择重复的时候,
|
||||
下面的结束条件和重复次数、重复到,这几个选项才出现,否则就隐藏。」
|
||||
⇒ 三行**服务端**就按"是否重复"给出 `hidden`(不是只靠 JS):
|
||||
· 回填编辑(本来就有 rrule)时它们该出现;
|
||||
· 新建(默认"不重复")时它们从一开始就是隐藏的 —— 页面上不会闪一下才收起来。
|
||||
⚠️ 行**本身**也藏:原来只有行**里面**那个 box 带 hidden,
|
||||
于是标签还孤零零留在表单里(用户看到的"还在")。
|
||||
⚠️ 用户同条还要「重复次数(含第一次)」那个括号去掉 ⇒ 标签改「重复次数」。 */ ?>
|
||||
<?php echo $evRowOpen('', !$evRepeating); ?>
|
||||
<?= $evLab('结束条件', 'ev-rep-end') ?>
|
||||
<div id="ev-rep-end-box" hidden>
|
||||
<div id="ev-rep-end-box"<?= $evRepeating ? '' : ' 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>
|
||||
@@ -167,19 +202,19 @@ $evLab = static function (string $text, string $for = '', bool $star = false) us
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?= $evLab('重复次数(含第一次)', 'ev-rep-count') ?>
|
||||
<div id="ev-rep-count-box" hidden>
|
||||
<?php echo $evRowOpen('', !$evRepeating || (int)$evRp['count'] <= 0); ?>
|
||||
<?= $evLab('重复次数', 'ev-rep-count') ?>
|
||||
<div id="ev-rep-count-box"<?= ($evRepeating && (int)$evRp['count'] > 0) ? '' : ' hidden' ?>>
|
||||
<input class="input" type="number" min="1" max="2000" id="ev-rep-count" name="rep_count"
|
||||
value="<?= (int)($rp['count'] ?: 10) ?>">
|
||||
value="<?= (int)($evRp['count'] ?: 10) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php echo $evRowOpen(); ?>
|
||||
<?php echo $evRowOpen('', !$evRepeating || (string)$evRp['until'] === ''); ?>
|
||||
<?= $evLab('重复到', 'ev-rep-until') ?>
|
||||
<div id="ev-rep-until-box" hidden>
|
||||
<div id="ev-rep-until-box"<?= ($evRepeating && (string)$evRp['until'] !== '') ? '' : ' hidden' ?>>
|
||||
<input class="input" type="date" id="ev-rep-until" name="rep_until"
|
||||
value="<?= e((string)($rp['until'] ?? '')) ?>">
|
||||
value="<?= e((string)($evRp['until'] ?? '')) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -193,14 +228,12 @@ $evLab = static function (string $text, string $for = '', bool $star = false) us
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<?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>
|
||||
<option value="1"<?= $tp === 1 ? ' selected' : '' ?>>空闲(别人还能约)</option>
|
||||
</select>
|
||||
</div>
|
||||
<?php /* 🔴 v44(用户 2026-10-12 第 2 条):「这段时间算忙碌,这一块直接给它去掉就行了,
|
||||
没有什么用。」⇒ 「这段时间算 / 忙碌 / 空闲」整块删除。
|
||||
⚠️ 服务端契约**没变**:`transp` 字段没随表单提交时,`!empty($_POST['transp'])`
|
||||
一律得 false(= 忙碌),与原来的默认值一致 —— 不会因为删了这一块
|
||||
而把已有日程的忙闲改掉(更新时传的是 `(int)($ev2['transp'] ?? 0)` 原值)。
|
||||
下方 script 里原来还有一段给 `ev-transp` 的联动,也一并删。 */ ?>
|
||||
|
||||
<?php echo $evRowOpen('full'); ?>
|
||||
<?= $evLab('备注', 'ev-note') ?>
|
||||
@@ -238,26 +271,37 @@ $evLab = static function (string $text, string $for = '', bool $star = false) us
|
||||
(见 app.js 的 onRange → writeDT),服务端契约不变。 */
|
||||
|
||||
/* 重复:选了周期才显示"结束条件";结束条件选"次数"才显示次数框、选"到某天"才显示日期框。
|
||||
三个框默认都 hidden —— 不重复时一个都不该出现(少即是清楚)。 */
|
||||
三行默认都 hidden —— 不重复时一个都不该出现(少即是清楚)。
|
||||
🔴 v44(用户第 2 条):藏的是**整行**(`.mv-row` / `.field`),不是行里那个 box ——
|
||||
只藏 box 的话标签还孤零零留在表单里。行 = box 的 `closest('.mv-row, .field')`。 */
|
||||
var freq = document.getElementById('ev-freq');
|
||||
var endBox = document.getElementById('ev-rep-end-box');
|
||||
var endSel = document.getElementById('ev-rep-end');
|
||||
var cntBox = document.getElementById('ev-rep-count-box');
|
||||
var untilBox = document.getElementById('ev-rep-until-box');
|
||||
function rowOf(el) {
|
||||
return el && el.closest ? el.closest('.mv-row, .field') : null;
|
||||
}
|
||||
function toggle(el, on) {
|
||||
if (!el) { return; }
|
||||
el.hidden = !on;
|
||||
var row = rowOf(el);
|
||||
if (row) { row.hidden = !on; } // 连带整行一起显隐
|
||||
}
|
||||
function syncRep() {
|
||||
if (!freq || !endBox) return;
|
||||
if (!freq || !endBox) { return; }
|
||||
var repeating = freq.value !== '';
|
||||
endBox.hidden = !repeating;
|
||||
toggle(endBox, repeating);
|
||||
var how = endSel ? endSel.value : 'never';
|
||||
if (cntBox) { cntBox.hidden = !repeating || how !== 'count'; }
|
||||
if (untilBox) { untilBox.hidden = !repeating || how !== 'until'; }
|
||||
toggle(cntBox, repeating && how === 'count');
|
||||
toggle(untilBox, repeating && how === 'until');
|
||||
}
|
||||
if (freq) { freq.addEventListener('change', syncRep); }
|
||||
if (endSel) { endSel.addEventListener('change', syncRep); }
|
||||
// 回填:编辑一条已有的重复日程时,把结束条件按库里的值选对
|
||||
<?php if ($rp['count'] > 0): ?>
|
||||
<?php if ((int)$evRp['count'] > 0): ?>
|
||||
if (endSel) { endSel.value = 'count'; }
|
||||
<?php elseif ($rp['until'] !== ''): ?>
|
||||
<?php elseif ((string)$evRp['until'] !== ''): ?>
|
||||
if (endSel) { endSel.value = 'until'; }
|
||||
<?php endif; ?>
|
||||
syncRep();
|
||||
@@ -277,4 +321,4 @@ $evLab = static function (string $text, string $for = '', bool $star = false) us
|
||||
})();
|
||||
</script>
|
||||
<?php unset($evF, $evAllDayDefault, $evIsAllDay, $evFields, $evAllDay, $evMv, $evMvStyle,
|
||||
$evRowOpen, $evLab, $evLabelStar); ?>
|
||||
$evRowOpen, $evLab, $evLabelStar, $evRp, $evRepeating); ?>
|
||||
|
||||
+194
-39
@@ -161,51 +161,111 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
<input type="hidden" name="back_view" value="<?= e($view) ?>">
|
||||
<input type="hidden" name="back_anchor" value="<?= e($anchor) ?>">
|
||||
<div class="cal-modal-body">
|
||||
<div class="field full">
|
||||
<label for="ce-cal">放在哪本</label>
|
||||
<?php /* v24 第 21 批 第 7 条:「选择日历本,带上颜色」。
|
||||
⚠️ 原生 <select> 的 <option> **画不出色块**(浏览器不让你在
|
||||
option 里放元素,background 也基本不生效)→ 所以做成
|
||||
「色块 + 下拉」并排:左边一块跟着当前选中的本子变色,
|
||||
右边还是那个原生下拉(保持零依赖、手机上也能滚)。
|
||||
切换时由下面那段 JS 同步色块颜色。 */
|
||||
/* 🔴 v24 第 21 批 第 7 条:**默认必须选"我自己的普通日历本"**。
|
||||
原来直接取 `$writable[0]` —— 而顺序是按 id,观影 / 旅行两本
|
||||
通常排在最前面。于是用户打开"新建日程"、直接打字、点"加上",
|
||||
建出来的其实是一条**观影记录**(或旅程)。
|
||||
这不是"显示不对",是**建错东西**,而且事后很难发现
|
||||
—— 用户以为日历里多了条日程,其实观影列表里多了一条记录。
|
||||
所以:优先把自己建的 kind='custom' 本子排在前面当默认;
|
||||
没有普通本时才退到第一个可写的本子。 */
|
||||
$firstCal = null;
|
||||
foreach ($writable as $c) {
|
||||
if ((string)($c['kind'] ?? '') === 'custom') { $firstCal = $c; break; }
|
||||
}
|
||||
if ($firstCal === null) { $firstCal = $writable ? $writable[0] : null; }
|
||||
/* ⚠️ 模板里**再兜一道**"快照":路由只会在**页面级**补一次本子,
|
||||
而这张表单是页面渲染时定格的 —— 万一补本失败(DB 异常等),
|
||||
这里至少还能看出"没有可写的普通本",不至于把"选中了观影本"
|
||||
当成正常状态。 */
|
||||
$hasPersonal = $firstCal !== null
|
||||
&& (string)($firstCal['kind'] ?? '') === 'custom'; ?>
|
||||
<div class="cal-book-row">
|
||||
<span class="cal-book-dot" id="ce-cal-dot"
|
||||
style="background: <?= e((string)($firstCal['color'] ?? '#4A7C82')) ?>;"></span>
|
||||
<select class="select" id="ce-cal" name="calendar_id">
|
||||
<?php /* ⚠️ v44:`.mv-grid` 在这里就打开 —— 下面的「日历本」行
|
||||
与「日期范围」字段块都要走观影式排版(标签右对齐 + 冒号)。
|
||||
原来 `.mv-grid` 是包在字段块那一小段里的,日课本在它**外面**,
|
||||
于是"日历本:下拉"落不到这套排版上(正是用户报的"不在一行")。 */ ?>
|
||||
<div class="mv-grid">
|
||||
<?php /* 🔴🔴 v44(用户 2026-10-12 第 1 条):「新建日程,放在哪本,改成日历本后面加冒号。
|
||||
现在这句话和日历本下拉框还有前面的颜色,仍然不在一行,
|
||||
我建议把颜色放在下拉框里面,就跟电影的影院选择一样,
|
||||
让日历本这三个字和下拉框在一条线上。」
|
||||
|
||||
三件事:
|
||||
① 标签改「日历本」(用户点名要这三个字),冒号由观影式排版的
|
||||
`.mv-row > .mv-lb::after` 给(与观影/任务一致);
|
||||
② **颜色不再是下拉框外的一个色块**,而是像"影院选择"那样
|
||||
**画进下拉里**(每行左边一个该本子的色点)。
|
||||
原生 `<option>` 画不出元素 ⇒ 换成**自定义下拉**
|
||||
(复用全站的 `.cs` 组件:按钮 + 弹出列表 + 搜索),
|
||||
真值仍写进隐藏的 `<input name="calendar_id">`(表单契约不变)。
|
||||
③ 因为换成了一个自绘的下拉,三者天然在同一行 ——
|
||||
不再有"标签一行、色块+下拉下一行"的错位。
|
||||
|
||||
⚠️ 无 JS 兜底:`.cs` 里**仍然保留**一个原生 `<select name="calendar_id">`
|
||||
(视觉收起、但它是真的提交字段)—— JS 没跑时它就是那个下拉,
|
||||
与 `_datetime.php` 的"隐藏 date 当值载体"是同一套思路
|
||||
(v43 那条"没有盒子的元素定位无效"的教训:这里用 `position:absolute;
|
||||
width:1px` 而非 `display:none`,保留可聚焦、可提交)。
|
||||
⚠️ 组件 id 保持 `cinemaPicker/csBtn/csMenu/csDot/csText` **不合适**
|
||||
(那是观影的编号)—— 用 `calPicker/cb*` 一套独立的,
|
||||
免得将来两处互相 getElementById 抢。
|
||||
⚠️ e2e / 断言里按 `#ce-cal` 定位的写法**必须继续成立**:
|
||||
原生 select 的 id 保持 `ce-cal`(只是被视觉收起)。 */ ?>
|
||||
|
||||
<?php /* 🔴 v24 第 21 批 第 7 条:**默认必须选"我自己的普通日历本"**。
|
||||
原来直接取 `$writable[0]` —— 而顺序是按 id,观影 / 旅行两本
|
||||
通常排在最前面。于是用户打开"新建日程"、直接打字、点"加上",
|
||||
建出来的其实是一条**观影记录**(或旅程)。
|
||||
这不是"显示不对",是**建错东西**,而且事后很难发现
|
||||
—— 用户以为日历里多了条日程,其实观影列表里多了一条记录。
|
||||
所以:优先把自己建的 kind='custom' 本子排在前面当默认;
|
||||
没有普通本时才退到第一个可写的本子。 */
|
||||
$firstCal = null;
|
||||
foreach ($writable as $c) {
|
||||
if ((string)($c['kind'] ?? '') === 'custom') { $firstCal = $c; break; }
|
||||
}
|
||||
if ($firstCal === null) { $firstCal = $writable ? $writable[0] : null; }
|
||||
/* ⚠️ 模板里**再兜一道**"快照":路由只会在**页面级**补一次本子,
|
||||
而这张表单是页面渲染时定格的 —— 万一补本失败(DB 异常等),
|
||||
这里至少还能看出"没有可写的普通本",不至于把"选中了观影本"
|
||||
当成正常状态。 */
|
||||
$hasPersonal = $firstCal !== null
|
||||
&& (string)($firstCal['kind'] ?? '') === 'custom'; ?>
|
||||
|
||||
<div class="mv-row">
|
||||
<span class="mv-lb">日历本</span>
|
||||
<div class="cs cal-book-cs" id="calPicker" data-open="0">
|
||||
<button type="button" class="cs-btn" id="cbBtn" aria-haspopup="listbox" aria-expanded="false">
|
||||
<span class="cs-dot" id="cbDot"
|
||||
style="background: <?= e((string)($firstCal['color'] ?? '#4A7C82')) ?>;"></span>
|
||||
<span class="cs-text" id="cbText"><?= e((string)($firstCal['name'] ?? '未选择')) ?></span>
|
||||
<span class="cs-caret" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div class="cs-menu" id="cbMenu" hidden role="listbox">
|
||||
<div class="cs-list" id="cbList">
|
||||
<?php foreach ($writable as $c): $ro = \App\Calendar::isReadonlyKind((string)$c['kind']);
|
||||
$isDef = $firstCal && (int)$c['id'] === (int)$firstCal['id']; ?>
|
||||
<?php /* v24 第 21 批 第 7 条:纯派生的本子(农历/节假日/传统节日/节气)
|
||||
列出来但**禁用** —— 直接不列的话,用户会以为"我的本子丢了";
|
||||
列为可选项又会让他白选一次再吃一个错误提示。
|
||||
禁用 + 一句 title 说明,是这个取舍里最不费解的一种。
|
||||
观影 / 旅行**不禁用**:它们可写(加一条 = 建一条观影/旅程)。 */ ?>
|
||||
<button type="button" class="cs-item<?= $ro ? ' is-disabled' : '' ?>"
|
||||
role="option"<?= $ro ? ' disabled' : '' ?>
|
||||
data-id="<?= (int)$c['id'] ?>"
|
||||
data-color="<?= e((string)$c['color']) ?>"
|
||||
data-text="<?= e((string)$c['name']) ?>"
|
||||
<?= $ro ? 'title="系统自动生成,不能直接加日程"' : '' ?>>
|
||||
<span class="cs-dot" style="background: <?= e((string)$c['color']) ?>;"></span>
|
||||
<span><?= e((string)$c['name']) ?><?= $ro ? '(自动生成)' : '' ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php /* 一个都没匹配上时露出来(平时 hidden,不占高度) */ ?>
|
||||
<div class="cs-empty" id="cbEmpty" hidden>没有匹配的日历本</div>
|
||||
</div>
|
||||
<?php /* ⚠️⚠️ 这是**真的提交字段**(不是"只给 JS 用"的影子):
|
||||
自定义下拉只是它的外观;JS 没跑时它就是那个原生下拉。
|
||||
视觉收起用 position:absolute + 1px(**不用 display:none**)——
|
||||
后者会让表单校验时不可聚焦(v37 那次的坑)。
|
||||
桌面无 JS 时它贴在按钮下方 1px 处、可点,仍能选。 */ ?>
|
||||
<select class="select cal-book-native" id="ce-cal" name="calendar_id" aria-label="日历本">
|
||||
<?php foreach ($writable as $c): $ro = \App\Calendar::isReadonlyKind((string)$c['kind']);
|
||||
$isDef = $firstCal && (int)$c['id'] === (int)$firstCal['id']; ?>
|
||||
<?php /* v24 第 21 批 第 7 条:纯派生的本子(农历/节假日/传统节日/节气)
|
||||
列出来但**禁用** —— 直接不列的话,用户会以为"我的本子丢了";
|
||||
列为可选项又会让他白选一次再吃一个错误提示。
|
||||
禁用 + 一句 title 说明,是这个取舍里最不费解的一种。
|
||||
观影 / 旅行**不禁用**:它们可写(加一条 = 建一条观影/旅程)。 */ ?>
|
||||
<option value="<?= (int)$c['id'] ?>"
|
||||
data-color="<?= e((string)$c['color']) ?>"
|
||||
<?= $isDef ? ' selected' : '' ?>
|
||||
<?= $ro ? 'disabled title="系统自动生成,不能直接加日程"' : '' ?>><?= e((string)$c['name']) ?><?= $ro ? '(自动生成)' : '' ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php /* 兼容旧断言/旧 JS:外面那个独立色块曾经叫 ce-cal-dot。
|
||||
现在颜色已经画进下拉里、不再需要它 —— 保留一个**隐藏**的同名元素,
|
||||
免得旧探针 document.getElementById('ce-cal-dot').style 直接抛错。 */ ?>
|
||||
<span class="cal-book-dot" id="ce-cal-dot" hidden
|
||||
style="background: <?= e((string)($firstCal['color'] ?? '#4A7C82')) ?>;"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$hasPersonal): ?>
|
||||
<span class="tip" style="display:block;margin-top:6px">
|
||||
⚠️ 你还没有**自己的日程本**,上面选的是一本"派生本"
|
||||
@@ -213,7 +273,6 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
<a href="<?= e(url('calendar/calendars')) ?>">日历本</a> 建一本吧。
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php /* v24 第 6 条:字段按 iCalendar / CalDAV 的 VEVENT 定(见 _event_fields.php)。
|
||||
新建时「全天」默认**不勾**(用户原话)。开始时间默认:
|
||||
带了 new_at 就用它(点日历的那一格),否则今天 09:00。
|
||||
@@ -233,12 +292,14 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
$evFields = ['starts_at' => $defDay . ' ' . $defHM . ':00',
|
||||
'ends_at' => $defDay . ' ' . $defEndH . ':' . substr($defHM, 3, 2) . ':00'];
|
||||
$evAllDay = false;
|
||||
/* ⚠️ v44:`.mv-grid` 容器**已经在上面日课本那一行之前打开了**
|
||||
(日课本自己也要走观影式排版)—— 这里只 require 字段块,
|
||||
不要再包一层,否则会出现嵌套的第二个 `.mv-grid`。 */
|
||||
$evMvStyle = true; // 观影式排版(用户第 5 条)
|
||||
echo '<div class="mv-grid">';
|
||||
require __DIR__ . '/_event_fields.php';
|
||||
echo '</div>';
|
||||
unset($evMvStyle);
|
||||
?>
|
||||
</div><?php /* /mv-grid */ ?>
|
||||
</div><?php /* /cal-modal-body */ ?>
|
||||
<div class="cal-modal-foot">
|
||||
<span class="cal-modal-tip" id="calAddTip"></span>
|
||||
@@ -558,6 +619,100 @@ $headText = $view === 'month' ? date('Y 年 n 月', $ts)
|
||||
syncDot();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
🔴 v44(用户 2026-10-12 第 1 条):日历本自绘下拉(色点画进下拉里)
|
||||
------------------------------------------------------------
|
||||
与观影的影院下拉同一套交互,但**状态写在选中的原生 `<select>` 上**
|
||||
(写 `.value` + 派发 change)—— 这样无 JS 兜底、既有断言(读 `#ce-cal`)
|
||||
与"提交契约"全都不用改:自定义下拉只是它的**外观**。
|
||||
|
||||
⚠️ 菜单 `.cs-menu` 是 `position:absolute`,而它**居中面板的滚容器里**
|
||||
(`.cal-modal { overflow-y:auto }`)。要看它在低端会不会被裁掉 ——
|
||||
真机几何断言里会量(见论文式验证)。
|
||||
============================================================ */
|
||||
(function () {
|
||||
var picker = document.getElementById('calPicker');
|
||||
var sel = document.getElementById('ce-cal');
|
||||
if (!picker || !sel) { return; }
|
||||
var btn = document.getElementById('cbBtn');
|
||||
var menu = document.getElementById('cbMenu');
|
||||
var dot = document.getElementById('cbDot');
|
||||
var text = document.getElementById('cbText');
|
||||
var list = document.getElementById('cbList');
|
||||
if (!btn || !menu || !dot || !text || !list) { return; }
|
||||
|
||||
/* 把 options 重建成列表项 —— 单一真值来源是那个 `<select>`,
|
||||
所以这里不需要模板另给一份数据、也不会两边不同步。 */
|
||||
function build() {
|
||||
list.innerHTML = '';
|
||||
Array.prototype.forEach.call(sel.options, function (o) {
|
||||
var b = document.createElement('button');
|
||||
b.type = 'button';
|
||||
b.className = 'cs-item' + (o.disabled ? ' is-disabled' : '');
|
||||
b.setAttribute('role', 'option');
|
||||
if (o.disabled) { b.disabled = true; b.title = o.getAttribute('title') || ''; }
|
||||
b.setAttribute('data-id', o.value);
|
||||
b.setAttribute('data-color', o.getAttribute('data-color') || '');
|
||||
b.setAttribute('data-text', o.textContent || '');
|
||||
var d = document.createElement('span');
|
||||
d.className = 'cs-dot';
|
||||
d.style.background = o.getAttribute('data-color') || '';
|
||||
var s = document.createElement('span');
|
||||
s.textContent = o.textContent || '';
|
||||
b.appendChild(d); b.appendChild(s);
|
||||
list.appendChild(b);
|
||||
});
|
||||
}
|
||||
function paint() {
|
||||
var o = sel.options[sel.selectedIndex];
|
||||
if (!o) { return; }
|
||||
text.textContent = o.textContent || '';
|
||||
var c = o.getAttribute('data-color') || '';
|
||||
if (c) { dot.hidden = false; dot.style.background = c; } else { dot.hidden = true; }
|
||||
}
|
||||
function closeMenu() {
|
||||
menu.hidden = true;
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
picker.setAttribute('data-open', '0');
|
||||
}
|
||||
function openMenu() {
|
||||
menu.hidden = false;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
picker.setAttribute('data-open', '1');
|
||||
}
|
||||
|
||||
build();
|
||||
paint();
|
||||
/* ⚠️ 原生 select 的 change 会由别处(旧 syncDot、无 JS 路径)触发,
|
||||
这里也监听一次,保证显示与真值永远一致。 */
|
||||
sel.addEventListener('change', paint);
|
||||
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (menu.hidden) { openMenu(); } else { closeMenu(); }
|
||||
});
|
||||
menu.addEventListener('click', function (e) {
|
||||
var it = e.target.closest ? e.target.closest('.cs-item') : null;
|
||||
if (!it || it.disabled) { return; }
|
||||
e.stopPropagation();
|
||||
sel.value = it.getAttribute('data-id');
|
||||
sel.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
closeMenu();
|
||||
});
|
||||
/* 点空白处收起(用 document 监听 + 判定 inside,不额外造遮罩节点)
|
||||
⚠️ 必须在**捕获阶段**判定:菜单项的 click 会 stopPropagation,
|
||||
但那是冒泡阶段 —— 捕获阶段先于它,这里判"点在不在 picker 里"
|
||||
就够精确了(点在菜单里 → 不收)。 */
|
||||
document.addEventListener('click', function (e) {
|
||||
if (menu.hidden) { return; }
|
||||
if (picker.contains(e.target)) { return; }
|
||||
closeMenu();
|
||||
}, true);
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && !menu.hidden) { closeMenu(); }
|
||||
});
|
||||
})();
|
||||
|
||||
var form = document.getElementById('calAddForm');
|
||||
var tip = document.getElementById('calAddTip');
|
||||
if (!form || !card || !window.fetch) { return; }
|
||||
|
||||
Reference in New Issue
Block a user