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:
2026-10-12 15:31:03 +08:00
parent 8c651b43eb
commit 075ac6bbeb
21 changed files with 2886 additions and 185 deletions
+48 -3
View File
@@ -38,6 +38,13 @@
* $dtRange 'start' | 'end' —— 这对控件属于同一个起止范围(见下)
* $dtDateType 'select'(默认,年/月/日下拉)| 'native'(旧的原生 date 框)
* $dtYearFrom / $dtYearTo 年份下拉的范围(默认:今年 -120 ~ 今年 +10;生日建议 1900)
* $dtLabel 🔴 v44:把标签**内联到控件同一行**(写成该字符串 + 「:」)。
* · 传空串 = 不要标签(起止范围里的第二个端点就该留空);
* · 不传(null)= 沿用旧行为(标签由宿主模板自己放,通常在上方一行)。
* 为什么要它:用户 2026-10-12 第 2 条要求「日期范围:」与最新控件
* **在同一行**。宿主的 `.mv-grid` 行结构把标签与控件分在两行,
* 与其在 CSS 里跟行结构较劲,不如让控件**自带**标签 —— 位置确定。
* $dtLabelStar 标签后是否跟一颗必填星号(配 $dtLabel 用)
* $dtPlugin 🔴 v38 插件开关:'antd' = 由 app.js 把这两个端点升级成
* **Ant Design 的 RangePicker**(用户 2026-10-10 点名要的插件)。
* ⚠️ 只用在**起止成对**的两个端点上(见下),且**只在观影模块试点**;
@@ -62,12 +69,29 @@ $dtYearTo = (int)($dtYearTo ?? ((int)date('Y') + 10));
留空 = 普通单点控件。 */
$dtRange = (string)($dtRange ?? '');
/* 🔴 v44:内联标签(见文件头)。`null`(不传)与 `''`(显式不要)语义不同,
所以这里不能用 `??` —— 得先看有没有这个键。 */
$dtHasLabel = array_key_exists('dtLabel', get_defined_vars());
$dtLabel = $dtHasLabel ? (string)$dtLabel : '';
$dtLabelStar = (bool)($dtLabelStar ?? false);
/* 🔴 v38:插件开关(见文件头)。只在**起止成对**的场景下有意义 ——
RangePicker 天生就是"一个控件管两端",单点控件挂它反而多一个"确定"。
⚠️ 即便声明了插件,**原生控件一个都不删**:JS 没跑 / 资源拉不动时,
用户看到的仍是那套自建下拉(渐进增强的老规矩)。 */
$dtPlugin = (string)($dtPlugin ?? '');
$dtUseAntd = ($dtPlugin === 'antd' && $dtRange !== '');
/* 🔴 v44:单个**带时刻**的 AntD 控件(任务的「截止时间」)。
`$dtPlugin = 'antd-single'` ⇒ app.js 把这一档升级成 **AntD 的 DatePicker(showTime)**
(不是 RangePicker —— 截止时间只有一个时刻,用范围控件会多一个空端点)。
用途(用户 2026-10-12 第 5 条):「截止时间也用我们最新的日历控件,
记得把现在这个控件删掉。」 */
$dtUseAntdSingle = ($dtPlugin === 'antd-single' && $dtRange === '');
/* 🔴 v44:容器类与 id —— 让"日程"这对能用与"观影"完全一致的结构
(`.dt-range > .dt-input` 直接子),从而命中那条宽泛的收起规则(见 CSS)。
不传 `$dtClass` 时维持旧行为(`.dt-input` 直接挂在 `.dt-range` / `.field` 下)。 */
$dtClass = (string)($dtClass ?? '');
$dtFieldId = (string)($dtFieldId ?? '');
/* ⚠️⚠️ 临时变量**一律带 dt 前缀**,一个都不能漏:这里原来写成
`$t = strtotime(...)`,把宿主模板里表示「这条任务 / 这条记录」的 `$t`
@@ -88,10 +112,30 @@ $dtSec = $dtTs ? (int)date('s', $dtTs) : 0;
.dt-semi 这个占位 span 保留 —— 老代码路径与 CSS 还在引用它,删了会牵连。 */
$dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired,
'id' => $dtId, 'range' => $dtRange, 'parts' => $dtUseParts || null,
'plugin' => $dtUseAntd ? 'antd' : null];
'plugin' => $dtUseAntd ? 'antd' : ($dtUseAntdSingle ? 'antd-single' : null)];
?>
<div class="dt-input<?= $dtRange !== '' ? ' dt-part' : '' ?><?= $dtUseParts ? ' dt-parts' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
<?php /* ⚠️ v44:内联标签是**兄弟节点**、排在 `.dt-input` **之前** ——
绝不放进 `.dt-input` 里面。
血泪(真机几何量出来的):antd 挂上后那条"收起原生端点"的规则会把
整个 `.dt-input` 绝对定位 + `clip` 掉(因为那里装着要藏起来的原生下拉);
标签若在它内部 ⇒ **跟着一起被裁没**,用户只看到一个没有标签的光控件
(实测:标签盒还在 x=316 但 `elementFromPoint` 命中的是 `.ant-picker`)。
放成兄弟之后,收起规则只影响端点本身,标签安然无恙、
并且与 `_datetime.php` 外面的 `.mv-row` / `.dt-range` flex 容器天然同行。 */ ?>
<?php if ($dtHasLabel && $dtLabel !== ''): ?>
<?php /* ⚠️ 标签上**不带** `$dtClass`:那个类名里含 `field`/`ev-dt`,
而 `.dt-range.dt-cols > .field { flex: 1 1 250px }`(0,3,0)会盖过
`.dt-range > .dt-inline-lab { flex: 0 0 62px }`(0,2,0)——
实测标签被撑成 261px,把最新控件挤到右边去("不在同一行左侧"的观感)。 */ ?>
<span class="dt-inline-lab mv-lb"><?= e($dtLabel) ?><?= $dtLabelStar ? '<i>*</i>' : '' ?></span>
<?php endif; ?>
<?php /* ⚠️ v44:不再套外层 `.field` —— 那会把 `.dt-input` 变成**孙子**,
于是 `> .dt-input` 那条宽泛的收起规则失效(日程"旧控件还在"就是这么来的)。
`$dtClass` 是**额外类名**,`.dt-input` 永远是本体;`$dtFieldId` 直接落在它身上。 */ ?>
<div class="dt-input<?= $dtClass !== '' ? ' ' . e($dtClass) : '' ?><?= $dtRange !== '' ? ' dt-part' : '' ?><?= $dtUseParts ? ' dt-parts' : '' ?><?= $dtRange === 'start' ? ' is-pending' : '' ?>"
<?= $dtFieldId !== '' ? 'id="' . e($dtFieldId) . '"' : '' ?>
<?= $dtUseAntd ? 'data-dt-antd="1"' : '' ?>
<?= $dtUseAntdSingle ? 'data-dt-antd-single="1"' : '' ?>
data-dt="<?= e(json_encode($dtCfg, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT)) ?>"
<?= $dtRequired ? 'data-dt-required="1"' : '' ?>>
<span class="dt-semi" aria-hidden="true"></span>
@@ -160,7 +204,8 @@ $dtCfg = ['name' => $dtName, 'seconds' => $dtSeconds, 'required' => $dtRequired,
</div>
<?php /* 用完即清,避免残留变量影响下一次 include */ unset(
$dtName, $dtValue, $dtRequired, $dtId, $dtSeconds, $dtCfg, $dtRange,
$dtDateType, $dtUseParts, $dtYearFrom, $dtYearTo, $dtPlugin, $dtUseAntd,
$dtDateType, $dtUseParts, $dtYearFrom, $dtYearTo, $dtPlugin, $dtUseAntd, $dtUseAntdSingle,
$dtHasLabel, $dtLabel, $dtLabelStar, $dtClass, $dtFieldId,
$dtTs, $dtTsTmp, $dtDate, $dtY, $dtMo, $dtD, $dtDays,
$dtHour, $dtMin, $dtSec,
$dtH, $dtM, $dtS, $hv, $mv, $sv, $dtYy, $dtMm, $dtDd
+93 -49
View File
@@ -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
View File
@@ -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; }
+21 -6
View File
@@ -218,14 +218,24 @@ if ($timed) {
下面的 `$nowH` 是当前小时(服务端时区,与页面口径一致),
把它并进区间端点 —— 于是红线必然落在区间内,定位也就有了正确的目标。
⚠️ 必须在**算 $nowH 之前**完成这一步(原代码把 $nowH 放在区间之后算,
这里把它提上来,避免"改了区间却没重新算位置"这种漏改)。 */
这里把它提上来,避免"改了区间却没重新算位置"这种漏改)。
🔴🔴 v44(用户 2026-10-12 第 3 条):「首页上的红线刻度显示不准确。
现在明明已经 7:44 了,但它那个红线还停留在最早的 7 点钟,
并没有走到 7:44 那个位置。这个只有在首页上是这样的。我点到日程里面,
那个红线的刻度好像是对的。」
⇒ 真根因:首页用的是 `date('G')`(**只取整小时**),而日程页用的是
`(int)date('G')*60 + (int)date('i')`(**精确到分**)。
7:44 时首页算出 7 → 红线停在 7:00,正是用户看到的。
⇒ 这里改成与日程页**同一套**(小时*60+分钟),两处的口径不再分叉。 */
$nowHNow = (int)date('G');
if ($nowHNow < $startH) { $startH = max(0, $nowHNow - 1); }
if ($nowHNow >= $endH) { $endH = min(24, $nowHNow + 2); }
if ($endH <= $startH) { $endH = min(24, $startH + 2); }
}
$nowH = (int)date('G');
$nowPos = ($nowH - $startH) / max(1, ($endH - $startH)); // 红线位置(0~1)
/* 红线位置:**分钟级**(与日程页同一算法)。见上面 v44 那条 —— 原来是 `date('G')`
(整小时),所以"7:44 的红线停在 7:00"。 */
$nowMinutes = ((int)date('G')) * 60 + (int)date('i');
$nowPos = (($nowMinutes / 60) - $startH) / max(1, ($endH - $startH)); // 红线位置(0~1)
$showNow = $nowPos >= 0 && $nowPos <= 1;
/* 🔴 v30 第 2/4 条:首页日程也用**和日程模块同一套冲突分列**。
@@ -304,10 +314,15 @@ unset($evRef);
</div>
<?php endfor; ?>
<?php /* 当前时刻红线:用户原话「需要在当前的时间或者是时刻那里显示一条红线」 */ ?>
<?php /* 🔴 v44(用户 2026-10-12 第 3 条):「红线左侧是有一个红点吗?
这个红点显示也不全,也不太好看。直接把红点去掉就行了,
直接一根红线就可以了。」
⇒ 那颗 `.ag-now-dot` 整个删掉(原来它是个 8×8 的圆,左端 `left:-4px`,
靠外溢一点点来"指示起点";用户觉得显示不全又不好看)。
⚠️ CSS 里那条 `.ag-now-dot` 也一并删(留在那儿是死规则,
还会让"红线只有一根"这件事在样式表里看不出结论)。 */ ?>
<?php if ($showNow): ?>
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%">
<span class="ag-now-dot"></span>
</div>
<div class="ag-now" style="top: <?= number_format($nowPos * 100, 2) ?>%"></div>
<?php endif; ?>
<?php /* 日程块:按"相对第一个刻度的小时数"定位(与日视图同一套比例)---- */ ?>
<?php
+93
View File
@@ -149,6 +149,99 @@ $rosterCount = $rosterCount ?? 0;
<?php endif; ?>
</div>
<!-- ==================== 移动端 API 令牌(v44 用户第 6 条)==================== -->
<?php /* 用户原话:「我们现在马上要开始移动终端的开发,所以现在要开发所有接口。」
这张卡是**发令牌**的地方 —— 没有它,App 连不上(接口做完了也没入口)。
为什么与 DAV 口令**分开**:
· DAV 那把是给第三方同步客户端(DAVx⁵ / iOS 日历)的,走 Basic 鉴权、
明文要手打进手机、一把管日历+通讯录;
· API 令牌是给**自家 App** 的,走 `Authorization: Bearer`、可多枚
(一台设备一枚)、可逐枚吊销、可设有效期、带设备名。
两件事的生命周期完全不同(吊销 DAV 口令会连带断掉日历同步;
吊销一枚 App 令牌不该影响别人的手机)。 */ ?>
<div class="card card-pad" id="api" style="margin-top:20px">
<h2 class="section-title">
App 接口令牌
<span class="hint">移动端 / 第三方程序调用用</span>
</h2>
<p class="small muted" style="margin:0 0 14px">
接口地址前缀 <span class="mono"><b><?= e((string)$apiBase) ?>/api/v1/</b></span>,
请求头带 <span class="mono">Authorization: Bearer &lt;令牌&gt;</span>。
先调 <span class="mono">GET <?= e((string)$apiBase) ?>/api/v1/ping</span> 探连通(免令牌)。
<br>完整接口清单见仓库里的 <span class="mono">docs/API.md</span>。
</p>
<div class="form-grid">
<div class="field full">
<label>接口根地址</label>
<input class="input mono" readonly value="<?= e((string)$apiBase . '/api/v1') ?>">
</div>
<div class="field">
<label>用户名 / 手机号</label>
<input class="input mono" readonly value="<?= e((string)$person['name']) ?>">
<span class="tip">令牌本身就是凭据,这一行只是给你对账用。</span>
</div>
</div>
<?php if (!empty($apiPlain)): ?>
<p class="tip" style="background:#FFF6E5;border-color:#E8C98A;padding:10px 12px;border-radius:8px">
<b>这是你的新令牌,只显示这一次,请立刻复制到 App 里:</b>
<?php if ((string)$apiLabel !== ''): ?>
(设备名:<?= e((string)$apiLabel) ?>)
<?php endif; ?>
<br><code class="mono" style="font-size:14px;word-break:break-all"><?= e((string)$apiPlain) ?></code>
</p>
<?php endif; ?>
<?php if (!empty($apiTokens)): ?>
<table class="res-table" style="width:100%;margin:6px 0 10px">
<thead>
<tr><th>设备名</th><th>签发时间</th><th>最近使用</th><th></th></tr>
</thead>
<tbody>
<?php foreach ($apiTokens as $tk): ?>
<tr>
<td><?= e((string)($tk['label'] ?: '(未命名)')) ?></td>
<td class="small muted"><?= e((string)($tk['created_at'] ?? '')) ?></td>
<td class="small muted"><?= e((string)($tk['last_used_at'] ?? '还没用过')) ?></td>
<td>
<form method="post" action="<?= e(url('settings/api_revoke')) ?>" class="res-inline"
onsubmit="return confirm('吊销这枚令牌?这台设备会立刻掉线。')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$tk['id'] ?>">
<button class="btn btn-ghost btn-sm" type="submit">吊销</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="small muted">还没有令牌。给每台设备发一枚吧。</p>
<?php endif; ?>
<?php if (\App\Auth::canWrite()): ?>
<div class="btn-row">
<form method="post" action="<?= e(url('settings/api_token')) ?>" class="res-inline">
<?= csrf_field() ?>
<input class="input" name="label" placeholder="设备名(如:我的手机)" style="max-width:200px">
<button class="btn btn-sm" type="submit">生成新令牌</button>
</form>
<?php if (!empty($apiTokens)): ?>
<form method="post" action="<?= e(url('settings/api_revoke')) ?>" class="res-inline"
onsubmit="return confirm('吊销全部令牌?所有 App 都要重新登录。')">
<?= csrf_field() ?>
<button class="btn btn-ghost btn-sm" type="submit">吊销全部</button>
</form>
<?php endif; ?>
</div>
<p class="small muted" style="margin:10px 0 0">
令牌只在生成那一次显示明文(库里只存哈希)。手机丢了就吊销那一枚。
</p>
<?php endif; ?>
</div>
<!-- ==================== 天气地点(用户 2026-10-03 第 1 条)==================== -->
<?php /* 原话:「天气这个这样吧,让用户在个人设置中填写自己的城市。」
所以地点从系统设置搬到了这里 —— 而且改成**城市名**,不是经纬度:
+22 -18
View File
@@ -36,19 +36,23 @@ $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>
<?php /* 🔴 v44(用户 2026-10-12 第 5 条):编辑表单也统一成观影式排版 +
最新时间控件 + 文案精简(与「新建任务」弹框一致)——
同一件事在两个入口长得不一样,用户会以为"进了别的页面"。 */ ?>
<div class="mv-grid">
<div class="mv-row">
<span class="mv-lb">任务名称<i>*</i></span>
<input class="input" id="tk-title" name="title" required value="<?= e((string)($tkT['title'] ?? '')) ?>">
</div>
<div class="field">
<label>截止时间(留空=不上日历)</label>
<div class="mv-row">
<span class="mv-lb">截止时间</span>
<?php /* 🔴 v44:截止时间用 AntD 单点控件(与新建同一套);括号去掉。 */ ?>
<?php $dtName = 'due_at'; $dtValue = $tkT['due_at'] ?? null;
$dtRequired = false;
$dtRequired = false; $dtPlugin = 'antd-single';
require __DIR__ . '/../_datetime.php'; ?>
</div>
<div class="field">
<label>负责人</label>
<div class="mv-row">
<span class="mv-lb">负责人</span>
<?php /* 第 8 条同一诉求:负责人也是「选人」,人多时用弹窗 + 首字母更快。
保留原下拉的「不指定」(pp-item-none + value=0)。 */ ?>
<?php
@@ -63,8 +67,8 @@ $tkEmbed = !empty($tkEditEmbed);
require __DIR__ . '/../_person_picker.php';
?>
</div>
<div class="field">
<label>循环</label>
<div class="mv-row">
<span class="mv-lb">循环</span>
<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' : '' ?>>
@@ -73,8 +77,8 @@ $tkEmbed = !empty($tkEditEmbed);
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label>挂到哪个父任务下</label>
<div class="mv-row">
<span class="mv-lb">父任务</span>
<select class="select" name="parent_id">
<option value="0">(这是一项独立的任务)</option>
<?php foreach ($tkParents as $p): ?>
@@ -85,8 +89,8 @@ $tkEmbed = !empty($tkEditEmbed);
</select>
</div>
<?php /* v24 第 9、10 条:分类与看板(看板把任务放到哪一块,分类是标签) */ ?>
<div class="field">
<label>分类</label>
<div class="mv-row">
<span class="mv-lb">分类</span>
<select class="select" name="category_id">
<option value="0">未分类</option>
<?php foreach ($tkCats as $c): ?>
@@ -96,8 +100,8 @@ $tkEmbed = !empty($tkEditEmbed);
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label>看板</label>
<div class="mv-row">
<span class="mv-lb">看板</span>
<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' : '' ?>>
@@ -106,8 +110,8 @@ $tkEmbed = !empty($tkEditEmbed);
<?php endforeach; ?>
</select>
</div>
<div class="field full">
<label for="tk-note">备注</label>
<div class="mv-row">
<span class="mv-lb">备注</span>
<textarea class="input" id="tk-note" name="note" rows="3"><?= e((string)($tkT['note'] ?? '')) ?></textarea>
</div>
</div>
+60 -46
View File
@@ -89,57 +89,72 @@ $lastColIdx = count($colList) - 1;
`back=tasks` 走白名单。 */ ?>
<input type="hidden" name="back" value="tasks">
<div class="cal-modal-body">
<div class="form-grid">
<div class="field full">
<label for="tk-title">要做什么 *</label>
<input class="input" id="tk-title" name="title" required placeholder="如:周末大扫除">
<?php /* 🔴 v44(用户 2026-10-12 第 5 条):「新建任务页面的排版还没有统一,
应该把标题放在前面,树框放在后面。」
⇒ 用观影/行程/日程那套 **`.mv-grid`**(标签 62px 右对齐 + CSS `::after` 冒号 +
控件占满剩余宽度),标题那一行在最前、其余字段依次跟上 ——
"标题在前、后面的框跟上"就是这套排版的自然顺序。 */ ?>
<div class="mv-grid">
<div class="mv-row">
<span class="mv-lb">任务名称<i>*</i></span>
<input class="input" id="tk-title" name="title" required
placeholder="如:周末大扫除">
</div>
<?php /* v24 第 5 条:截止时刻也走全局统一的「日期 + 时分秒」控件。
秒这一档保留(与其它模块同一口径),默认 23:59:59 —— "这天之前做完"
比"这天 00:00 之前"更符合直觉。 */ ?>
<div class="field">
<label for="tk-due">截止时间(有才会出现在日历里)</label>
比"这天 00:00 之前"更符合直觉。
🔴 v44(用户第 5 条):「截止时间也用我们最新的日历控件,
记得把现在这个控件删掉。」
⇒ 换成 AntD 的**单点** DatePicker(带时刻):
`$dtPlugin='antd-single'`(见 app.js 的 mountAntdSingleDT)。
原生那几档下拉由 CSS **真删掉**(不是 clip 藏起来)。
标签去掉那个括号(见下一条)。 */ ?>
<div class="mv-row">
<span class="mv-lb">截止时间</span>
<?php $dtName = 'due_at'; $dtId = 'tk-due'; $dtValue = null;
$dtRequired = false;
$dtRequired = false; $dtPlugin = 'antd-single';
require __DIR__ . '/../_datetime.php'; ?>
</div>
<div class="field">
<label for="tk-assign">负责人</label>
<?php /* 🔴 v44(用户第 5 条):「负责人默认是自己。」
⇒ 默认选中当前登录的人(原来 `$ppSelected = []`,空着)。
⚠️ 用 `Auth::personId()` 取"我",与后端"负责人为空"的兜底一致。 */ ?>
<div class="mv-row">
<span class="mv-lb">负责人</span>
<?php
$ppName = 'assignee_id';
$ppPeople = $people;
$ppSelected = [];
$ppSelected = [(int)\App\Auth::personId()];
$ppMode = 'single';
$ppOpenBtn = '选择负责人';
$ppOpenBtn = '更换负责人';
$ppTitle = '谁负责这件事';
$ppEmptyText = '不指定';
$ppAllowEmpty = true;
require __DIR__ . '/../_person_picker.php';
?>
</div>
<?php /* v24 第 14 批 用户第 2 条:新建时就能定状态(默认「待办」)。
⚠️ 选「已完成»的话它不会出现在看板上(已完成已从看板撤走),
只在「任务设置 → 已完成」里能翻到 —— 这句提示必须写上,
否则用户会以为"刚建的任务丢了"。 */ ?>
<div class="field">
<label for="tk-status">状态</label>
<?php /* 🔴 v44(用户第 5 条):「状态下面选"已完成"的话,它只在"已完成"里出现,
看板上不再显示,这句话也去掉。」
⇒ 删掉那句 `.tip`(状态语义由状态徽标本身表达)。
⚠️ 用户同条要求「任务页面,任务下面有一行字…这个提示不要了」——
那是**看板顶部**那块「状态说明」(见上方 legend 那段)。 */ ?>
<div class="mv-row">
<span class="mv-lb">状态</span>
<select class="select" id="tk-status" name="status">
<?php foreach (\App\Task::STATUS as $sk => $sv): ?>
<option value="<?= e($sk) ?>" <?= $sk === 'todo' ? 'selected' : '' ?>><?= e($sv) ?></option>
<?php endforeach; ?>
</select>
<span class="tip">选「已完成」的话,它只在「已完成」里出现,看板上不再显示。</span>
</div>
<div class="field">
<label for="tk-repeat">循环</label>
<div class="mv-row">
<span class="mv-lb">循环</span>
<select class="select" id="tk-repeat" name="repeat_rule">
<?php foreach (\App\Task::REPEAT as $rk => $rv): ?>
<option value="<?= e($rk) ?>"><?= e($rv) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label for="tk-cat">分类</label>
<div class="mv-row">
<span class="mv-lb">分类</span>
<select class="select" id="tk-cat" name="category_id">
<option value="0">未分类</option>
<?php foreach ($categories as $c): ?>
@@ -148,19 +163,21 @@ $lastColIdx = count($colList) - 1;
</select>
</div>
<?php /* 用户第 7 条:看板 = 任务"归属哪摊事",新建时直接选(不再是顶上的切换条) */ ?>
<div class="field">
<label for="tk-board">看板</label>
<div class="mv-row">
<span class="mv-lb">看板</span>
<select class="select" id="tk-board" name="board_id">
<?php foreach ($boards as $b): ?>
<option value="<?= (int)$b['id'] ?>"><?= e((string)$b['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="field">
<label for="tk-share">共享给(可多选,任何人完成都算完成)</label>
<?php /* 第 8 条的同一诉求:人多时不要下拉里瞪眼找。换成弹窗 + 首字母联想。
⚠️ 候选里去掉**自己**——「共享给自己」是空操作;组件没有"排除项",
所以在传进去之前先过滤掉。 */ ?>
<?php /* 🔴 v44(用户第 5 条):「"共享给"后面有个括号,可多选,任何人完成都算完成,
这个括号里面的内容也去掉。」⇒ 标签只留「共享给」。
第 8 条的同一诉求:人多时不要下拉里瞪眼找。换成弹窗 + 首字母联想。
⚠️ 候选里去掉**自己**——「共享给自己」是空操作;组件没有"排除项",
所以在传进去之前先过滤掉。 */ ?>
<div class="mv-row">
<span class="mv-lb">共享给</span>
<?php
$ppName = 'shares[]';
$ppPeople = array_values(array_filter($people,
@@ -172,8 +189,8 @@ $lastColIdx = count($colList) - 1;
require __DIR__ . '/../_person_picker.php';
?>
</div>
<div class="field full">
<label for="tk-note">备注</label>
<div class="mv-row">
<span class="mv-lb">备注</span>
<textarea class="input" id="tk-note" name="note" rows="2"></textarea>
</div>
@@ -184,12 +201,12 @@ $lastColIdx = count($colList) - 1;
服务端 attach_save_files(..., 'attach', ...) 零改动就能收。
⚠️ 拖进来的走暂存(stage_token[]),随表单一起认领;
直接点选走 $_FILES 路径。两条路服务端都接(见 attach_save_files)。 */ ?>
<div class="field full">
<label for="sd-tk-attach">附件(可多选)</label>
<div class="mv-row">
<span class="mv-lb">附件</span>
<?php $sdName = 'attach[]'; $sdId = 'sd-tk-attach'; $sdSlot = 'photo';
require __DIR__ . '/../_stage_drop.php'; ?>
</div>
</div><?php /* /.form-grid */ ?>
</div><?php /* /.mv-grid */ ?>
</div><?php /* /.cal-modal-body */ ?>
<div class="cal-modal-foot">
<span class="cal-modal-tip" id="taskAddTip"></span>
@@ -225,17 +242,14 @@ $lastColIdx = count($colList) - 1;
</form>
<?php endif; ?>
<?php /* 状态到底是什么(用户第 6 条:「待办和进行中,有什么区别?」)——
把三个状态的一句话解释直接摆在看板上方,而不是藏在 title 里。
他问出这句话,就说明界面上没有一处讲清楚过。 */ ?>
<div class="tk-legend">
<?php foreach (\App\Task::STATUS as $sk => $sv): ?>
<span class="tk-lg-item">
<span class="kb-st is-<?= e($sk) ?> is-static"><?= e($sv) ?></span>
<span class="tk-lg-hint"><?= e(\App\Task::statusHint($sk)) ?></span>
</span>
<?php endforeach; ?>
</div>
<?php /* 🔴 v44(用户 2026-10-12 第 4 条):「任务页面,任务下面有一行字:
"待办记下来了,还没动手,排队等着进行"之类的。这个提示不要了,去掉。」
⇒ 整块「状态说明」legend **删掉**(连那三个状态徽标也一起去掉)——
用户要的是"任务下面不要再挂一行字",留着徽标仍然是一行字。
⚠️ `Task::STATUS_HINT` / `statusHint()` **保留在类里**(它是这三个状态语义的
唯一书面定义,后端/文案用得上),只是**界面不再渲染**。
⚠️ `.tk-legend` 那条 CSS 留着不动:删 CSS 会牵连缩进,而它只是空规则,
真正的约束是"模板里不再输出 TK-LEGEND 这一块"。 */ ?>
<?php /* v24 第 14 批 用户第 2 条:**每张卡上都能看到并改状态**。
⚠️ 与上面搬卡那份同理:表单全页只有一份,卡片上只放 `data-st-*`,