v24 第 10 批:名册真性能(根因是每卡内联表单)等九条

- #6 名册线上真实数据(1184 人)卡顿:根因是**每张卡片内联一整份分组弹层**——
  全页 2369 个 form / 27234 个 input / 22496 个 radio / 98100 DOM 节点,
  密码管理器(Bitwarden 等)逐个扫描 form 把主线程占死(用户报的就是这个冲突)。
  改为卡片只带 data 属性 + 全页**唯一一份**共享浮层 #rosterMenus。
  实测:form 2369→3、input 27234→25、DOM 98100→17659、解压 7694→2306 KB、
  长任务 1/52ms → 0。新增哨兵 _fb_formbloat_check.py(含反向验证)。
- #5 日视图翻页失效:day_columns() 无条件吸到周一,天视图只有 1 列 → 永远显示那周周一。
  改为仅 span>1 时吸附(周视图行为不变)。
- #2 财务「欠款」类别(负债);并修一个真 bug:负债填负数会让净资产**反向变大**。
  写入口 normalizeAmount 归一化 + summary/timeline 侧 abs 防御 + Schema v17 纠正老数据。
- #3 账目行「盘点」「修改」两个并列按钮(各自就地弹面板);图移到账目上方,
  总金额图跨整行 + 每个账目一条线 + 类别筛选条;charts() 改为覆盖全部账目。
- #4 日程新增「农历」「节假日」两本不可删除只读日历(app/Holiday.php):
  两级自动更新 —— 内置规则(公历固定 + 清明节气 + 农历节日)保证断网也有;
  官方 JSON 镜像自动抓取补齐调休。抓取**不在渲染路径联网**(响应后 fastcgi_finish_request
  + 频率闸门),缓存落 storage/cache。仅派生本不可写日程。
- #6 日程页设置按钮前加「跳到某一天」(选中即跳)。
- #7 起止范围改用一个 Semi dateTimeRange 控件(观影/旅程/节点/日程共 4 处);
  新增 window.fbSetDT / fbUpgradeRange;修范围重挂(React 非受控组件须 unmount 再 createRoot)
  与范围里 required 未摘除导致表单无法提交两个真实问题。

验证:e2e 786/0、batch10 44/0、batch9 38/0、batch_e 91/0、upgrade 126/0、picker 28/0、
semi_integrate 27/0、formbloat 5/0(+反向 3 红)、mobile 688/0、interact 97/0、drag 5/0、geom 5/5;
线上自检 19/19 + 线上真实账号巡检 28/0(含 Firefox 实测)。
This commit is contained in:
2026-10-04 10:46:50 +08:00
parent 52cc1ecccb
commit b2ca4f336e
16 changed files with 1624 additions and 286 deletions
+77 -4
View File
@@ -22,12 +22,31 @@ final class Calendar
/** 日程附件的宿主类型(统一附件表用;v24 第 6 条:日程也能挂附件) */
public const ATTACH = 'calendar_event';
/** 系统预置的两本默认日历(kind 与名字都写死,用户的自定义本一律 kind='custom') */
/** 系统预置的默认日历(kind 与名字都写死,用户的自定义本一律 kind='custom')
*
* v24 第 10 批 #4:加了两个**不可删除的只读本** ——
* · lunar 农历(每天的农历日期 + 农历节日),由 Holiday::lunarRange 派生;
* · holiday 节假日(法定假日的**休息与调休**),由 Holiday::year 派生,
* 并且**自动更新**(官方一公布就换用官方数据,见 Holiday 的类注释)。
* 它们和 movie/trip 一样是"从别处派生、不落 calendar_events"的视图本,
* 而且 kind ≠ 'custom' → Calendar::delete() 天然删不掉(用户要求"不可删除")。
*/
public const DEFAULT_KINDS = [
'movie' => ['观影', '#7A4E8C'],
'trip' => ['旅行', '#3F7D5A'],
'movie' => ['观影', '#7A4E8C'],
'trip' => ['旅行', '#3F7D5A'],
'lunar' => ['农历', '#8C6D3F'],
'holiday' => ['节假日', '#B0342C'],
];
/** 派生型只读日历(内容不从 calendar_events 取,也不能往里加日程) */
public const READONLY_KINDS = ['movie', 'trip', 'lunar', 'holiday'];
/** 这本是不是"派生只读"的(农历/节假日/观影/旅行都算) */
public static function isReadonlyKind(string $kind): bool
{
return in_array($kind, self::READONLY_KINDS, true);
}
/**
* 建本时的预设色板(与影院管理同一个交互:取色器 + 点一下就换)。
*
@@ -73,7 +92,10 @@ final class Calendar
// 常量里的 '#7A4E8C' 之类直接比字符串(日历本颜色是给页面用的,
// 大小写混着存会让"颜色一样吗"这种判断时对时错)。
strtoupper(AvatarColor::normalize($color) ?: '#4A7C82'),
$kind === 'movie' || $kind === 'trip' ? $kind : 'custom',
// ⚠️ 别写成 `'kind' => …`:这是个**位置参数**数组(对应 6 个 ? 占位符),
// 混一个命名键进去会让 PDO 报 `Invalid parameter number` ——
// 而且是**静默的**:整个 seedDefaults 抛异常,日历本一本都建不出来。
isset(self::DEFAULT_KINDS[$kind]) || $kind === 'custom' ? $kind : 'custom',
self::nextPosition($ownerId),
]);
return (int)$db->lastInsertId();
@@ -762,6 +784,57 @@ final class Calendar
}
}
// ---- 农历(v24 第 10 批 #4)----
// 用户原话:「日程中,增加两个不可删除的日历,一个是农历…」
// 每一天都出一条(标题 = 农历月日),农历节日另标一个名字。
// 它是**派生只读**的:不落 calendar_events,删不掉,也不能往里加日程。
if (isset($byKind['lunar'])) {
$c = $byKind['lunar'][0];
foreach (Holiday::lunarRange($from, $to) as $l) {
$fest = (string)($l['fest'] ?? '');
$title = (string)$l['title'] . ($fest !== '' ? ' · ' . $fest : '');
$out[] = [
'cal' => (int)$c['id'],
'color' => (string)$c['color'],
'calname' => (string)$c['name'],
'title' => $title,
'date' => (string)$l['date'],
'time' => '',
'all_day' => true,
'kind' => 'lunar',
'ref_id' => 0,
// 农历本没有可点进去的详情;给一个月视图锚点即可(点它=看那天)
'href' => url('calendar', ['view' => 'day', 'd' => (string)$l['date']]),
];
}
}
// ---- 节假日:放假 + 调休(v24 第 10 批 #4)----
// 用户原话:「一个是法定假日的休息和调休信息,法定假日这个,需要有自动更新功能…
// 不是让你更新,而是让系统,代码自动更新。」
// 自动更新在 App\Holiday 里:内置规则保证"永远有",官方数据一到就覆盖成官方口径。
if (isset($byKind['holiday'])) {
$c = $byKind['holiday'][0];
// [from,to] 可能跨年(12 月末的月视图会看到次年 1 月初),两年都要取
$years = [(int)substr($from, 0, 4), (int)substr($to, 0, 4)];
foreach (array_unique($years) as $y) {
foreach (Holiday::range($y, $from, $to) as $h) {
$out[] = [
'cal' => (int)$c['id'],
'color' => (string)$c['color'],
'calname' => (string)$c['name'],
'title' => (string)$h['title'],
'date' => (string)$h['date'],
'time' => '',
'all_day' => true,
'kind' => 'holiday',
'ref_id' => 0,
'href' => url('calendar', ['view' => 'day', 'd' => (string)$h['date']]),
];
}
}
}
// ---- 有截止时间的任务 ----
$st = Database::get()->prepare(
"SELECT t.id, t.title, t.due_at, t.status