laoyang
|
2c44318a51
|
压缩只留 1600 档;视频缓存失效加版本号 (#56 #57)
一、尺寸档位(用户:「400压缩的太多了。800也不够,只保留一个1600」)
media.php 白名单从三档收到一档:
1600 => image/resize,l_1600/quality,q_88
400/800 删除。质量从 q_85 提到 q_88(只留一档就该偏清楚那侧)。
照片墙跟随改 1600,缩略图与大图共用一份缓存。
传旧 ?w=400 的链接会落到「非法值 ⇒ 原图」分支,不报错、只是不压缩。
二、视频仍显示 HEVC(用户:「他说是H.265,还是不让播呢?」)
实测服务端 15 个视频**全部 h264**,无一个 HEVC:
ffprobe 逐个查过,codec=h264,pix_fmt=yuv420p/yuvj420p
media.php 返回 Content-Type: video/mp4,body 里无 hvc1/hev1
ETag 与实际内容完全一致
问题在浏览器缓存:
Cache-Control: private, max-age=604800(7 天)
浏览器对 <video> 有自己的媒体缓存策略,不完全遵守 ETag
Range 分段缓存会把每段当独立资源
src 不变时直接复用已有媒体对象
修法:让 URL 变。media_url() 加第三个参数 $ver,
用附件 size 当版本号 → media.php?k=...&v=2295119
为什么用 size 而不是 mtime/md5:
mtime —— 对象在 OSS 上,本地拿不到可靠时间
md5 —— 要读整个文件,几百 MB 视频不可接受
size —— 转码前后必然不同(1.22MB→2.19MB),且本就在库里,零开销
照片墙已接入。其余调用点可逐步铺开。
|
2026-11-05 13:11:44 +08:00 |
|
laoyang
|
d6af211a8e
|
图片走 OSS 实时压缩:取图带 x-oss-process,本地存储不受影响 (#56)
用户 2026-11-05:「如果图片是放在本地服务器的…就不管它。
如果是传到 OSS 里面的…就用 OSS 的压缩就行了。」
查证:OSS 图片处理确实存在,取图 URL 加 x-oss-process 即可实时压缩,
原图不动、不占额外存储。
三处改动(缺一个都会出错):
1. OssDriver
- get() 新增 $process 参数
- authorization() 新增 $subresource:x-oss-process 属于子资源,
必须计入 CanonicalizedResource,否则 403 SignatureDoesNotMatch
- URL 拼接带上 ?x-oss-process=...
2. Storage::read()
- 新增 $process,仅当驱动是 oss 时透传
- 本地驱动忽略(用户明确要求「本地不管它」)
- 带 process 时跳过"本地优先"缓存(那个缓存里是原图,
拿原图当压缩版返回等于压缩没生效)
3. media.php
- 新增 ?w= 参数,白名单三档 400/800/1600
- 按尺寸分档缓存到 _proc/<w>/<key>,避免互相覆盖
- 无 w 时行为完全不变(300+ 处既有 img 不受影响)
白名单而非透传的理由:OSS 图片处理按次计费,
放任 ?w= 任意值会让 ?w=1、?w=9999 这类请求把处理次数打上去。
实测(一张 6602 KB 的图):
原图 6602 KB
w=400 26 KB 省 99.6%
w=800 108 KB 省 98%
w=1600 468 KB 省 93%
应用位置(保守,先动最确定的):
_attachments.php 照片墙缩略图 → w=400
trips/show.php 行程题图 → w=1600
下载链接一律不加 w(用户要原文件,不能偷偷降质)
|
2026-11-05 13:06:29 +08:00 |
|
laoyang
|
3c9fb22def
|
视频转码:上传时自动转 H.264,已有视频批量转换 (#57)
用户 2026-11-05:「以后所有视频全部转成H.264」
新增 app/Video.php:
- isVideo / available / probe / needsTranscode / toH264
- 判据:编码不是 h264 或像素格式不是 yuv420p/nv12 就要转
(10bit、4:2:2 的 h264 浏览器也不认,光看编码不够)
- 参数:libx264 veryfast crf23 + yuv420p + aac 128k + faststart
+ scale=min(1920,iw):-2(4K 缩到 1080p,-2 保证高度偶数)
- 转完自检编码与像素格式,不对就不采用
Attachment::save() 接入:
- needsTranscode() 先判,已合规的不重复转(不白等几十秒)
- 转码失败不阻断上传,原样保存 + error_log 记原因
- 转完改 mime/ext 为 video/mp4,否则前端判断不出来
- 转码产物是临时文件,必须清理(否则塞满 /tmp)
批量转换已有视频(15 个):
- 13 个已是 h264 跳过,2 个 hevc 转成功
- 原地覆盖同 key,数据库引用全不断
- 原文件备份到 storage/_hevc_backup/
验证:4 个抽样全部 h264/yuv420p 或 yuvj420p,浏览器可播。
|
2026-11-05 13:01:39 +08:00 |
|
laoyang
|
f58a128816
|
行程:#47~#51 打包 —— 双轨状态、动态类型、动作按钮、去提示 (#51)
#47 编辑浮层居中、类型换行、去掉手动状态选择、按钮改胶囊
#48 计划/实际双轨:完成即转实际行程,取消≠删除,记真实开始/结束时间
#50 类型点击无反应(@Builder 值传参不建立依赖)
#51 节点类型动态加载(服务端9种,原端上硬编码5种)+ 地址结构化回调
API 新增:GET /trips/<id>/node-types、PATCH /trips/<id>/nodes/action
DB 新增:trip_nodes.actual_started_at / actual_ended_at
|
2026-11-05 12:40:57 +08:00 |
|
laoyang
|
a1140437a1
|
视频:修 media.php 的 304/Range 冲突,HEVC 解不了时给明确提示 (#52)
304 判定没有排除 Range 请求:视频第二次打开时浏览器同时发
Range + If-None-Match,命中 304 拿到空响应 ⇒ 黑屏。
改成 $hasRange 为真时跳过 304。顺手补 Last-Modified(部分浏览器需要它才允许 seek)。
另查明用户的视频是 H.265/HEVC,Chrome/Firefox 无法解码 —— 浏览器端无解,
检测到解码失败时显示提示卡片 + 下载入口,不再给一块黑屏。
|
2026-11-05 12:40:52 +08:00 |
|
laoyang
|
f92a98a3a3
|
子任务跟随父分类,修复父子跨本导致客户端无法显示层级
用户问:「任务是带有子任务功能的,能否通过 caldav,把任务的子任务也同步给客户端?」
## 答案:能,但实测发现父子被分到了不同本
改前数据:
父 #12「轻舟的幼儿园发票」 category_id = 1(家庭事务)→ tasks-1.ics
子 #29「送财务报销」 category_id = NULL(收件箱) → tasks-inbox.ics
客户端渲染「家庭事务」时看不到子任务,渲染「收件箱」时又找不到父任务,
⇒ 不可能显示成层级,只能两条并列。
## 改法
### 1. 子任务跟随父分类(TaskDav::effectiveCat)
有效分类 = 自己的 category_id
↓ 为空且是子任务时
父任务的 category_id
⚠️ **不改数据库**,只在视图层算。
改库等于篡改用户数据,而且单向不可逆。
边界:
| 情况 | 有效分类 |
|---|---|
| 自己无分类 + 父有分类 | 跟父(父子同本) |
| 自己无分类 + 父也无分类 | 都进收件箱(父子同本) |
| 自己有分类 | 用自己的(尊重用户明确设定) |
`tasksIn()` 改为 SQL 联查 `p.category_id`(一次到位,避免 N+1),
再在内存里按有效分类过滤。
### 2. SUMMARY 去掉硬加的「└ 」前缀
原来给子任务 SUMMARY 加「└ 」。这会**污染用户数据** ——
客户端把标题写回来(PUT)时前缀被存进库,用户的标题就多了一坨。
层级改由标准属性表达:
- `RELATED-TO:task-12` —— RFC 5545 §3.8.4.5 标准父子
- `X-FAMILYBOOK-PARENT:轻舟的幼儿园发票` —— 展示用补充,不支持的客户端无视
### 3. 跨本时的降级提示
用户明确给子任务设了别的分类时(尊重其选择),父子仍会跨本。
那种情况在 DESCRIPTION 尾部补一句人话说明,避免客户端把
「有 RELATED-TO 但找不到父」当成数据损坏而不显示这条。
## 验证
改后分布:
| 本 | 总数 | 父 | 子 |
|---|---|---|---|
| 家庭事务 | 7 | 6 | 1(#29) |
| 工作相关 | 19 | 19 | 0 |
| 个人事务 | 2 | 2 | 0 |
| 党支部工作 | 1 | 1 | 0 |
| 轻舟相关 | 1 | 1 | 0 |
| 收件箱 | 2 | 2 | 0 |
- 总数仍 32 条,没丢没重
- **所有父子都在同一本里**
- HTTP 端到端 39 项全过(含跨本隔离、鉴权、日历本回归)
## 客户端支持度说明(服务端无法改变)
| 客户端 | 层级显示 |
|---|---|
| DAVx⁵ + Tasks.org | 认 RELATED-TO,显示缩进子项 |
| 苹果提醒事项 | 有原生子任务 UI,对 RELATED-TO 支持有限 |
| Thunderbird | 不显示层级,两条并列 |
数据是正确的,显示成什么样取决于客户端能力。
想在所有客户端都看到层级,只能靠前端自己渲染 —— 那是网页端的事。
|
2026-11-02 16:07:45 +08:00 |
|
laoyang
|
610d524e68
|
CalDAV 待办改为按分类拆成多本(对齐参照 DAViCal)
用户提供参照服务器 nas.yangyq.net/caldav/synccalendartest(DAViCal 1.1.9)并指出
「应该可以直接显示那个家庭的待办分类」。
## 参照实测结果
那台服务器的待办本结构:
/caldav/<user>/tfxnz/ 家庭计划 VTODO
/caldav/<user>/zlqlzm/ 工作任务 VTODO
/caldav/<user>/tdotbsnl/ 个人成长 VTODO
/caldav/<user>/home_todo/ Inbox VTODO
⇒ 每个分类是**独立的 calendar 集合**,各自 supported-calendar-component-set
只含 VTODO,客户端里显示成并列的多个待办列表。
VTODO 正文还带 CREATED / LAST-MOD / DTSTAMP / SEQUENCE / PRIORITY /
PERCENT-COMPLETE / STATUS / DESCRIPTION / LOCATION / TRANSP。
## 上一版的问题
第一版做成「一个「家庭待办」本装全部任务」,所有分类混在一起,
客户端里只有一个列表 —— 用户要的是分类可见。
## 这一版
### 本的组织
/dav/tasks-<categoryId>.ics/ 每个 task_categories 一行 = 一本
/dav/tasks-inbox.ics/ category_id 为空的任务(Inbox)
⚠️ 用 `tasks-<id>` 而不是裸 id:真实日历本是 `/dav/<id>.ics/`,
裸 id 会与它们撞号(calendar_events 与 task_categories 是两套独立序列)。
### 分类跟随「本」
客户端把资源 PUT 进哪一本,任务就归哪个分类。
这是「按分类拆本」的自然语义,也让「在客户端里移动任务到另一本」等价于改分类。
### 跨本隔离
GET/DELETE 单条时会校验「这条任务确实属于这个本」,
否则改一下 href 就能用 A 本读到 B 本的任务。
## 验证(39 项全过)
真实数据(5 个分类 / 32 条任务):
| 本 | 条数 |
|---|---|
| 家庭事务 | 6 |
| 工作相关 | 19 |
| 个人事务 | 2 |
| 党支部工作 | 1 |
| 轻舟相关 | 1 |
| 收件箱 | 3 |
每本 supported-calendar-component-set 均只含 VTODO,无 VEVENT。
PUT 到 tasks-1.ics 后 Location 返回 /dav/tasks-1.ics/task-40.ics,
GET 读回、更新、完成、删除全部正常。
回归:原有日历本(观影等)PROPFIND/GET 正常,组件类型仍是 VEVENT。
鉴权:无口令 401、不存在的分类本 404、非法段 404。
测试数据已清理,tasks 仍为 32 条。
## 文件
- app/TaskDav.php 重写为多本模型
- app/Ics.php VTODO 导出/解析(上一版已加,本版未改)
- dav.php 五处拦截改多本
## 已知限制
看板名(正在进行/等待反馈/等待结果)通过 CATEGORIES 输出给客户端,
但客户端改它不会反向改看板 —— 标准 VTODO 没有「看板列」概念。
分类则双向(由「在哪一本」决定)。
|
2026-11-02 15:41:54 +08:00 |
|
laoyang
|
c86658966c
|
CalDAV 增加待办(VTODO)双向同步
用户原话:「网页端的 caldav 功能,除了能够同步日程外,增加可以同步任务(待办)的功能」
## 三个设计选择(用户已定)
1. 存放位置:作为 /dav/ 内的一个日历本,不另开 /taskdav/ 入口
2. 同步方向:直接双向读写
3. 字段映射:标准映射
## 字段映射
| VTODO | tasks 列 | 说明 |
|---|---|---|
| UID | 派生 task-<id> | 与日历 UID 空间隔离 |
| SUMMARY | title | 子任务加「└ 」前缀(纯展示) |
| DESCRIPTION | note | |
| DUE | due_at | 零点视为整天,用 VALUE=DATE |
| STATUS | status | todo→NEEDS-ACTION,doing→IN-PROCESS,done→COMPLETED |
| COMPLETED | done_at | 仅 done 时输出 |
| PERCENT-COMPLETE | status | doing=50,done=100 |
| CATEGORIES | 看板名 | 让客户端能归类 |
| RELATED-TO | parent_id | 标准父子表达 |
## 改动
### app/Ics.php:并列增加 VTODO 能力
不改 parse()/export() 的任何行为(VEVENT 路径注释里记着多轮踩坑史),
新增 exportTodos() / vtodo() / parseTodos() / normalizeTodo(),复用底层工具。
### app/TaskDav.php(新建,约 380 行)
虚拟日历本:href 固定 /dav/tasks.ics/,BOOK_ID = -1
(calendar_events.calendar_id 是 INT UNSIGNED 恒 ≥1,负数永不撞)。
权限口径与网页端完全一致,复用 Task::visibleTo / Task::canWrite /
Task::create / Task::update / Task::destroy,不另写一套。
### dav.php:前置拦截,原有日历逻辑零改动
PROPFIND / REPORT / GET / PUT / DELETE 各分支开头判断是否待办本,
是则走 TaskDav 并 exit。另外在 /dav/ 的本清单里追加待办本
(否则客户端根本不知道有这一本)。
## 过程中修掉的 4 个真 bug
1. **Class "TaskDav" not found** → 缺 use App\TaskDav;
autoload 要求带上命名空间前缀,漏了会让整个 dav.php 全线 500。
2. **PUT/GET单条/DELETE 全 404** → 原来全用 isTaskHref(),
它只比「整条路径 == /dav/tasks.ics/」,带子资源时必然 false。
新增 isTaskPath() 做前缀匹配(且要求前缀后必须是 / 或结束,
避免 /dav/tasks.icsXXX 被误判)。
3. **PUT 到不存在的资源名返回 403** → 原实现直接抛「任务不存在」。
按 RFC 4918 §9.7,PUT 到不存在的 URI 就是创建资源。
改为:只有确实存在且有权访问时才走更新分支,否则新建。
4. **注释里的 markdown 加粗导致 PHP 语法错**
`作为 **/dav/ 内的一个日历本**` 里 `**/` 形成 `*/`,提前闭合块注释。
PHP 注释里不要写 markdown 加粗。
## 验证
### Ics 层(34 项全过)
生成 → 解析 → 归一往返,含三种客户端完成态表达
(STATUS:COMPLETED / 只有 COMPLETED 时间戳 / PERCENT-COMPLETE:100)。
### TaskDav 层(45 项全过,真实 32 条任务)
读写、ETag 稳定性、权限、事务回滚。
写操作全在事务里做并回滚,不污染真实数据。
### HTTP 端到端(37 项全过)
OPTIONS → PROPFIND 发现 → REPORT → GET 整本 → PUT 新建
→ GET 读回 → PUT 更新 → DELETE,另含回归与越权检查:
- 原有日历本 PROPFIND/GET 仍正常,组件类型仍是 VEVENT
- 无口令 401、错口令 401、不存在的任务 404
测试按 RFC 跟随 Location(新建时服务端分配 id 并告知真实 href),
这也是真实客户端的行为。
### 数据完整性
测试前后 tasks 均为 32 条,测试数据已清理。
测试用 DAV 口令已 revoke(不影响用户原有口令)。
## 部署
app/Ics.php、app/TaskDav.php、dav.php 三个文件已上传并验证。
⚠️ 服务端文件不在客户端仓库,以后改这些要先比对服务器 md5 再验
(本次踩过:改了没上传,拿旧代码验出假结果)。
|
2026-11-02 15:29:36 +08:00 |
|
laoyang
|
821cfd1542
|
Holiday:去掉三处 400 天上限,农历/节日/节气在全量范围下不再丢失
## 问题
lunarRange / traditionalRange 都有 `$i < 400` 硬上限。
2026-10-30 客户端改成全量同步(1970~2100,47846 天)后,
这个「防异常」的保险变成「削正常功能」:
- 只算前 400 天 ⇒ **今天的农历是空的**
- 传统节日同样只剩前 400 天 ⇒ 今年的春节/中秋全丢
solarTermRange 另有一个问题:只取首末两年,
130 年范围下只算 1970 和 2100 的节气,中间 128 年全丢。
## 修法
- 三处统一改为:夹到 Lunar::MIN_YEAR~MAX_YEAR,再按
MAX_LUNAR_YEARS(30 年)截断,去掉按天计数上限
- solarTermRange 改为逐年遍历
## 实测(部署后)
今天 2026-11-02 → 廿四(修复前为空)
今年 11 个传统节日、24 个节气,齐全
2021~2031 农历 4017 条(修复前 400),节气 264 条(修复前 48)
1970~2100 农历 10958 条 / 0.47 秒(按 30 年上限截断,符合设计)
## 注意
本文件不在客户端仓库,需单独上传服务器。
验证前必须比对服务器 md5,否则会拿旧代码验出假结果(本次踩过)。
|
2026-11-02 08:51:34 +08:00 |
|
laoyang
|
ce752073db
|
品牌改名:index.php 里 site_name 兜底值 家书 → 岁月家(共 2 处)
|
2026-11-01 19:57:38 +08:00 |
|
laoyang
|
0b8012f5dd
|
品牌改名:家书 → 岁月家(网页端标题/logo/文案;首页标题改「某某某的家庭岁月」)
|
2026-11-01 19:55:40 +08:00 |
|
laoyang
|
7f3df7a40b
|
日程新建/修改不再刷新页面:服务端片段 + 就地重绘 (#37)
用户原话(2026-11-01):
「修改和新建,都不应该刷新页面,而是使用js动态调整。」
## 改法:不再手写复刻渲染,改成"服务端渲染片段 → 前端整块替换"
历史上「新建不刷新」是靠手写复刻服务端渲染做的(`putMonth` / `putTimed` /
`relayoutColumn`),它自己的注释里就记着一次真实事故:新加一条 07:00 的日程被
append 到末尾 ⇒ 落进「+N 条」看不见,刷新后又跑到第一位。修改要处理的情况更多
(跨天 / 全天状态 / 换日历本改颜色 / 改重复规则 / 挪到别的月),复刻必然出错。
⇒ 现在只有**一处渲染**:前端取服务端画好的视图片段整块替换。
"插进去的样子"与"刷新一次的样子"必然相同。
## 服务端
- `calendar/event_update` 认 `X-Requested-With`(与 event_add / event_del 同一套约定)
回 JSON;失败也回 JSON(可预期的拒绝不该把用户甩到一个刷新过的页面上)。
三处失败出口 + 四处成功出口收口成两个闭包,避免"某条路径仍刷页"。
- 抽出 `calendar_view_data()`:整页渲染与局部重绘共用同一份取数/建网格逻辑。
- 新增 `calendar/view` 片段路由(月/周/日视图片段)。
- 抽出 `home_agenda_events()` + 新增 `home/agenda` 片段路由(首页卡片)。
- 抽出 `templates/calendar/_view.php`:视图区从页面模板里搬出来(原文一字未改,
已用脚本逐行比对证明 270 行零差异)。
- `templates/layout.php` 加载 `cal_sync.js`。
## 前端
- 新增 `assets/js/cal_sync.js`:`refresh()` 按当前视图取片段替换;
只换内容不换容器;保首页卡片滚动位置;重绘后补跑红线定时器;
失败退回整页刷新(宁可刷一次,也不要"没刷新却也没变")。
- `entry_popup.js` 的「保存」改 fetch 提交 → 关浮层 + 轻提示 + 局部重绘。
名册/观影/行程/任务四个来源保持原生提交,范围不收不必要的回归面。
- 删掉日历页那 ~270 行手写插入,新建统一走同一个 `refresh()`。
- 「点格子新建」的 per-element 绑定改成 document 委托(重绘后新节点才有监听)。
## 本轮实测抓到并修掉的真 bug
`form.id` 被同名 input 遮蔽:日程编辑表里有 `<input name="id">`,
HTMLFormElement 的具名访问优先级高于原型上的 `id` ⇒ `form.id === 'calEditForm'`
恒为 false ⇒ 日程也走原生提交、整页刷新。
改用 `form.getAttribute('id')`。
## 验证(全部做过反向验证)
- 浏览器端到端 28 项:编辑/新建都不导航、改动原地可见、视图不跳、数据岛同步、
重绘后点格子仍有效、红线定时器重建、无 JS 异常
- 反向验证 7 项:把 refresh 置空 / 改成 reload / 不调 reinit / 抹掉上下文属性,
判据逐条转红(证明判据有牙齿)
- DOM 等价性 4 项:月/周/日视图与首页卡片,重绘后与刷新后的 DOM **逐字节相同**
(这条判据本身也做了反向验证:删一个字、多一个节点都能抓到)
- 服务端 11 项:非 AJAX 仍 302 且真存库、AJAX 回 JSON、失败也回 JSON、
片段路由可用、未登录被拒
- #35 跨天回归 11 项:三段的 seg/cont/is-seg-* 标记全在、贴满整列、不进全天行
- 静态检查:37 个模板片段无作用域违规;20 块内联 JS 语法全过;运行期零 Warning
|
2026-11-01 09:04:35 +08:00 |
|
laoyang
|
01d5bc262f
|
跨天日程:周/日视图按列分段画成一条,不再拆成碎片 laoyang/FamilyBook#35
用户 2026-10-30:「网页端,跨天日程显示还是有问题…如果一个日程跨了三天,
应该在日历本上怎么显示呢?在周视图和日视图,应该怎么显示?现在显示的有问题。」
## 修复前是什么样(实测,不是推断)
一个「11/10 20:00 → 11/12 02:00」的跨三天日程,服务端 month() 展开成三条
(首日带 time、中间天两者皆空、末日带 end_time),喂给 build_time_grid 后:
全天行:11/11、11/12 各一条 ← 中间两天变成"全天日程"
时间网格:11/10 一个小块,高度仅 2.08%(≈30 分钟)
三个问题叠加:
· 开始日的结束时刻不在同一天 ⇒ 判成"没有结束" ⇒ 退化成 30 分钟
· 中间/末日既无 time 又不是全天 ⇒ 被"按全天处理"的兜底塞进全天行
· 三条互不相干,用户看不出"这是同一个日程"
## 方案(用户已确认)
1. 周视图:跨列连续长条 —— 毎列各画一段,视觉连成一条
2. 日视图:铺满当天并标延续
3. 中间天归时间网格,不进全天行
## 改了什么
**app/Calendar.php** —— 服务端补两个字段
· `span_days`:整条日程跨了几天(0 = 当天来回)
· `seg`:start / mid / end / only
同时修掉一个真 bug:原来 `$first` 是"窗口内遍历的第一个",
日视图只查一天时,中间天会变成 `$first` 被当开始日处理。
⇒ 一律用**真实日期**跟 `$base`/`$base+span` 比较,不看遍历顺序。
**index.php::build_time_grid** —— 核心重构
· 先按 `kind:ref_id:occ` 归堆,把同一日程的逐天条目合成一条
(带 occ 是因为重复日程不同次发生的 ref_id 相同;带 kind 是因为
观影/旅程的 ref_id 是各自表里的 id,`custom:5` 和 `movie:5` 是两回事)
· 跨度以服务端 `span_days` 为准,不再用"窗口内铺了几列"推断
(日视图下那个推断必然不成立)
· 跨天条目拆成每列一段,按 `seg` 决定纵坐标:
start = 开始时刻→24:00,mid = 整列,end = 0→结束时刻
· 跨天条目**不参与 conflictLanes** —— 它必然占满整列,
拿去分列会把同一天所有日程都挤窄
**templates/calendar/index.php** —— 分段渲染
· `is-seg-start/mid/end` 类;延续段前面加 `↳` 标记,不重复显示时间
· 跨天段用 `left:0; width:100%`,单天块保持原来的 `calc(2px + N%)`
**assets/css/app.css** —— 让三段看着连成一条
· start 下圆角归零、下边框透明;mid 上下都平;end 上圆角归零、上边框透明
· 段与段之间原本有 1px 白边 + 左右内缩,实测缝隙 10.9px,看着像断成三段
⇒ 跨天段贴满整列后降到 1.0px(只剩列边框)
## 为什么在列内画段,不做跨列绝对定位层
`.tg-body` 左边有 56px 固定 gutter,窄屏还会横向滚动
(`.cal-tg-card{overflow:auto}` + 每列 min-width 92px),`.tg-col` 又是
`overflow:hidden`。跨列层要自己算 gutter 偏移、跟着横向滚动跑、还不能被裁 ——
每个都是新坑。在列内画段天然对齐,代价只是段间有 1px 列边框缝,用 CSS 糊掉。
## 验证
**逻辑层(PHP,用真实 build_time_grid)**
· 周视图跨三天:3 段(start/mid/end),全天行为空 ✓
· 日视图三个位置(起始/中间/结束天)各自正确 ✓
· 回归:单天 + 真全天仍正常;冲突分列仍生效;跨天与普通日程共存正常 ✓
· 反向验证:用修复前的代码跑同一判据**正确转红**
(旧版 timed=1 all_day=2,高度只有 2.08%),证明判据非恒真 ✓
**视觉层(Playwright + 真实 CSS + include 真实模板)32 项全过**
· 横向连续:start→mid 缝隙 1.0px,mid→end 1.0px(判据 ≤2.5px)✓
· 纵向:start 底边贴列底 0px;mid 占满整列(顶 0 / 底 0);
end 顶边贴列顶 0px 且只占开头一小段 ✓
· 样式:start 下圆角 0px + 下边框透明;mid 上下圆角 0;end 上圆角 0 +
上边框透明 ✓
· 无裁切:三段都落在列内 ✓
· 回归:真全天仍在全天行;跨天不再出现在全天行;单天日程正常 ✓
· 窄屏 375px:三段仍可见(w=91)✓
**反向验证 4/4 全部抓到**
故意做坏四个变体(段留单天内缩 / 保留圆角 / 中间段只画 30% /
下边框不透明),判据全部转红 —— 说明"32 项全过"不是因为判据恒真。
## 已知取舍
跨天段占满整列,同一天的普通日程与它在视觉上会重叠。
这是日历的固有取舍(Google 日历同样如此),用户确认的方案里也接受。
|
2026-10-30 23:43:10 +08:00 |
|
laoyang
|
73c84c1910
|
geo:移除地址联想代理接口(端上改用鸿蒙自带 Map Kit)laoyang/FamilyBook#33
用户指示「端侧不用高德,用鸿蒙自带的接口」。
端上已改用 @kit.MapKit 的 site.queryAutoComplete(),这个服务端代理随之无用:
- ApiHandler:删 case 'geo' 与 geo() 方法
- Geo:删 TIPS_URL 常量与 tips() 方法
网页端不受影响:它走浏览器端 JS API(AMap.AutoComplete),
从来不经过 /api/v1/geo/tips(已确认 addr_suggest.js 零引用)。
验证:接口返回 404(已删),calendar/books 等其它接口仍 401(正常鉴权)。
|
2026-10-30 08:11:47 +08:00 |
|
laoyang
|
29b2ca0e3c
|
geo:新增地址联想接口 /api/v1/geo/tips(高德 inputtips 代理)laoyang/FamilyBook#36
鸿蒙端地点输入要「输入简单地址 → 下拉带详细地址」,但高德 Web 服务 key
不能进客户端(反编译/抓包即得,配额会被跑光、key 可能被封)。
网页端用的 JS API key + 安全密钥是域名白名单那套,原生端没有等价机制
⇒ 走服务端代理,与网页端 addr_suggest.js 同一个上游(/v3/assistant/inputtips)。
- Geo::tips():调高德 inputtips,把三套不齐的字段归一成
name / detail(省市区+门牌) / district / address / lng / lat
- 过滤公交地铁线路(typecode 15xx)—— 对填地点没用
- 只看 body 里的 status 而不是 HTTP 码(配额用完高德回 200 + status=0)
- 少于 2 字不查,返回空数组而不是报错(阈值与网页端一致)
- ApiHandler::geo():GET only、要求登录(这是花配额的接口)
⚠️ 现有 key 是 JS API 类型,调 Web 服务接口返回 USERKEY_PLAT_NOMATCH(10009),
需另配 Web 服务 key 才能真正工作。同样原因,既有 Geo::regeo() 一直是失效的
(cityFromPoint 在走离线兜底),此问题另行处理。
解析逻辑已用 4 组模拟高德响应验证(同名可区分/行政区划兜底/线路过滤/
空 name 跳过/失败返回空),19 项全通过,含源码一致性检查。
|
2026-10-30 08:03:20 +08:00 |
|
laoyang
|
df657e5e5a
|
地址联想:下拉候选补详细地址,同名地点可区分 laoyang/FamilyBook#32
用户 2026-10-30:「输入一个简单地址、下拉显示详细地址时,不能只把地点告诉我,
一定要把详细地址也放在后面。因为同一个地址可能有好几个,我根本不知道应该选哪一个。」
原来用的是 AMap.AutoComplete 的内置下拉 —— 它只画 name,同名地点
(多个「星巴克」、同一商场的不同入口)长得一模一样,无法判断该选哪个。
⇒ 改成「高德只当查询器、下拉自己画」:
· 构造时不传 input,高德不再塞自己的面板
· 输入防抖 260ms → ac.search(kw) → 自己渲染 result.tips
· 每行 = 地名(14px 深色)+ 省市区/详细地址(12px 灰字)
选中后的既有行为原样保留(只是入口从高德的 select 事件换成自建候选的点击):
data-addr-fill 回填、data-addr-detail 写门牌、框下 .fb-addr-hint 提示、
fb-addr-picked 事件。
几处刻意的取舍:
· 面板挂 body + position:fixed,不挂输入框父节点 —— 否则会被 .mv-row
的两列网格挤掉、或被子表单弹框的 overflow:hidden 裁掉
· placePanel 量不到尺寸(0x0)时**只跳过定位、不收起**。原来收起了,
导致候选查到却一显示就消失(实测踩到)
· 少于 2 字不查,并收起面板,避免残留旧候选被误点
· 候选行用 textContent 逐个填,不拼 HTML(地点名含 < & 会破版/注入)
验证:jsdom 27 项(含 from/to 不串、SDK 未就绪降级、自愈)+
真实浏览器 54 项(桌面/手机视口 × 表单/弹框/贴底部三种布局)全通过;
反向验证 3 个坏变体(不渲染副标题/副标题为空/不挂 body)判据全部正确转红。
|
2026-10-30 07:37:55 +08:00 |
|
laoyang
|
42aff02687
|
读书上传+在线阅读+笔记导入:补 books/{upload,chapters,chapter,import_notes} #28
用户原话(2026-10-28):「鸿蒙端对齐网页端」。读书端上只有书架(状态/评分/进度/
笔记),上传解析、在线读、笔记导入都在网页端。
补四个 API 端点(模型 BookParser / Book::chapters/chapter/replaceChapters /
BookNoteImport 早已齐,纯粹没暴露):
· POST /books/upload 上传 EPUB/TXT 等 → 存文件 → BookParser 解析 →
建书 + replaceChapters + EPUB 自带封面
· GET /books/<id>/chapters 章节列表(无正文)
· GET /books/<id>/chapter?seq=N 单章正文
· POST /books/import_notes 粘贴读书笔记原文 → 解析 → 导入(不信前端结果)
判据:_i28_book_api.py 11/0(上传建书+落章节 · 章节列表 · 单章正文 ·
笔记导入),_i28_falsify.py 注掉 upload 分支变红。
|
2026-10-28 22:47:38 +08:00 |
|
laoyang
|
b6dc60452a
|
电影影评+评论:修半成品并补全 /movies/<id>/{reviews,review,comments} #27
用户原话(2026-10-28):「鸿蒙端对齐网页端」。观影详情只能看平均分,
不能写影评/评论。盘点发现服务端其实有份**半成品且带 bug**:
· comments GET 写死"只有我写过影评才返回" —— 错,改为独立 reviews 端点;
· comments POST 调 addComment 参数**完全错位**(parent_id 传给了 feelings、
target_person 传给了 parentId)—— 评论实际存不进;
· 缺影评/评论的删除端点。
现在对齐 Web 端 reviews 四条路由:
· GET /movies/<id>/reviews 全部影评(含评论树)+ 我的影评 + 统计 + 感受目录
· POST /movies/<id>/review 写/改我的影评
· DELETE /movies/<id>/review 删我的影评
· POST /movies/<id>/comments 评论/回复(review_id/content/parent_id)
· DELETE /movies/<id>/comments?comment_id=N 删我的评论
判据:_i27_movie_review_api.py 22/0(写/改影评 · 列表含 mine · 评论/回复 ·
删评论级联 · 删影评 · 越权删别人影评被拒),_i27_falsify.py 注掉写分支变红。
|
2026-10-28 22:29:55 +08:00 |
|
laoyang
|
ad71c80f48
|
任务共享:补 /tasks/<id>/shares API 端点 #26
用户原话(2026-10-28):「鸿蒙端对齐网页端」。任务端上无「共享给谁」入口,
Task::shares/setShare/dropShare 模型早就有(Web 端 tasks/share、tasks/unshare 在用),
只缺 API 子端点。
· GET/POST/DELETE /api/v1/tasks/<id>/shares —— 共享列表 / 加 / 取消
(canWrite 才允许;Task::find 本来就带 shares 字段,详情一次就能拿到)
判据:_i26_task_share_api.py 13/0(共享增/列/删 · 越权被拒),
_i26_falsify.py 注掉 shares 分支变红。
|
2026-10-28 22:10:15 +08:00 |
|
laoyang
|
47247e39b9
|
资源附件+共享:补 /resources/<id>/{attach,shares} API 端点 #25
用户原话(2026-10-28):「鸿蒙端对齐网页端」。资源端上明说「附件与共享
还在网页端」,根因是资源 API 只有 title/content,附件与共享没接口。
补两组子端点(模型 Resource::ATTACH=resource、shares/setShare/dropShare 早就有):
· GET/POST/DELETE /api/v1/resources/<id>/attach —— 附件列表 / 传 / 删
(删走 attachment_id query,与 tasks 同招;可写者才允许,别人家的 404)
· GET/POST/DELETE /api/v1/resources/<id>/shares —— 共享列表 / 加 / 取消
(canWrite 才允许,越权 404 —— find 对看不到的资源返 null,不泄露存在性)
判据:_i25_resource_api.py 21/0(传/列/删附件 · 共享增/列/删 · 越权被拒且库里没变)
· _i25_falsify.py 注掉 attach 列表与 shares 分支各自变红。
|
2026-10-28 22:00:14 +08:00 |
|
laoyang
|
e44f37e902
|
财务折线:补 /ledger/<id>/timeline API 端点 #24
用户原话(2026-10-28):「鸿蒙端对齐网页端」。财务概览卡「净资产变化」在
端上是空占位,数据源 Web 端早有(Ledger::timeline + ledger/total_chart),
只缺一个 API 给端上。
· GET /api/v1/ledger/<id>/timeline —— 返回时序点数组(date/net/asset/debt)
加当前汇总。与 Web 端 total_chart 用**同一份 Ledger::timeline($id)**,
两端曲线必然一致。
判据:_i24_timeline_api.py 8/0(2xx · items 数组 · 每点 net=asset-debt ·
有数据源),_i24_falsify.py 注掉 timeline → ④ 变红。
|
2026-10-28 21:47:27 +08:00 |
|
laoyang
|
cd56a9d8fe
|
笔记图片块:补 API 上传子端点(图文混排的前半程)#23
用户原话(2026-10-28):「鸿蒙端对齐网页端」,笔记端上只支持纯文本,
图片块显示成「[图片]」,且编辑带图笔记会静默删光图片块。
这半程只改服务端、补一个缺口:
· POST /api/v1/notes/upload —— 笔记图片上传,返回附件 key。
放在 notes() 的 $id>0 分支之前(上传是草稿态,不依赖笔记 id,
新建笔记插图时还没有 id)。图片先挂 target_id=0,等 blocks 带
key 提交时由 Note::claimUploads 认领(与 Web 端 notes/upload 同一套)。
Attachment::save 只回 id 不回 key ⇒ 反查 Note::ATTACH=0 的 list 拿 key。
· dispatch 里 notes case 从 notes($id,$method) 改成 notes($id,$sub,$method),
否则 $sub='upload' 根本传不进去(405)。
判据:
· _i23_note_image_api.py 11/0(上传返 key · 带 blocks 新建后 text+image
两块都在 · PATCH 后图片块不被删 —— 这是 P0 数据丢失修复的核心证据)
· _i23_falsify.py 注掉 upload 分支 → ①② 变红(命中 2/2)
|
2026-10-28 21:34:43 +08:00 |
|
laoyang
|
7c86859ab9
|
笔记附件:修好拖放、传完回编辑态、选中即上传 #21
用户原话:「网页端,笔记应该可以同时上传多个附件。」
诊断后确认「多选」本来就是好的(multiple + name=file[] + 后端数组分支,
一次选 3 个混合类型真的存了 3 行)。真正坏的是另外两件事:
1) 拖放是假承诺。界面写「把文件拖到这里」,但没有任何 drop 监听器:
拖完 input.files 仍是 0、POST 0 个文件。更糟的是 dragover 没有
preventDefault ⇒ 浏览器按默认行为直接打开那个文件,正在写的笔记
连未保存正文一起丢。
修法:给框加 data-sd-for="noteAttInput",复用 app.js 里既有的
bindInput() → bindZone()。一行属性,不另写一套拖放。
踩过的坑:data-sd-for 写的是**裸 id**,第一版带了个 `#`,选择器
变成 [data-sd-for="#noteAttInput"],一个都匹配不上,拖放照样没人接。
2) 传完跳回阅读态(redirect 不带 edit=1),附件区在 $editMode 分支里
根本不渲染 ⇒ 用户看到的是"没传上"。attach_add / attach_del 两处
都补 edit => 1。
另外补了即时反馈:change 一来就把提示换成「已选择 N 个文件:…,正在
上传…」并自动提交。用 requestSubmit(btn) 而不是 form.submit() ——
后者绕过 submit 事件、也不带按钮的 formaction/formenctype,提交会跑去
notes/save 且非 multipart,文件直接丢。
判据:
- _b40_note_multi_attach.py 16/0(点选多选 / 拖放 / 回编辑态 / 单选回归 / 提示文案)
- _b40_falsify21.py 三处注入各自红在预期编号(data-sd-for 带# → ③④a-d;
requestSubmit→submit → ①-b;去掉 edit=1 → ③-a/b)
- _b40_regress21.py 20/0(删除生效且回编辑态 / 越权删除被拒 / 公共绑定器
未受影响 / MAX_PER_HOST=60 端到端仍生效)
- _b40_regress21_falsify.py 关掉上限与归属校验各自变红
|
2026-10-28 20:13:22 +08:00 |
|
laoyang
|
4941e9db9c
|
fix(home): 从浮层把任务标成完成时该行不消失(只修了复选框那条路)(#20)
用户第二次提同一条需求。核了线上代码:4 文件 md5 与本地一致、mtime 18:14,
部署没问题;复跑原判据仍 20/0 全绿。判据绿 + 用户能复现 ⇒ 判据有盲区。
盲区:首页把任务标成完成有**两个入口**
① 每行前面那颗复选框 —— home.php 末尾的脚本(上一版已修)
② 点整行弹浮层 → 面板里「状态」三段按钮 → assets/js/task_panel.js 的 syncRow()
上一版只改了 ①。syncRow() 只改徽章与复选框的 aria 状态,**从不删行**,
所以从面板点「已完成」后行留在原地,只把徽标换成「已完成」。
用户点的就是 ②,而原判据 grep 全文没有 panel / 浮层 / data-fb-entry —— 完全没测这条路。
改法:
- 把删行逻辑抽成 window.fbHomeTaskDrop(row, wasStatus) 放 assets/js/app.js,
两个入口共用一份。作用域限定 [data-tk-home-host]:任务**看板**页的行也带
data-task-row,那里勾完成只改状态、**必须保留该行**(看板按状态分栏,删了就没处看)。
- task_panel.js 的 syncRow() 在 d.status === 'done' 时调它;
wasStatus 必须在改写 data-tk-status **之前**读,否则概述卡不知道该减「在做」还是「待办」。
- home.php 原来那份本地实现改成薄转发,避免两份实现改一处忘一处。
- 顺带修掉一条说谎的注释:原注释写"补偿了一个可撤回的提示",但代码里从来没有
撤回按钮,只有一句 3.2 秒自动消失的 say()。
- _card_tasks.php 补记"两个入口"这件事。
子任务安全:task_panel_json() 用 panel_of 找**父任务**,回的 status 是父任务的,
所以面板里勾子任务不会把父任务那一行删掉(判据 ⑱ 专门守这条)。
判据:
- 新增 _b38_panel_done_drop.py(21 条,走真实浮层点击):
修复前 **16 通过 / 5 失败**(⑥⑦⑧⑬⑭)→ 修复后 **21 / 0**
- 注入反证 _b38_falsify.py:把删行那两行用 if(false) 挡掉,锚点断言 count==1,
立刻红回同样的 5 条、关键条目 ⑥⑧⑬⑭ 命中 4/4,副本自动还原
- 原 _b37_home_task_drop.py(复选框路径)仍 **20 / 0**
- _b37_regress.py **11 / 0**:看板仍改状态且**不删行**、详情页子任务复选框正常、
空态 display 真的是 none
- home.php 内联 JS 0 块坏;app.js / task_panel.js node --check 全过
|
2026-10-28 18:29:59 +08:00 |
|
laoyang
|
ea6e42c27f
|
fix(home): 近期任务勾选完成后该行不消失(连概述卡计数也不减)(#20)
用户原话:「首页,近期任务,如果勾选了完成,应该刷新一下这个列表,把完成的删掉。」
## 原来的行为
首页那段复选框脚本 AJAX 成功后只改徽章文字(`kb-st` → 「已完成」),
**没有任何"移除这一行"的逻辑**。而且 templates/home.php:160 那条注释还写着
这是**当年的刻意设计**:
> ⚠️ 改完**不摘掉这一行**:用户点完正想看自己刚完成什么,立刻消失会让人怀疑
> "是不是点到删除了"。刷新时由服务端决定它还在不在。
用户现在明确推翻这条:完成的就该从列表里删掉。
## 服务端本身是对的
首页路由只取 `doing` + `todo`(index.php:839),`done` 本来就不在列表里
⇒ 手动刷新这一行确实会消失,问题纯粹在"AJAX 完成后前端没同步"。
所以这次一行服务端代码都不用改。
## 改法
新增 `dropTaskRow(row, wasStatus)`,AJAX 成功后(**按服务端返回的 `d.status` 判断**,
不是拿点击前的 `next`)做三件事:
1. **删掉这一行**;
2. **同步「概述」卡右下的「N 件在做 / N 件待办」** —— 那两个数字与这份列表
**同源**(同一个 `$tasks`,见 _card_brief.php:53),只删行不减去数字
⇒ 列表 2 条、概述说 3 件在做,自相矛盾;
3. **一条不剩时把空态提示露出来**(服务端渲染的 `[data-tk-empty]` 原来一直 hidden)
—— 否则卡片变成一片空白,用户以为加载坏了。
配套的小改动:
- `_card_tasks.php`:空态提示改成**一直渲染、靠 `hidden` 切换**,加 `data-tk-empty`
钩子(不靠匹配文案 —— 文案改一个字就失效)。外层加 `data-tk-home-host`。
- `_card_brief.php`:两个统计 `<a>` 加 `data-hbs="doing|todo"` 钩子,
同样**不靠中文文案定位**(这是 JS 要读的东西,不能把展示文案变成接口)。
- `app.css`:补 `.empty-hint[hidden] { display: none !important; }` ——
浏览器默认的 `[hidden]{display:none}` 优先级最低,哪天有人给它加 `display:flex`
这句提示就会在"有任务时"也冒出来(与 app.css:601 那条注释是同一约定)。
## 几个判据上的讲究
- **按 `d.status` 判断而不是 `next`**:`tasks/status` 是幂等接口,
拿服务端返回的状态才不会"手快连点两下,第二下把行删了"。
- **`wasStatus` 要在改 `data-tk-status` 之前存**:删行时要靠它决定减「在做」还是「待办」。
第一版直接在删行时读 `data-tk-status`,读到的已经是 `done` —— 自己写的时候就踩了。
- **提示文案补一句"已从近期任务移出,去「全部」里能看到"**:
删行的原罪是"用户以为点到删除了",所以必须**明说它去哪了**,
而不是沿用服务端那句「已标成「已完成」」(那会让人以为行还在,下一眼发现没了更慌)。
## 验证
**主判据 `_tooling/_b37_home_task_drop.py` —— 20 通过 / 0 失败**
夹具:把库里历史 doing/todo 任务临时改状态腾空卡片(**不能靠 `deleted_at`** ——
`Task::byStatus()` 的 SQL 里根本没有 `deleted_at IS NULL`,软删对首页无效,实测过),
造 2 条 doing + 1 条 todo,跑完逐个恢复。
覆盖:勾「进行中」→ 行移除 + 「件在做」2→1 + 「件待办」不误减;
勾「待办」→ 行移除 + 「件待办」1→0 且链接藏起;
删到最后一条 → 0 行 + 空态露出;刷新后与服务端事实一致;库里状态真的是 done。
**注入反证 `_tooling/_b37_falsify.py` —— 三次独立注入,各自红在对应处**
| 注入 | 结果 | 命中的判据 |
|---|---|---|
| ① 不删行(原始 bug) | 通过 12 / **失败 8** | ③④⑤⑪⑭⑮ |
| ② 删行但不减概述卡计数 | 通过 17 / **失败 3** | ⑤⑫⑯ |
| ③ 不限空态(删完一片空白) | 通过 19 / **失败 1** | ⑮ |
每次注入前断言锚点唯一(#474),恢复后回到 20/0。
**回归 `_tooling/_b37_regress.py` —— 11 通过 / 0 失败**
看板勾选仍能改状态且**不删行**(与首页行为不同是有意的)、
详情页子任务复选框仍能改状态、`[hidden]` 的 `display` 真的是 `none`、
`data-hbs` 钩子在、卡片「全部」入口仍指向 tasks。
⚠️ 回归里踩了一个判据假设错:我一开始按"详情页也该有父任务复选框"写,
直接 FAIL。真实情况是详情页的复选框**只给子任务**(show.php:181 在子任务循环里),
父任务没有 —— 是判据错了不是产品坏了(#429 同族)。
**内联 JS 校验 `_tooling/_b37_inline_js_check.py`**:把渲染后 HTML 的 `<script>`
抽出来逐个 `node --check`(`php -l` 不看 HTML 里的 JS)。
并做了反证:喂一段坏 JS,它确实报 1 块坏,不是恒绿。
工单 #20
|
2026-10-28 18:13:49 +08:00 |
|
laoyang
|
e733c4f057
|
fix(resources): 新建资源时带着上一次的附件(暂存清单从不复位)(#19)
用户原话:「资源模块,新建资源,会把之前的附件带进来,就是说,附件那里不是空的。」
## 根因
`#resAddCard` 是新建/编辑**共用**同一个弹窗(v42 第 7 条),
复位函数 `resetToCreate()` 清了标题/正文/归属人/共享人,**唯独漏了附件**:
- 缩略图卡片与隐藏令牌 `stage_token[]` 是 queueDrop() 上传成功后
**动态插进 `.stage-list`** 的(assets/js/app.js:668),只有点卡片上的「×」才移除;
- 关弹窗只做 `addCard.hidden = true`,DOM 原样留着 ⇒ 下次打开就看见上一次的附件。
## 不只是"看着脏"
上传后**没保存就关掉**的那批,暂存目录还在(没被认领),令牌残留在表单里
⇒ 下次点「确定」时被 `StagedUpload::claim()` 认领到**新资源**上。
实测(`_tooling/_b36_res_attach_reset.py` 的 D1):
修复前新建的资源 id=3 附件数 **=1** —— 用户会看到"新建的资源莫名多了张照片"。
修复后同一路径附件数 **=0**。
## 改法
新增 `clearStaged()`:清 `.stage-cell` 卡片 + 兜底清 `input[name="stage_token[]"]`
+ 把 `.stage-list` 藏回 hidden(与模板初始状态一致)。
两处调用:
- `resetToCreate()` —— 点「新建资源」时
- `openEdit()` —— 点「编辑」时(openEdit 并不灌附件,已有附件走列表里独立的
「附件」弹窗,所以此刻表单里的卡片一定是"上一次没做完的残留")
⚠️ 兜底清令牌这步不能省:用户看不见令牌,但它才是真正会被提交的东西
(DOM 删了、数据还在 —— #456 同族)。
## 验证
`_tooling/_b36_res_attach_reset.py`(走真实 UI,三条关闭路径 A/B/C + 数据层 D):
| | 修复前 | 修复后 |
|---|---|---|
| A 取消后再新建(卡片/令牌) | 1 / 1 | **0 / 0** |
| B 保存成功后再新建 | 1 / 1 | **0 / 0** |
| C Esc 关闭后再新建 | 1 / 1 | **0 / 0** |
| D 新建资源的实际附件数 | **1**(带进来了) | **0** |
| 合计 | 通过 6 / **失败 7** | **通过 13 / 失败 0** |
**判据有牙齿**:`_tooling/_b36_falsify.py` 把 `clearStaged()` 注入成第一行 `return;`
(等价修复前),判据立刻红回 7 条,恢复后 13/0。锚点命中数断言 =1。
`_tooling/_b36_res_regress.py` —— 回归 13/0,确认没弄坏既有能力:
编辑仍能灌入标题/正文/id、底栏「共享」「附件」两颗按钮仍显示、
「新建+上传+保存」附件照旧真的挂上(附件数=1)、
列表里独立的附件面板组件(`sd-attach-panel`)仍在。
顺带查了其它用 `_stage_drop.php` 的模块(books/calendar/movies/people/tasks/trips),
都没有"共用弹窗 + 复位"这套结构,同一个坑不存在。
工单 #19
|
2026-10-28 17:50:51 +08:00 |
|
laoyang
|
fc81347e8d
|
fix(notes): 调过宽度的图片保存后变孤儿(#18)
用户报「笔记功能,上传图片失败」。实际不是上传失败,是上传成功后
保存那一步把图片弄丢了。
根因:图片块 content 有两种形态
- `photos/xxx.jpg` —— 宽度 100% 时,前端不写后缀
- `photos/xxx.jpg|50` —— 宽度调到 20~99% 时,serialize() 加上宽度
但 Note::claimUploads() 与 Note::files() 直接拿 content 去匹配
attachments.key(`WHERE key IN (...)` / `isset($used[$key])`),
带后缀的那个**永远匹配不到**,于是:
1. 草稿附件(target_id=0)认领失败 ⇒ 图片变孤儿,笔记里图没了
2. files() 排除不掉已用作图片块的附件 ⇒ 同一张图既当图片块、
又在底部附件区重复出现
默认 100% 不写后缀,所以表现为"有时好有时坏",
极易被误判成上传环节的问题。
修法:新增 Note::imageKey() 统一剥 `|宽度` 后缀,
claimUploads() 与 files() 都先过它。
刻意修在**服务端**、而不是"前端别写后缀":后者会把宽度功能废掉
—— 渲染时要按 content 里的宽度出图(见 templates/notes/form.php
的 $imgParts 与 templates/notes/index.php:98,那两处本来就剥了,
所以只有服务端这两处漏了,属于不对称实现)。
updateBlock() 走同一条 claimUploads(),自动被覆盖。
验证(本地 PHP 8.5 + MariaDB,真实 UI 操作,非手搓 DOM):
- 主判据 _tooling/_b35_width_falsify.py:修复前 100%→1712 成功 /
50%→target_id=0;修复后两条路径都认领成功,7 通过 0 失败,
且 content 仍带 `|50`(宽度功能没被牺牲)
- 附件不重复 _tooling/_b35_dup_attach.py:阅读态底部附件 0 条、
图片块 1 张、img 61px vs figure 990px(宽度真的生效),5 通过 0 失败
- 判据有牙齿:把 claimUploads() 注入回旧形态后立刻 2 条 FAIL,
恢复后回到 7 通过 0 失败
工单 #18
|
2026-10-28 16:03:36 +08:00 |
|
laoyang
|
ff818a3deb
|
feat(nav): 导航站点支持自定义图标上传(含两处连带修复) #16
需求:网页版导航,新增/编辑导航地址时没有自定义图标功能。
## 功能
- 站点表单加 enctype=multipart/form-data,新增「自定义图标」控件:
40px 预览 + 选择图片 + 清除自定义图标(仅编辑且有自定义图时出现)
- 选择文件后本地即时预览(URL.createObjectURL,用完 revoke,不落 base64)
- 按文件内容判类型(finfo + <svg 探测),不信任扩展名;上限 1MB
- icon_custom 三态语义:不选文件=不动 icon 列;选文件=icon_custom=1;
勾清除=icon='' & icon_custom=0。普通改名不碰 icon 列,不会丢图
- saveUploaded() 改随机文件名。原实现按 md5(icon_key(url)) 命名,
单用户系统没事、多用户会互相覆盖
- 新增 Nav::iconFileInUse():图标文件按内容共享,删旧文件前先查还有没有
别的记录在用
- 图标从 uploads/ 目录取不到(线上 403),走 /nav_icon.php?f= 公开出口
## 连带修复 1:ICON_STORE 常量从未定义
SimpleNav 搬迁时漏搬 define('ICON_STORE', 128),NavFetch.php 第 667/739
行在用。PHP 8 抛 Error: Undefined constant "ICON_STORE",只要走到 GD
位图分支就 500。因为 saveUploaded() 一直是死代码(有定义无调用点),
这个错一直没被触发过。现补进 app/bootstrap.php(带 if (!defined) 保护)。
## 连带修复 2:22 处裸函数调用缺 self::
搬迁时把自由函数改成了 NavFetch 的静态方法,但类内部调用点没跟着改。
PHP 报 Error: Call to undefined function App\http_get() @ NavFetch.php:171。
这既是「全部更新」按钮 500 的根因,也是「图标一直抓不到」的真相 ——
整条抓取链路(http_get / attr_value / absolutize / third_party_icon_urls /
default_icon_candidates / meta_content / parse_icon_links / manifest_icons /
is_third_party_icon / looks_generated_tile / grab_icon / ensure_utf8)
一处都没跑起来过。
修后 CLI 实测 NavFetch::meta('https://www.baidu.com') 返回
name=[百度一下,你就知道]、icons=3、图标落盘 3818 字节。
## 验证
- php -l 5 个文件全过
- 端到端 21/0:真实上传 PNG → 列表出 img.nav-ico → nav_icon.php 返 200
+ PNG 头 → 编辑换图 → 清除 → 只改简称不丢图标 → 清理
- 语义验收:带自定义图的站点点「全部更新」后 icon 文件名不变、custom=1;
不传图的站点 custom=0(以服务端回报条数为证据,不看"未变")
|
2026-10-28 12:48:47 +08:00 |
|
laoyang
|
eaa74bc593
|
fix(nav): 「我的短语」输入框被撑成 200px 高 #17
用户原话:「地址设置,短语那里,输入框怎么那么高啊。」
病根:模板把 input 包在 .field 里,而 .field 本身又是 .inline-form 的 flex 子项。
.field 是 flex-direction: column ⇒ 主轴是垂直方向,
于是 `.inline-form .input { flex: 1 1 200px }` 的 flex-basis
落在了**高度**上,再叠加 flex-grow 继续拉长。
.inline-form 的 align-items:center 又让它垂直居中,
看起来就是"一个很高的输入框 + 旁边孤零零一个按钮"。
线上实测(部署前,Playwright 量真实 rect):
短语输入框 h = 200.00px ← 正好等于 flex-basis
同页正常 input h = 45.80px
差 154.20px
修法:换用专属类 .nav-phrase-form,不动通用规则。
· 外层横向 flex(输入区 + 按钮),只把输入区当 flex 子项;
· 输入区内部仍走 .field 的纵向堆叠,并显式把内部 .input 的 flex 收回去
(这一条是关键:不收的话 flex-basis 又跑回垂直轴);
· 按钮 align-self:flex-end 跟输入框底对齐(tip 在下方,居中会显得按钮浮起来);
· 窄屏 ≤480px 按钮换行靠左。
为什么不去改 .inline-form 那条通用规则:
全站另有 15 处在用 .inline-form,且都是"input 直接当水平子项"的正确用法,
改通用规则会波及那些页面。本处是**孤例**(唯一把 .field 嵌进去的)。
验证(线上真实页面):
· 短语输入框 200.00px → 45.80px,与同页正常 input 差 0.0px
· label/input/tip 仍垂直堆叠;按钮与 input 垂直交叠 7.7px
· 回归:个人设置/任务看板/任务详情/笔记/名册/日程/微信读书/导航,
11 个 .inline-form 内的 input 全部 45.80px,零偏离
|
2026-10-28 11:57:36 +08:00 |
|
laoyang
|
34625cd7d7
|
fix(web): 上传框点选后无反馈 + 同一文件重复入库(#15)
用户报:资源模块新建时点「上传附件」选文件,页面没有任何变化,
虽然文件实际传上去了、保存也成功,但界面看不出传没传成功。
根因:暂存上传框有三个入口,只接了两个 —— 点框、拖放都能用,
**点选文件触发的 input change 事件没人监听**。
- 新增 bindStagePick():把点选接进 queueDrop(),与拖放走同一条链路,
点选后同样出缩略图卡片 / 状态文字 / 可撤掉(行为一致,不各写一套 UI)。
带去重指纹(name+size+lastModified),避免清空再选时传两遍。
- 修 slot 读错元素:data-stage-slot 渲染在框(.upbox)上而非 input 上,
原来 input.getAttribute() 恒为空串,槽位从来没传上去过。
对资源附件看不出差别,但海报/票根(2:3、按比例重裁)会拿到错槽位。
- queueDrop 增加「没清单就别跑」守卫并返回布尔说明是否接管。
顺带修掉一个我自己在第一版引入的 bug(流水线打回):
文件传完没有从 input.files 摘掉 ⇒ 同一文件同时存在于暂存令牌与 $_FILES
两条通路 ⇒ 服务端两条路径各入库一次 ⇒ 一张图存两行。
实测:点选 1 个文件 → attachments 2 行;观影第 35 批 2 张→4 行、编辑+1→6 行。
- 新增 dropFromInput():传完就把该文件从 input.files 摘掉(只摘一张,
多选时后面的还排在上传链里),只留暂存令牌一条路径。
验证:
- 点选反馈 e2e 17/0(修复前 3 红)
- 查库 e2e 8/0:点选 1 个文件 → attachments 正好 1 行(修复前 2 行)
- 反向验证 10 条全过:三处注入各自精确变红,且不得波及拖放;
并固化「同样注入下渲染探针仍全绿」这一事实
- 观影第 35 批 13/3 → 16/0
- 全量流水线 2h37m EXIT=0 已部署,线上 md5 与本地一致
|
2026-10-27 10:43:26 +08:00 |
|
laoyang
|
e826540d2a
|
修复 PDF 预览被 Chromium 屏蔽(iframe sandbox 禁用内置阅读器) #14
现象:资源模块点开 PDF 附件,预览区只剩「此页面已被 Chromium 屏蔽」。
根因:preview.js 的 pdf 分支给 iframe 挂了 sandbox
(allow-same-origin allow-scripts,引入于 v24 8f80f3e)。
Chromium 只要看到 iframe 上有 sandbox 属性就禁用内置 PDF 阅读器插件,
表现即「被屏蔽」。五组同源对照实测(Chromium 153):
· 无 sandbox → 正常渲染
· allow-same-origin allow-scripts → 屏蔽
· allow-scripts → 屏蔽
· + allow-popups-to-escape-sandbox→ 仍然屏蔽(网上答案说能救,本版本不行)
· + allow-forms allow-popups → 仍然屏蔽
⇒ 加白名单无效,只能不加。
改动:删掉该 setAttribute,换成 17 行实测结论注释防止以后被"为安全"加回去。
安全性说明:PDF 由浏览器内置阅读器扩展在独立扩展进程渲染,不是普通页面
上下文,拿不到父页 window/DOM/cookie;PDF 是原生文档,没有能访问父页的
脚本环境。服务端另有 nosniff + MIME 内容嗅探(media.php),PDF 不会被当
HTML 执行。若将来要禁止 PDF 内嵌 JS,正确做法是换 pdf.js 自渲染。
验证:
· 新增 _tooling/_pdf_e2e.py,11 条断言全通过 0 失败,已登记进流水线
(挂「全路由」标签)。判据落在渲染层:frame 列表无 chrome-error、
有内置阅读器扩展 frame、iframe 内文字不含「屏蔽」。
· 反向验证:注入 sandbox 回去 → 4 条转红;恢复后 11/0。红绿闭环成立。
· 全量流水线 2h21m 跑完,EXIT=0,已部署。
· 线上 md5 与本地一致(988b804f),setAttribute('sandbox' 已不存在。
|
2026-10-27 01:00:12 +08:00 |
|
laoyang
|
a7fc7dcb0e
|
日程表单的日历本下拉只列可写 custom 本(#4)
用户原话(2026-10-26):「网页端,添加日程,如果日历本是只读的,例如别人
共享给我只读,或者系统自带日历、观影、纪念日等,就不要出现在日历本选框中,
只出现那些可写的。」
三条判据缺一不可(只做一条都修不好):
① canWrite() —— 挡「别人共享给我且只读」。
实测:只有这一类能被 canWrite 拦住。
② !isReadonlyKind() —— 挡系统只读本(农历/节假日/传统节日/二十四节气)。
它们的 owner_id 就是我 ⇒ canWrite() 恒真,光靠 ① 一本也拦不住。
③ kind === 'custom' —— 挡观影/旅行的派生本。
这两本**确实可写**(往里加一条 = 建一条观影/旅程记录),
但不是"放日程的地方",放在新建日程下拉里只会让人建出别的东西。
- app/Calendar.php:新增 writableForEvent() / filterWritableForEvent(),
作为**唯一口径**(日历页、首页卡片、API 都走它),
避免 v82 那种"两个入口的下拉内容可以不一样"的老毛病。
判据用 READONLY_KINDS 常量而不是 kind 白名单:新增系统本只要登记就自动生效。
- templates/calendar/index.php、index.php:改用上面的口径。
- templates/calendar/_add_modal.php:清掉恒假的 disabled 与「(自动生成)」
死代码(前端已过滤,留着只会误导);保留 data-kind 给探针当锚点;
按钮色点与文案改成**服务端就带出来**(无 JS 时也看得见颜色和本名)。
验证:
- 浏览器探针 28/0:含自建夹具(只读共享 / 可写共享 / 自己的)三类,
与 PHP 侧独立重算的期望集逐项比对,真保存一次,
以及**绕过前端直接 POST** —— 只读共享回「这本日历你没有写权限。」且库无新增;
观影/旅行回 ok:true 且真的落 events 表(不进 calendar_events)。
- 反向验证 6/6 全抓住:退回 canWrite / 只看 kind / 不管 custom / 全滤空 /
只读共享漏进下拉 / 拆掉 event_add 的 canWrite 闸门。
最后一条注入时**只有 ⑪ 变红**,证明它测的是服务端拦截而非前端下拉。
- 全量流水线(--all)退出码 0,已部署并确认线上 4 个文件 php -l 全通过。
工单:#4
|
2026-10-26 14:59:57 +08:00 |
|
laoyang
|
daf5ca904e
|
v94 必填项改成「标签红色加粗」,去掉红色星号 (#3)
需求(用户 2026-10-25):必填项原来在标题后面加一个红色星号,排版经常不齐。
改成「标题本身红色 + 加粗」——用户习惯后,看到红色加粗的标题就知道是必填。
CSS(assets/css/app.css):
- :root 新增 --req:#B03A2E、--req-w:700。
· 不用 --red(#C0533F):那是动作红(删除按钮底色),当文字色放在暖纸底
#F7F3EA 上对比度只有 ≈3.4:1,13-14px 标签偏浅;#B03A2E 约 4.6:1。
· --req-w 必须 700 不能 650/600:本站标签基线字重本来就是 600
(.form-grid > .field > label / .card .field > label / .mv-row > .mv-lb /
.dt-range-label 四处都是 600),650 会被取整回 600 = 完全没加粗。
- 文件末尾新增「必填项统一标记」段,覆盖四种标签形态:
label.is-req / .mv-lb.is-req / .dt-inline-lab.is-req / .dt-range-label.is-req,
用 !important 打穿各宿主选择器的特异性(颜色与字重是语义标记,不该被面板样式盖掉)。
- 冒号跟着变红:.mv-lb::after / .dt-inline-lab::after 原本是灰色 400,
只改标签本体会剩「红字 + 灰冒号」的半截效果。
- 包裹层兜底(.field.is-req > label 等),只作用直接子级 ——
不写 .field.is-req label,那会把「提醒」那排 checkbox 的小标签一起染红。
- 深色主题给备用色 #F08A78。
- 保留 .mv-row > .mv-lb > i(万一有没清干净的星号不至于露灰星花,且静态断言会报出来)。
模板(16 个文件 / 30 处):
- _event_fields.php / _datetime.php 的标签闭包第三参数 $star → $req,
$dtLabelStar → $dtLabelReq,删掉死引用 $evLabelStar。
- 其余 14 个模板把 `<i>*</i>` 换成给标签加 is-req 类。
- 用显式 .is-req 而不靠 required 属性反推:CSS 无父选择器,:has() 旧 WebView
不可用,且影院名称/板块名本来就靠 JS 校验没有 required。
验证:
- 静态 _fb_v94_static.py 22/0,反向 9/9(含「--req-w 必须严格大于从 CSS 解析出的
标签基线字重」,不写死 700)。
- 真浏览器 _fb_v94_req.py 63/0,反向 5/5(加粗/必须红/!important 必要性/
星号必须消失/每形态单独覆盖,五类注入各自精确命中预期断言)。
- 全量流水线(含 60+ 套件)通过并部署。
- 线上真实页面 _fb_v94_live.py 31/0:观影「片名」、旅程「旅程名」、
成员「姓名」、导航「网址/所属板块」实测 color=rgb(176,58,46) weight=700,
同页非必填 color=rgb(44,35,29) weight=600,冒号同色,无星号。
|
2026-10-26 06:45:38 +08:00 |
|
laoyang
|
f1b09a2f41
|
v93: 首页日历本下拉能拉了 / 日程浮层只留编辑删除 / 重复日程三档 / Markdown 标题四级阶梯补齐
用户两条:
① 「新建日程,日历本是拉不下来的,是空的,好像只有一个"工作"什么的」
② 「点日程后的详情跟原来的弹框几乎一模一样,这个详情就没有意义了」
⇒ 只显示「编辑」「删除」;重复日程的删除/编辑要有三档
(当前 / 从现在到以后 / 全部)
【① 根因】`_add_modal.php` 被日程页与首页共用,HTML 一样,但自绘下拉的整套 JS
只内联在 `templates/calendar/index.php` 底部 ⇒ 首页那份没有交互,点了菜单不展开;
而按钮上是服务端渲染的默认本("工作"),所以看着"只有一个工作"。
修法:抽 `assets/js/cs_picker.js`(全站 defer、按 `[data-cs-picker]` 初始化、
`data-cs-ready` 幂等)。不扫所有 `.cs` —— 观影表单的影院下拉有自己的内联绑定,
两边都绑会"点一次开两次"。
【②】浮层自定义日程不再显示「详情」(派生条目仍只有真链接「详情」);
删除/编辑三档措辞按用户原话;`occ` 为空时只给「删除全部」,绝不让空串触发整条删除。
编辑三档的入口在 `fb_entry_json()` 把 `occ` 拼进 edit 地址(`?d=`)——
不带就只有"修改全部"一档。
【顺带修掉 v85 没做完的一半】全量套件在 v61 A9b 报红,查下去不是本轮改坏的:
CSS 只把 `##`(h4)调到 17px,`###`(h5)留在 15.5px(正文 15px,只差 0.5px)、
`####`(h6)留在 14.5px(比正文还小),而那段注释自己写的就是"三级都要调大"。
阶梯重排 h3/h4/h5/h6 = 22/20/18/17,四级两两不同号。
更值得记的是四条判据全部只盯 h4(v61 A9b、v85 B2/B2b 前身、静态断言、线上 A4),
而 h4 恰好是唯一修好的那一级 ⇒ 各加"每一级/最矮的一级"判据,并用
「只把 h6 打回 14.5px ⇒ A9b 仍绿、只有 A9b2 变红」证明新判据有独立价值。
【工具链】`_b61_falsify.py` 的 main_all 改成每个 case 起独立子进程 ——
原先同一进程连跑 10 遍探针,Playwright 的临时 profile 建/删会在同一轮里累计,
撞沙箱批量删除钩子被 SIGTERM(症状:输出戛然而止、退出码 1、一条 FAIL 都没有)。
验证:v93 正向 28/0 · v93 反向 3 处注入各自被抓 · v61 34/0 · v61 反向 9/9 ·
v85 md_heading 12/0 · v85 静态反向 8/0 · 全量流水线通过并已部署。
|
2026-10-25 14:52:43 +08:00 |
|
laoyang
|
a7b3d8cfd8
|
v85: 首页日程刻度按结束时间定范围 / Markdown 标题真正有层级
用户报了两处(网页版):
① 「今天的日程,有一个日程到下午五点半,首页日程刻度只有九点。」
真根因:掐头去尾只按 `time`(开始时间)算范围,`end_time` 压根没参与
⇒ 08:30-17:30 被当成"8 点半结束",endH = 8 + 2 = 10,
刻度只画到 09:00;色块高 540 分钟硬塞进 3 小时格子,被 min(96, …) 夹住糊成一片。
修法:收集「开始 + 结束」两组分钟数,endH 由 max(结束) 向上取整到小时;
保留"区间必须包含此刻"的原逻辑与下限保护。
② 「日记 markdown 代码都没解析,## 后面的是标题,现在显示的还是和正文一样。」
真根因:渲染一直是对的(`## x` → `<h4 class="md-h">`),问题全在视觉 ——
`h4.md-h` 是 15px,而阅读态正文 `.note-read-block` 也是 15px,字号完全同号;
标题只有 font-weight:500,肉眼看不出层级。
修法:h3/h4/h5/h6 分别 18/17/15.5/14.5px,父级 .md-h 字重 600、行高 1.45,
h6 用 muted 色;`:first-child` 去掉多余的顶部外边距。
反向验证:
_fb_v85_agenda_range.py 13/0(钉死 08:00 做 A/B + 真实 now 复核 5 条)
_fb_v85_md_heading.py 9/0(真实 Markdown::render + 真实 CSS + 计算值)
_fb_v85_static_falsify.py 7/0(部署静态断言 6 处注入各自都能变红)
线上真值复核(family.yangyq.net,真实库 #1853 工程管理硕士开题 08:30~17:30):
旧模板 刻度 07~09(3 行)下沿 10:00,色块 top=50%/h=50% —— 复现用户所见
新模板 刻度 07~17(11 行)下沿 18:00,色块 top=13.64%/h=81.82% = 95.46% 完整在车道内
CSS h4.md-h 17px / 正文 15px / .md-h font-weight 600
|
2026-10-24 09:31:08 +08:00 |
|
laoyang
|
4b58fec796
|
v82: 首页卡片补「新建日程 / 新增成员」+ 两处红线
用户原话:「日程,没有新建日程按钮,名册,也没有新增成员这个按钮。
另外,首页上的日程,红色线不对,变短了左侧缺了一块。在日程模块中,
那个红线左侧是不是还有一个红点?如果有,去掉。」
首页卡片就地操作(不用进模块):
- HomeCards.php 的 agenda/contacts 补 add + add_modal 元数据
- templates/calendar/_add_modal.php:从 calendar/index.php 抽出的
「新建日程」弹框片段(首页与日程页共用;片段自读变量一律加 cal 前缀,
避免 require 共享作用域覆盖宿主变量)
- templates/people/form.php:加弹框形态(pfModalOnly / pfModal),
弹框形态下「取消」走 data-fb-add-close
- index.php:首页 agenda/contacts 两个 $homeAddModals 分支;
calendar/event_add 支持 back=home 白名单跳转(不采信任意 POST 字符串)
红线(左端缺一块 / 多余红点):
- .ag-now 原来写 left: var(--ag-gutter,50px),但 v80 起它已经在
.ag-lines 内部(那层自己已有同样缩进)⇒ 左沿跑到 100px、比车道多
出一整份刻度宽。改成 left: 0
- .tg-now::before 整条删除(日程模块页那个 8px 红点)
涉及共用件(HomeCards/index.php),已跑全量:e2e 1572/0、
v82 正向 20/0、v82 反向 15/0、流水线全绿并已部署。
|
2026-10-24 04:25:02 +08:00 |
|
laoyang
|
fd90e33d94
|
v81:首页卡片「新增电影」「新建纪念日」弹框(不离开首页就把事办了)
用户 2026-10-22:
「首页上的其他卡片也应该有这些基本功能。比如电影观影,它有一个"新增电影"的功能;
如果是纪念日,就有一个"新增纪念日"。实际上,就是把原来模块里弹的那个框,现在拿到
这边来。目的就是为了在首页上就能快速完成一定工作,而不用进到模块里面。」
做法:复用模块页**同一份**表单,不另写。
- app/HomeCards.php:TYPES 每张卡多登记 add(按钮文案)/ add_modal(弹框容器 id)。
- templates/home.php:按登记表挂 data-fb-add-open="#<id>"(没登记的仍用 v78 的
data-tk-add-open,两种写法并存但各有唯一来源);页尾 require 模块的表单模板。
- assets/js/add_modal.js(新):通用开关,只认 [data-fb-add-modal] /
data-fb-add-open / data-fb-add-close;幂等闸门;Esc 先让 antd 收面板;
打开那一刻才 boot 日期控件(hidden 时尺寸为 0,antd 会定位到屏幕左上角)。
- templates/{movies,days}/form.php:只在首页内嵌时($fbAddModalAttr)挂通用属性
并输出 back=home,模块页行为一字不变。
- index.php:days/save 与 movies/create 加 back=home 白名单(取 POST 再比字面量,
不把 POST 当跳转地址)。
过程中修掉两个真问题:
- 登记表第一版写 anAddCard,而 days/form.php 实际是 anNewCard ⇒ 按钮属性写对了、
DOM 也不报错,点下去毫无反应。已加「按钮指向的容器真的存在」判据与部署静态
断言里的交叉校验(拿登记表 id 去 templates/ 里逐个找 id="...")。
- 探针判「提交后回首页」原本读 pg.url,而 wait_for_load_state(networkidle) 在
点击那一刻本就 idle 时立刻返回 ⇒ 读到提交前的地址,恒绿。改成拦那次 POST 的
响应看 (状态码, Location),拆成 ⑭a 提交真发生 / ⑭b 302 指向 home / ⑭c 浏览器停在 home。
验证:正向探针 23/0 · 反向验证 14/0(七注入各自变红)· e2e 1572/0 · 几何 428/0。
|
2026-10-22 22:05:17 +08:00 |
|
laoyang
|
b1d4502148
|
v80 首页日程:修「右侧缺一块」——色块百分比按含刻度的整宽算了
用户:「网页端,首页,日程好像显示不全,右侧是缺一块的,是不是左侧有刻度,
导致你算宽度的时候,没算对?」—— 猜对了,而且比想的严重。
原因:色块内联 `left: calc(46px + L%)` 是**绝对**让位(46px 给刻度),
`width: calc(W% - 4px)` 的百分比却相对**含刻度的整宽**(.ag-grid 481px)。
于是 100% 时色块宽 477、左端 48,右端落到 744,而卡片右边界只有 700
⇒ 多出 44px 被 .hc-agenda 的 overflow:hidden 裁掉,日历本名正好在那截里。
日历页 .tg-block 没这个病:它左边刻度是独立的一列(.tg-col),百分比天然相对列宽。
修法(换参照系,不是调数字):新增 .ag-lanes 定位层,左让刻度、右留 2px,
色块与红线都挪进去,百分比就相对真正可用的宽度算;刻度宽度收成单一变量
--ag-gutter(50px),以后改刻度只改它。内联删掉 46px。
验证四层:
- _fb_agenda_geom.py 27/0:不越界 / 左右留白 2&4px / 宽度自洽 / 与日历页同公式
- _fb_agenda_pixel.py 8/0:截图数着色列,右沿留白 5px(修前 0px)
- _fb_agenda_falsify.py 11/0:退回旧写法与车道超宽都变红,合法微调仍绿
- e2e 1558/0 · 几何 428/0 · b37 5/0
同时修两处失效探针:
- _fb_b37_ui.py 点 #taskAddBtn(v78 已改成 [data-tk-add-open])导致基线超时全红
- _fb_b37_falsify.py 注入锚点写死 calc(46px + ...),改到会重现该 bug 的 width 上
部署脚本加 v80 三条静态断言(已逐条注入确认会红)。
|
2026-10-22 20:22:54 +08:00 |
|
laoyang
|
f927d7fb23
|
v79 服务端:端上任务面板需要的三个口子
配合鸿蒙端 v79(任务面板 / 任务模块功能补齐),服务端只补三处,
其余全部复用既有路由。
① GET /api/v1/tasks?view=boards —— 只回看板 + 分类元数据,**不带任务**。
端上原来是从已加载任务里反推看板,那样只能看到"有任务的看板":
用户刚建好、还没放任务的看板在端上永远选不到(表现为"网页能建,
端上挪不过去")。
② POST /api/v1/tasks/<id>/attach(multipart 字段 file)与
DELETE /api/v1/tasks/<id>/attach?attachment_id=<aid> —— 都要先
requireWrite() 再 Task::canWrite(),两者都回整条任务(端上直接重画)。
③ ApiHandler::taskOut() —— Task::find() 只 SELECT t.*,不含看板名/色
⇒ 详情与各动作回包统一补 board_name / board_color_calc(7 个返回点)。
🔴 两条踩过的坑写进注释:
· Api::dispatch() 只解析「资源/id/子动作」三段,附件 id 不能拼进路径;
· DELETE 带 JSON body 时 Api::param 读不到(实测),id 必须走 query。
另新增 Attachment::removeFrom():删附件前先确认它确实挂在这条任务上,
拿别条任务的附件 id 来删自己的 → 404(已由 _fb_v79_api.py 钉死)。
验证:php -l 全过、e2e 1558/0、_fb_v79_api.py 26/0、已部署并冒烟。
|
2026-10-22 16:40:52 +08:00 |
|
laoyang
|
40fd6c501c
|
v78 服务端:首页任务改成「就地处理」面板 + 新建任务搬到首页
用户原话:
「网页端,首页点击一个任务后,除了展示信息,应该有相应的功能,可以修改状态:
待办、进行中,可以挪动分组,可以拖动上传附件,可以添加评论,添加子任务等。
其目地,是为了让用户不离开首页,就能对任务进行简单的处理。」
「另外,在全部任务旁边,放一个新建任务的功能,其实就是把任务中的新建和编辑功能,
搬过来,用弹出框来处理。」
一、首页任务浮层由「只读信息」改成「可操作面板」
- 新增 templates/tasks/_panel.php:状态三段 / 看板 chips(带看板色)/
子任务(复用 _task_check.php)/ 附件拖放框(复用 _attachments.php)/ 讨论
- 新增路由 tasks/panel:只吐一块 HTML(找不到 404)
- 既有七个动作路由各加一条 ajax_panel 回包分支(状态 / 挪分组 / 新建子任务 /
评论增删 / 附件增删),没有新开写接口
- helpers.php 新增 task_panel_wanted() / task_panel_json();
fb_entry_json() 的 payload 增加 panel 字段(只有任务会给)
- 每次动作回**整块新面板**:一次动作会牵动子任务计数、附件张数、看板选中态,
逐处同步 DOM 必然漏一处
- 拖进来的附件**立刻上传**(任务已存在,不该再让用户点一次保存),
不复用 _stage_drop.php 那套「暂存 + 认领」
二、新建任务搬到首页
- 从 templates/tasks/index.php 抽出公共片段 templates/tasks/_add_modal.php
(字段与原来一字未改),首页与任务页共用同一份
- 新增 assets/js/task_add.js 管开关(两页共用,幂等绑定);
打开那一刻才 fbUpgradeRange() + fbBootAntd()(hidden 时宽高为 0,antd 会定位错乱)
- App\HomeCards 登记表里加 add => 新建任务,首页卡头与「全部任务」并排
- tasks/create 支持 back=home(白名单),新建完回首页
三、其它
- assets/js/task_panel.js:事件委托;动作后整块换面板;
顺手同步首页那张卡的状态徽标与复选框(关掉浮层不能还是旧状态);
传附件时保住用户写了一半的留言与子任务标题;取不到面板时退回只读信息
- app.css 追加面板样式(含 color-mix 的 @supports 兜底)与卡片窄到宽 2 时允许折行
验证:本地 e2e 1558/0、几何 428/0 + 660/0、v78 反向验证 11/0、
片段作用域体检 35/0、php -l 与 node --check 全过
|
2026-10-22 12:40:14 +08:00 |
|
laoyang
|
036e208809
|
v77 服务端:旅程题图上传端点 + 修「PATCH 把题图清空」的真 bug
新增 POST /api/v1/trips/<id>/cover(multipart 字段 cover),
槽位走 ImageSlot 的 cover(1920×1080 / 16:9),与网页版
trips/form.php 同一条 Upload::image 管线;换图顺手删旧对象。
修一个已经发生过的静默数据丢失:端上编辑旅程走
PATCH /api/v1/trips/<id>,那个分支的 $d 里没有 cover_key
⇒ saveDetails() 里 $d['cover_key'] ?? '' 得到空串 ⇒ 题图被写成
NULL。用户在手机上只改了个名字,题图就没了。
⇒ 语义改成「没传 = 不动」,要清空必须显式传空串。
另加 Trip::setCover():换图只动 cover_key 一列,不复用
Trip::update()(那条要求 title 必填、还会重写 summary/is_public,
下次加字段漏一个就又静默清空一次)。
线上已部署并复核:ApiHandler.php / Trip.php 均含新代码、php -l 通过。
|
2026-10-22 10:14:54 +08:00 |
|
laoyang
|
8a9629650a
|
v72:日历本显示与管理合并成一张清单,颜色改用内置调色模态框
用户 2026-10-21 两条:
「日历设置里面日历本显示和日历管理可以放在一起」
「预制的颜色点了之后没反应……选颜色那个地方点了之后是弹出 Windows 的
一个颜色选择框,这个不好,我们把它改成内置的」
改动:
· calendar/display 整页删除,并入 calendar/calendars(保留 302 老地址)
· 新增 templates/_color_picker.php(按钮 + 值载体,表单/独立双模式)
· 新增 templates/_color_picker_modal.php(全页唯一模态框,预置色 + hex 手输)
· 共享本提到同一清单,标注归属与权限;owner 校验服务端做
· 清掉 display.php 时代遗留的死 CSS(.cal-set-page / .cal-set-book* 等)
部署中抓出并修掉两处真问题:
· 部署脚本「清掉被取代文件」清单漏了 display.php —— tar 只新增/覆盖,
git rm 掉的文件在线上永远留着,被第 7 段断言拦下(断言是对的,缺的是删除步骤)
· _color_picker.php 里残留 <input type="color" class="cp-trigger">:
原生取色控件既不能有子元素、也没有 data-cp-open ⇒
按钮回显失效(找不到 .cp-trigger-dot/hex)+ 模态框永远打不开,
用户点到的还是 Windows 取色框。改成 <button type="button"> + 色点 + hex
(v72 探针在修复前实测 2 条红,修复后 50/0)
|
2026-10-21 20:01:23 +08:00 |
|
laoyang
|
b45cf671d1
|
v71c:首页「通知消息」改成正规卡片,样式直接复用通知页
用户 2026-10-21:
「先把web端的首页修一下,通知消息这个,也是首页的卡片,可以调整顺序和大小。
另外,这个样式也不好看,我看个人头像点击后,通知消息那个页面的样式就不错,
直接复用就可以了。」
① 卡片化(HomeCards 注册表 + 默认布局 + 取数)
· TYPES 新增 'notif'(w4/h4,route=notifications,more=全部通知)
· DEFAULT_LAYOUT 钉在 brief 之后,与 days 并一行(宽 4)——
若插在 brief 与 agenda 之间,agenda/tasks 会被挤成一上一下(实测 C13 顶边 407 vs 751)
· data() 加 case 'notif' → 新增 notifData():最新 10 条 + 未读数,
与原来的取数口径一字不差;关掉卡片就不查这两条 SQL
· templates/home.php 删掉网格前那块写死区块(v30 第 5 条「必须在往年的今天前面」
这条约束作废 —— 那时还没有卡片体系);index.php 不再单独传 notifs/notifUnread
② 样式复用 templates/notifications/index.php
· 结构与通知页完全一致:头像在前 → 正文 → kind 图标在右(带 ntf-kind-{kind})
(旧首页那块是"图标在前、头像第二",正是用户说不好看的那一版)
· 新增 .hc-ntf 作用域只补"卡片比整页小"的三条差异:正文允许折两行(宽 2 时
单行省略会把正文全吃掉)、时间挪到正文下一行、未读数做小注脚
· 不带每条右侧「删除」按钮(4×4 的卡里塞两个按钮会把正文挤成两三个字),
删除在通知页做,卡片右上角有「全部通知」
③ 顺带修掉 v63 A1 抓住的一处:'notif' 的 mod 给 'home' 而不是空串
(通知不在 Module::LIST,但它是首页自己的卡片;空串会让「全部通知」成为
一排同色模块按钮里唯一白底 ghost 的那个)
⇒ TYPES docstring 立规矩:route 非空就必须有 mod,认不出归哪个模块就归 home
验证(familybook_push.sh 第 6 轮全绿,PUSH_RC=0,2h11m)
v62 首页卡片 56/0 · v62 反向验证 14/0(新增 M8–M12 五条注入)
b30 通知 16/0 · b30 反向验证 18/0
v63 40/0 · v63 反向验证 51/0 · e2e 1523/0 · 部署静态断言空跑 RC=0
同批带上上一轮 v70/v71 的日历修复(已单独验过,本轮流水线一并回归):
· v70 日历导入后条目不显示:+N 条从纯文本 span 改成真按钮,点开是当天全部
条目浮层;AJAX 新建后就地插入与服务端同形(回 entry,不让前端自己拼)
· v71 ICS 导入顺序:主事件 → 覆盖行 → EXDATE 由 Calendar::importIcs() 统一保证
|
2026-10-21 13:22:37 +08:00 |
|
laoyang
|
b2b103df0f
|
v68:修好日程「地点」的高德联想(v54 起静默失效)+ 账本四条动作路由
用户 2026-10-20:「网页端添加日程的时候,输入地点,应该调用高德接口,
获得准确的详细地址,现在这个功能不好用了,先修一下。」
## 根因不是 key、不是域名白名单 —— 是插件从来没被拉起来
`layout.php` 里 `fb-amap` meta 给的 SDK 地址**不带 `plugin`**,而加载器优先用它
⇒ `window.AMap.AutoComplete` **恒为 undefined** ⇒ 联想实例建不出来。
更糟的是 `addr_suggest.js` **一进函数就打 `data-addr-bound='1'`**,
于是"绑定失败"被记成"已经绑定过",此后**再也不重试** —— 全链路静默,
界面上就是"输了字什么也不弹"。
(用真浏览器逐环量过才定位:key 有效、`inputtips` 真能返回 10 条候选,
问题全在我们自己这一侧。)
修法(双保险):
- `layout.php` 新增 `needAutoComplete()`:SDK 到位后 `AMap.plugin('AMap.AutoComplete', cb)`,
超时也放行;启动顺序改成 **SDK → 插件 → 我们的脚本**。
- `addr_suggest.js` 就绪判据收紧成 `!!(AMap && AMap.AutoComplete)`;
`data-addr-bound` 改为 **new 成功之后**才打;新增 `ensurePlugin()`
并监听 `fb-amap-ready` 自愈(插件晚到也能接上)。
## 「准确的详细地址」
日程的地点**只有一栏**(`_event_fields.php` 只有 location),只把名字写回去,
回头要导航/找人还是查不到门牌。⇒ 地点框加 `data-addr-detail="1"`,
选中候选后写「名字 · 详细地址」(名字在前,人认的是名字),
并在这一行下面给一条「高德:省市区 · 门牌」提示。
⚠️ 提示行必须插在**整行之后**,塞进 `.mv-row` 会把 62px 标签列挤掉(v24 踩过)。
⚠️ 详细地址必须**延后一拍**写:AutoComplete 在 select 回调之后还会把 `poi.name`
塞回输入框,同步写会被它覆盖(第一版就因此白写)。
## 账本:服务端补四条路由
`Ledger::rename / setShare / unshare / shares / updateEntry / removeEntry`
**模型一直都有**,`ApiHandler::ledger()` 却只开了「建账本 / 记一笔 / 删账本」
⇒ 端上只能把「共享给谁」「账本名字」画成灰条,账目也改不了删不掉。
新增:`PATCH /ledger/<id>`(改名)、`/ledger/<id>/shares`(GET/POST/DELETE)、
`PATCH|DELETE /ledger/<id>/entries`(id 走 `Api::int('id')` ——
`Api::dispatch()` 只解析 3 段,`/ledger/3/entries/17` 的第 4 段会被丢掉)。
`Ledger::mine/find` 补 `is_owner`(`my_perm` 只有 read/write,分不出主人);
改共享只允许主人(422)。
## 验证
- `familybook_push.sh` 全量流水线跑到 v47 停在 1 条失败:
「③ 选中候选后地点被换成规范地名」—— 因为这条旧契约是 `loc == '万象城 CGV'`,
而我们现在**有意**写成「名字 · 详细地址」。
⇒ 断言**升级**(不是放宽):既要求名字在前、又要求门牌进来,比原来更严。
- `_fb_v47_addr_sim.py` 复验 **16 / 0**;
并做注入验证:把 `data-addr-detail="1"` 删掉 → ③ 立刻变红(证明断言真守着)。
- `_fb_v68_addr.py` 三模式 5/0(`--old` 拆掉两处时候选 0 条、`--one` 只拆 layout 仍 5/0)。
- 线上核对:`/api/v1/ledger/1/shares` 返回 **401**(不是 404,路由已生效)。
|
2026-10-20 21:35:47 +08:00 |
|
laoyang
|
1bc313adc6
|
v65+v66 服务端:笔记/纪念日两处数据丢失 + 旅程车辆与节点类型 API
## v65:两处**静默数据丢失**(网页端凑巧没暴露,走 API 的客户端全中)
1. 笔记:`Note::create()` / `Note::update()` 读的是 **`blocks`**,而 `notes()`
传的是 `body` ⇒ 新建的笔记**只有标题、一个字都没有**;`PATCH` 只改标题时
更是把**整条正文的块全删掉**(`update()` 是整组替换)。
⇒ 新增 `noteBlocksIn()`:请求给 `blocks`/`body` 就按请求替换,
都没给(只想改标题/标签)就**沿用库里的块**。
2. 纪念日:`Anniversary::find()` 的 SQL 不含 people,原来那个兜底
`$cur['people'] ?? []` 恒为空 ⇒ **只改标题的 PATCH 会把关联人全清掉**。
⇒ 新增 `annivPeopleOf()` 真去查一次。
## v66:旅程的三项设置接口(端上原来无 API 可调)
- `vehicles`:GET / POST / PATCH / DELETE(`/api/v1/vehicles[/<id>]`)
· `vehicleUsedCount()`:还有行程在用就不让删
· `vehicleInput($cur)`:请求优先、缺了沿用库里(部分更新不把别的字段清空)
- `nodeTypes`:同上;列表走 `NodeType::all()`(内部自愈补内置类型)
· 内置类型不让删(`unprocessable` 422 + 服务端原话,不是 500)
- 两条路由**与 trips 平级**(`case 'vehicles':` / `case 'nodetypes':`)——
`Api::dispatch()` 只解析 `/<res>/<id>/<sub>` **3 段**,写成 `trips/vehicles`
会被当成列表请求
- `/ping` 的 features 加 `trips_vehicles` / `trips_nodetypes`(端上可探测老服务端)
## 验证
- `php -l app/ApiHandler.php` 无错
- `familybook_push.sh`:部署完成;线上核对 `vehicles: 1` / `nodetypes: 1`,
无令牌打 `/vehicles` 回 **401**(不是 404 ⇒ 路由真的存在)
- local smoke:v43…v66 全部版本套件逐个绿(含几何/交互/DAV/vCard)
- ⚠️ 流水线最后三步因**工具问题**假红,已修工具并单独复跑全绿(与本次代码无关):
· `_fb_login_api.py` 收尾删自己的 273 文件副本,撞上执行环境的批量删除钩子
⇒ 进程被砍、退出码 1。`familybook_push.sh` 现在 `export FB_NO_PURGE=1`
(副本改名挪走不真删)⇒ 复跑 **30 / 0**
· `_fb_v57_falsify.py` 只拷 `ets/`,而守卫 v66 起要读 `resources/…/form_config.json`
⇒ 副本基线不绿。改成拷整个 `src/main` ⇒ 复跑 **12 / 0**
· 另两条单独复跑:设备会话接口 **56 / 0**、设备管理网页渲染 **23 / 0**
|
2026-10-20 18:32:06 +08:00 |
|
laoyang
|
3459eea8f3
|
v64:观影豆瓣自动获取 + 海报/多图上传(移动端 API)
端上「记一场」要与网页端同步实现(用户 2026-10-19 六条),
服务端这一半只加**移动端入口**,网页端(index.php 那条路由)一行没动。
- ApiHandler:新增 `/movies/douban` 子路由(action=suggest / action=thumb /
默认抓详情+海报落盘),逻辑与网页端 `movies/douban` 逐条对齐;
新增 `/movies/<id>/poster` 与 `/movies/<id>/photos`(multipart 上传),
字段名与端上一致(poster / photo);
新增 `normalizeFiles()` 把 `photo[]` 的多文件从"按字段竖切"转成"一行一个";
`movieData()` 认 `poster_key` 并**校验形态**(只接受 posters/xxx 形式);
features 声明 movies_douban / movie_media(端上可探测老服务端)。
- Douban:新增 `apiThumbUrl()` —— 端上打不到 `index.php?r=...`,
把豆瓣图片代理换成 `/api/v1/...` 形式(拼法只此一处);
联想候选带上**过白名单后**的原始图片地址(未过白名单给空串,不下发 SSRF 跳板)。
- Movie:新增 `setPoster()`(单字段更新 —— 端上是"先建记录再传图"两段式,
用 update() 要拼一份完整草稿,漏一个字段就静默覆盖用户刚填的内容)与
`addPhotos()`(**追加**语义,与网页表单用的 savePhotos() 替换语义并存不混用)。
验证:接口 e2e 57/0(自带豆瓣 mock,离线可跑)· 反向验证 18/0 ·
本地全量 e2e 1521/0 · 线上部署 + 站点自检 19 条全过 +
`/api/v1/movies/douban` 401(路由通)· ping.features 已含两条新能力
|
2026-10-19 23:41:15 +08:00 |
|
laoyang
|
2f7dc6ca23
|
v63:卡片配色 / 日历本靠右 / 地图题图 / 设置页清理 / 公共标签颜色 / 令牌下线
- 卡片右上角按钮改用「来自哪个模块」的主题色:HomeCards 登记表新增 mod 字段
与 module(),首页从登记表取色(原来是模板里硬编码 in_array(['agenda','tasks'])
白名单,新加带按钮的卡片会默默变灰)
- 首页日程把日历本放到色块右侧:.ag-ev 改弹性行,「时间+标题」包进
.ag-ev-main(flex:1 1 auto; min-width:0),日历本作右侧兄弟。
原来 display:block 会把日历本顶到第二行,色块一矮就完全看不见
- 旅程地图显示题图:服务端把 media_url(cover_key) 塞进地图数据,
地图卡片左侧 40x40 题图(object-fit:cover),没有题图不留灰框
- 系统设置第一块改叫「系统标语」;删掉「首页上那一行小字」提示、
「不是全家的统一口号」说明、「上次结构升级有步骤失败了」整框;
运行环境删「排错时看这一块」与「亲属连线」那一行(功能与数据未动)
- 公共标签:说明文字全删;列表与编辑合并成一行(色块+名字+N人+箭头+编辑);
person_groups 加 color 列(Schema addColumn,老库可升);点「编辑」弹框
改名字与颜色、点「完成」提交;下面那个独立编辑列表撤掉。删除挪进弹框
- 个人设置删掉「手动签发接口令牌」整块;settings/api_token 路由保留但挡下提交
(只删界面会留下知道地址就能用的后门);令牌只由登录后自动签发,
设备管理卡保留。apiTokens/apiPlain/apiLabel/apiBase 四个变量一并撤掉
|
2026-10-19 17:27:37 +08:00 |
|
laoyang
|
2e40133d8f
|
v62:影院管理补齐接口(统计 / 年度报告 / 高级搜索)+ 名册带工作经历
用户 2026-10-19:「设置里面的影院管理,这些功能都还没有,应该补充,
就是增加影院呢、修改影院呢、高级搜索、统计、年度报告,这些全部都应该补充进去。」
一、三个新子路由(都是**家族级**,必须在 `$id > 0` 分支之前处理)
GET /api/v1/movies/stats 总览 + 按影院 / 按人 / 按月 / 按年
GET /api/v1/movies/report 年度报告(以登录者为中心,?year= 可切年)
GET /api/v1/movies/search 片名 / 影院 / 人 / 起止日期的组合搜索
⚠️ 全部复用**网页端那一份 `MovieStats`**(overview/byCinema/byPerson/byMonth/
byYear/annual/years/search)—— 统计数字两边必须一致,否则用户会发现
"网页 124 部、App 121 部"。
⚠️ 三个路由都做了 **`$id === 0` 守卫**:`/movies/stats` 不能被当成"id=0 的详情"。
⚠️ `search` 在条件全空时**不查全库**(返回 has_cond=0),与网页 `hasCond` 同行为 —
否则"搜索"等于"列出全部观影"。
二、features 声明 movies_stats / movies_report / movies_search
老端上据此隐藏入口(新端上不用探测,但让能力可发现是这套约定的一部分)。
三、名册带 `jobs`(用户:「名册的列表太空了,应该显示更多内容,例如工作单位、职务」)
新增 `PersonJob::mapFor(array $personIds)` —— **一次 `IN (…)` 批量取回**。
⚠️ 不能在装饰循环里逐人 `PersonJob::list()`:名册上千人就是上千次查询(N+1)。
⚠️ 排序与 `list()` 逐字一致(当前段在前 → 开始时间倒序 → id 倒序),
否则"列表显示的单位"和"详情里第一段"会是两个不同的单位。
⚠️ 端上列表实际用的是 `persons.org` / `persons.title`(本地库有那两列),
`jobs` 是给"接口直出"的场景预备的 —— 两条链路都保留。
验证:_fb_v62_cinema_api.py 45/0(独立副本 + 真 MariaDB);
覆盖影院 CRUD 10 条 + 统计 8 + 年报 7 + 搜索 7 + 名册带 jobs 4;
php -l 全量通过。
线上:本提交对应的代码**已在线上**(13:17 那次部署带上去的,非本次操作),
真机实测统计页显示的是真实数据(横店电影城 4 / 上影影城 3…)。
|
2026-10-19 15:27:08 +08:00 |
|
laoyang
|
9a66068c33
|
v62:纪念日列表修正 / 分类图标与设置菜单 / 首页卡片体系
- 纪念日列表:display:flex 从 td 移到内层 div(写在 td 上会破坏表格列算法,
表现为关联人列与其它列高度不一致);行内列序按表头对齐;
改完再点「新建」不再带出上次修改的数据;文案改为「与我相关」「来自名册」;
别人记的、把我挂成关联人的条目一并显示(split() 返回 hiddenIds,
路由层据此过滤「接下来的日子」,与两张表同一套可见性)
- 分类图标:新增 app/AnnivIcon.php(17 个行内 SVG 图标 + 同 key 的 emoji
供日历纯文本处使用,内网无 CDN 不用 FontAwesome);去掉「看日历」按钮;
设置按钮改为菜单,第一项进 days/settings 独立页;分类可自选图标,
列表徽标与日历条目都按分类显示
- 首页卡片:新增 app/HomeCards.php 与 templates/home_cards.php;
8 格宽 × 高 2/4/6/8;首页拆成 templates/home/_card_*.php 共 8 个片段;
home/cards 可改尺寸、调顺序、开关
|
2026-10-19 12:51:17 +08:00 |
|
laoyang
|
ad7f43af68
|
v61:新增账号自助接口(GET /account + POST /account/password)
端上新增「个人设置」页要用:用户 2026-10-19 要求把抽屉里的
「通知消息 / 个人设置 / 修改密码 / 系统设置」都收进"点右上角头像"那一页,
所以端上必须能自己改密码,不能再指着用户去网页改。
GET /api/v1/account → 我的资料 + 账号信息 + 当前设备
POST /api/v1/account/password → 改密码(old_password/password/password2)
规则与网页那条改密码路由逐条对齐(不能端上比网页松):
· 必须验旧密码;
· 两次新密码一致;
· 过 Account::validatePassword()(≥8 位 + 字母数字都要有)。
额外收紧一条:新密码不能与原密码相同(网页没拦,端上拦了没坏处)。
两处刻意为之:
· 改密码**不查写权限**(网页那条路由只要求登录,guest 也能改自己的密码)
—— 加了写权限检查就会出现"网页能改、App 不能改"的权限裂缝;
· 改完**不动现有令牌** —— 用户在自己手机上改密码不该被踹下线。
features 声明补 account / account_password,端上可探测。
验证:_fb_v61_account_api.py 20/0(独立副本 + 自检 + 随机端口 + 真 MariaDB,
覆盖 5 种错误路径 + 改完旧密码登不进/新密码能登/原令牌仍有效);
php -l 全量通过;线上部署已完成。
|
2026-10-19 12:19:06 +08:00 |
|