feature/store-mobile-refractive-archive(基线 origin/test @ 63972df0)/h5/tablet 保留不动(长官 D1 拍板)这活是纯前端。后端 18 个端点、鉴权体系、机构开关、数据字典四层全部可以白拿,零改动;
真实工作量集中在把平板端 27 个文件 8548 行里的横屏布局假设按手机端绿系令牌重做,
外加一个必须先拆掉的物理障碍(seat-auth-recovery.ts 硬编码 /h5/tablet 跳转)。
🔴 9-15 更正(记账见 §八 #16):以上"后端零改动"结论只在 D1-D8 范围内成立。长官 9-15 新增
「门店结论已读回执」需求(D9)后不再成立——本期新增新字段 store_conclusion_read_at、一条 alembic
迁移、一个新 POST 端点、门店端+家长端两处 DTO 出参改动。后端改动范围详见 §九。
| # | 议题 | 拍板结果 | 对方案的约束 |
|---|---|---|---|
| D1 | 平板端去留 | 两端都留 | 这是新增不是迁移;web/app/h5/tablet/** 一行不动,回归风险=0 |
| D2 | 功能范围 | 全量对等 | 含 920 行趋势对比图 + 506 行高级视功能,手机端一个不少 |
| D3 | 双眼(OD/OS)排布 | 照抄家长端自适应三列 | grid-cols-[76px_1fr_1fr],不用 Tab、不用上下堆叠 |
| D4 | 机构开关 | 沿用 enable_refractive_record |
后端零改动;只改 1 处文案 |
| D5 | 手机端色系 | 绿系(跟手机端其他页面统一) | 长官把选择权交回;决定性理由:手机端版本主要服务没有平板的门店,不存在需要对齐的平板端 |
| D6 | 分析页呈现 | 和平板端一样(长官 9-15) | 不改后端、不在前端猜结构;p1_diag 是 string,原样段落渲染 |
| D7 | 列表手机号 | 不显示(长官 9-15) | 用真实 11 位手机号(如 15612301299)真渲染实测:360~414px 四档,行2「性别·年龄·生日·手机号」较可用宽度缺口约 3~97px(414px 最短名缺 3px,360px 最长名缺 97px),故手机端列表不展示手机号;此前用截断串 ···1299 测出的"放得下"是假结论(记账见 §八) |
| D8 | 4L 防控方案呈现 | 保持彩色卡片 2×2(长官 9-15) | 与平板端一致;长官看过效果图后确认「要现在这样」 |
| D9 | 已读回执范围 | 只做已读/未读,不做通知(长官 9-15) | 门店能看见谁没读、自己跟进;微信推送另立项,不拖本期 |
铁律:下任何结论前必须物理 grep,并带对照组证明查法有效。
下表中 4 条是证伪/纠偏 —— 说明这些结论如果凭印象写,方案就会派单出错。
原假设:手机端有个类似平板 TABLET_FEATURES 的菜单常量数组。
$ grep -n "isParentSeat" web/app/h5/chat/page.tsx
565: const [isParentSeat, setIsParentSeat] = useState<boolean>(false);
...
2929: {isParentSeat ? (
2998: ) : (
3129: )}
3130: </div>
$ sed -n '2928p' web/app/h5/chat/page.tsx
<div className="px-4 pt-1.5 pb-0 flex items-center justify-start gap-2 overflow-x-auto scrollbar-hide" id="v4ConsultEntry">
真相:chat/components/menu-sidebar.tsx 是会话历史侧抽屉(新建会话/历史/设置/退出),没有功能项数组。
手机端真正的功能入口是 chat/page.tsx:2928-3130 里一排内联 chip 按钮,按 isParentSeat 劈成两套:
门店坐席走哪套?getSeatType() 在 localStorage 无 seat_type 键时默认返回 "store"
(web/app/h5/chat/lib/auth.ts:619-626)→ isParentSeat=false → 走 2998-3129 这套。
→ 对方案的影响:加入口不是"数组加一行",要在 2998-3129 分支里插一个新的
{featureFlags.enable_refractive_record && (<button …>视光档案</button>)} 块。
$ grep -n -B3 -A8 "getSeatType" "web/app/h5/family/[id]/exam/new/page.tsx"
664- useEffect(() => {
665: if (!getSeatToken() || getSeatType() !== "parent") {
666- router.replace("/h5/chat");
667- return;
668- }
同款 gate 在 family/page.tsx:76、family/new/page.tsx:105、family/[id]/page.tsx:562 一致出现。
结论:家长端 4 个页面统一把「门店坐席」和「无 token 访客」都踢回 /h5/chat。
门店手机端必须另建路由,不能复用 family 那套页面本体(但样式与组件可以照抄,见 V13)。
$ grep -rl '1fr 108px 108px' web/app/h5/tablet/pane/panes/refractive-record/ | while read f; do echo "$f: $(grep -c '1fr 108px 108px' "$f")"; done
web/app/h5/tablet/pane/panes/refractive-record/frame5-record-entry.tsx: 11
web/app/h5/tablet/pane/panes/refractive-record/record-canvas.tsx: 5
合计 16 处;frame5 的 11 处中 1 处是注释提及,代码 10 处:
L123, L452, L485, L495, L567, L580, L598, L607, L616, L657。
record-canvas.tsx 的 5 处全部落在 dead code 上(见 V4)。
→ 当前真实渲染路径需要改的是 10 处,不是 16 处。
$ grep -rn "<PatientRail\|<RecordCanvas" web/app/
web/app/h5/tablet/pane/panes/refractive-record/patient-rail.tsx:70: // ⚠️ 本文件当前未被 refractive-record-pane.tsx 渲染(仅 import 未 <PatientRail> 实例化),
(1 命中——命中的是 patient-rail.tsx:70 自己注释里字面写的 "<PatientRail>",不是真实 JSX 渲染)
# 排除注释后,才是真正证明"0 处真实 JSX 渲染"的查法:
$ grep -rn "<PatientRail\|<RecordCanvas" web/app/ --include="*.tsx" | grep -v "//"
(0 命中)
# 对照组(证明 grep 查法能抓到真实 JSX 渲染):
$ grep -n "<Frame5RecordEntry" web/app/h5/tablet/pane/panes/refractive-record/refractive-record-pane.tsx
839: <Frame5RecordEntry
857: <Frame5RecordEntry
875: <Frame5RecordEntry
🔴 记账:v0.1 此处曾贴出未经实跑的『0 命中』,9-15 自检发现并更正——真实是 1 命中(命中自己的注释),排除注释行后才是 0。
代码自证:patient-rail.tsx:70 注释原文「本文件当前未被 refractive-record-pane.tsx 渲染(仅 import 未
结论不变:patient-rail.tsx(361行) + record-canvas.tsx(415行) = 776 行不用搬(V4 结论本身没错,错的是原证据命令贴的输出)。
$ grep -rln "max-w-\[420px\]" web/app/h5/ | wc -l
11 # 任意元素口径(不限 md: 前缀):SeatStoreCard.tsx、rumor-buster/layout.tsx、
# eye-nav/layout.tsx、ChatDemoReact.tsx、find-similar/page.tsx、family/layout.tsx、
# field-sheet.tsx、family/[id]/page.tsx、chat/page.tsx、upload-sheet.tsx、
# seat-wx-auth-landing.tsx(共 16 处命中,含 family/[id]/page.tsx ×2、chat/page.tsx ×2)
$ grep -rln "md:max-w-\[420px\]" web/app/h5/ | wc -l
5 # 页面容器口径(限定 md: 前缀):eye-nav/layout.tsx、family/layout.tsx、
# rumor-buster/layout.tsx、find-similar/page.tsx、chat/page.tsx
# (共 7 处 grep 命中,其中 eye-nav:8 与 family/layout:3 是注释提及,真代码 5 处一一对应上述 5 个文件)
$ grep -rn "430px" web/app/h5/
(0 命中)
"360" 的全部命中都是 SVG viewBox 坐标系 / 弹层 maxWidth: 360,不是页面容器设计基准。
结论:唯一有代码实证的宽度规范是容器 md:max-w-[420px](5 个文件真代码)。方案与效果图一律按 420px 写,
不得在文档里写"设计基准 360"(无出处)。
$ grep -c "require_store_seat" app/api/refractive_records.py → 21
$ grep -c "@router\." app/api/refractive_records.py → 18
18 个 @router. 端点,每个函数签名都带 seat: dict = Depends(require_store_seat),
18 对 18、一一对应、无端点漏挂鉴权;多出的 3 次是 docstring / import / 注释。
$ grep -rn "device\|platform\|Source\b" app/api/refractive_records.py app/services/refractive_record_service.py app/schemas/refractive_record.py
(0 命中)
# 对照组:
$ grep -n "device_id" app/auth/middleware.py | head -1
256: # 镜像 require_org_seat...不同 device_id 并存放行
$ grep -rln "platform\b" app/api | head -3
app/api/store_subseat.py / app/api/admin_parent_seats.py / app/api/voice.py
纠偏:不是"0 处 tablet/平板字样"——app/api/refractive_records.py:1 docstring 写着
「Phase 63 · 平板端视力与屈光检查记录 API」,refractive_record_service.py:1076 有一处注释引用了
mockup 文件名含 tablet。两处都是注释不是逻辑分支,
所以「后端逻辑零改动可直接给手机端复用」成立,但不能说"0 处 tablet 字样"。
端区分靠鉴权层:app/auth/middleware.py:398-409 require_store_seat 显式拒 seat_type=="parent"。
// web/app/h5/family/components/field-sheet.tsx —— 导出 4 个(不是 2 个)
export function FieldButton({ value, placeholder = "请选择", onClick, ariaLabel })
export function SheetShell({ title, onClose, children })
export function OptionSheet({ title, options, value, onPick, onClose })
export function MultiOptionSheet({ title, options, value, onConfirm, onClose })
// web/app/h5/family/components/date-wheel.tsx —— 还导出 TimeWheel 与主题类型
export interface DateWheelAccent { border: string; bg: string; }
export function DateWheel({ value, onChange, minYear, maxDate, ariaPrefix = "出生", accent = DEFAULT_ACCENT })
export function TimeWheel({ value, onChange })
// web/app/h5/chat/components/upload-sheet.tsx
export type UploadKind = "refraction" | "axial" | "oct" | "fundus";
export function UploadSheet({ open, onOpenChange, kind, onConfirm })
⚠️ date-wheel.tsx:23 注释写着「平板门店端「视光档案」是蓝色主题(#2563eb)」→ 手机端复用时
必须传自己的 accent(绿系),否则会把平板蓝带进手机端。
⚠️ UploadKind 目前只有 4 种,若手机端要传视光档案的其他影像类型需先扩这个 union。
$ grep -n "fetchSeatOrgFeatures\|applySeatOrgFeatures" web/app/h5/chat/page.tsx
852: if (getSeatToken() && !urlTempToken) {
853: const seatState = await fetchSeatOrgFeatures();
3281: applySeatOrgFeatures(orgFeatures);
3282: setFeatureFlags(applyParentFeatureGate(getOrgFeatureFlags()));
$ grep -n "enable_refractive_record" web/app/h5/chat/lib/auth.ts
276: enable_refractive_record?: boolean | null;
结论:手机端 bootstrap 阶段早就在拉机构开关,而且 auth.ts:276 的类型里
Phase 63 当时就把 enable_refractive_record 字段准备好了。
入口门禁直接 getOrgFeatureFlags().enable_refractive_record 即可,零新增。
$ sed -n '456,458p' "web/app/(dashboard)/admin/orgs/page.tsx"
<div className="font-semibold text-slate-800 text-sm">视光档案</div>
<p className="text-xs text-slate-500 mt-1 leading-relaxed">
开启后,本机构席位可在平板端使用视光档案功能;默认关闭
全仓把"视光档案"与"平板端"绑定、且属于用户可见 UI 文案(非注释)的,只有这 1 处。
子代理只查了 web/app 就判"文件不存在"。扩范围复查:
$ find web/app -path "*orgs/new*" -o -iname "*switches*" | wc -l
0 # ← 代理据此误判"不存在"
$ find settlement/web -path "*orgs/new*" -name "*.tsx" -o -path "*switches*" -name "*.tsx"
settlement/web/app/platform/minicup/orgs/new/page.tsx
settlement/web/app/platform/minicup/orgs/[orgId]/switches/page.tsx
$ grep -rn "enable_refractive_record" settlement/web/app/platform/minicup/orgs/new/page.tsx settlement/web/app/platform/minicup/orgs/\[orgId\]/switches/page.tsx
new/page.tsx:41: { key: "enable_refractive_record", label: "视光档案", defaultOn: false },
switches/page.tsx:68: { key: "enable_refractive_record", label: "视光档案", desc: "开放视光档案模块" },
# 对照组(证明查法有效):
$ grep -rn "enable_myopia_world" settlement/web/app/platform/minicup/orgs/new/page.tsx | head -2
29: | "enable_myopia_world"
62: { key: "enable_myopia_world", label: "近视的世界", defaultOn: true },
结论:两个文件真实存在,但它们的文案是 label: "视光档案" / desc: "开放视光档案模块",
都没提"平板端" → 全仓只有 1 处文案要改,不是 3 处。
平板端 lib/api.ts 本体是纯 REST 客户端无 UI,但依赖链里带了平板专属跳转:
$ sed -n '8,9p' web/app/h5/tablet/pane/panes/refractive-record/lib/api.ts
import { authHeaders } from "../../../../../chat/lib/auth";
import { recoverOrRefreshSeatAuth } from "../../../../lib/seat-auth-recovery";
$ grep -n "/h5/tablet" web/app/h5/tablet/lib/seat-auth-recovery.ts
27: window.location.href = "/h5/tablet?login=seat";
49: window.location.href = "/h5/tablet?login=seat";
51: // 门店密码平板(非微信):标记被踢 + 跳纯 /h5/tablet · 门户读 flag 显「已在其他设备登录」+席位登录窗。
59: window.location.href = "/h5/tablet";
(真跑是 4 行,第 51 行是注释提及不是真实跳转逻辑——跟 V3/V4/V5 排除注释行的处理方式对齐,
真正的条件跳转只有 27/49/59 三处。9-15 第二轮自检核实:v0.1 原贴的这段 grep 输出漏了第 51 行。)
后果:手机端若直接复用 api.ts,401 续期失败时会把手机端用户踢到平板登录页。
这是本次唯一必须动到平板侧代码的地方 —— 需把跳转目标参数化(或给手机端另建一份 recovery)。
⚠️ 参数化会动到平板端在用的文件,属于 D1「平板端不动」的唯一例外,需长官知悉。
$ sed -n '283,290p' "web/app/h5/family/[id]/exam/new/page.tsx"
<div className="grid grid-cols-[76px_1fr_1fr] gap-2 items-center mt-2.5">
<div />
<div className="text-[11px] font-extrabold text-[#94a3b8] text-center">右眼 OD</div>
<div className="text-[11px] font-extrabold text-[#94a3b8] text-center">左眼 OS</div>
平板:1fr 108px 108px(眼格宽度写死,窄屏缩不了)
家长端手机:76px 1fr 1fr(标签写死 76,两眼平分剩余宽)
🔴 记账(9-15 第二轮自检更正):v0.1 原算式只减了 Sect 卡的 p-3.5(14px×2),漏减了外层页面容器的
px-4(16px×2)。真实嵌套链(已 grep+sed 逐层核实):
$ cat web/app/h5/family/layout.tsx (节选)
<div className="relative w-full md:max-w-[420px] min-h-screen shadow-2xl">
$ sed -n '931p' "web/app/h5/family/[id]/exam/new/page.tsx"
<div className="px-4 pt-2.5 pb-8">
layout.tsx 上的 md:max-w-[420px] 只在 ≥md(768px)断点生效——手机端真实视口(<768px)走的是
w-full,容器宽 = 设备真实视口宽,不是固定 420px;px-4(16px×2=32px)是在这层 w-full 容器
内部又包了一层,v0.1 算式漏掉了它。
重算(容器宽 − px-4×2(32) − Sectp-3.5×2(28) − 标签列76 − gap-2×2(16))÷ 2:
| 容器宽 | 算式 | 算式结果 | 等价复现真渲染实测 | vs 平板 108px |
|---|---|---|---|---|
| 420px(PC 微信内嵌壳的桌面断点宽,非手机端真实视口) | (420−32−28−76−16)÷2 | ≈134px | 133px | 更宽 |
| 375px(常见 iPhone 视口,经验参考) | (375−32−28−76−16)÷2 | ≈111.5px | 110.5px | 更宽 |
| 360px(常见安卓视口,经验参考——V5 已证"非代码断点",见下方说明) | (360−32−28−76−16)÷2 | ≈104px | 103px | 更窄(103 < 108) |
⚠️ 结论更正:v0.1 写的"两者都比平板的 108px 宽"是错的,已删除。真实情况是窄屏(360px 附近)
下手机端两眼格反而比平板窄——落地时若目标机型含 360px 附近安卓机,需额外确认这个更窄的眼格是否够放
输入框文字/单位。
360px 仍作为数据点保留的理由:V5 已证"360 是页面容器设计基准"这个说法查无实据,但 360px 作为安卓机型
常见的真实屏幕宽度(经验值,与"代码里有没有写死这个断点"是两回事)依然是本功能要适配的下限场景之一,
故本表按"最窄常见机型经验值,非代码断点"的口径保留它,不因 V5 的证伪而删除这个数据点。
"等价复现真渲染实测"列的方法(9-15 二轮自检真实补测,如实说明局限):本功能 mockup v1 尚未画出,
无法直接打开本功能的效果图去量;因此改为用 exam/new/page.tsx 已上线组件的真实 className 对应的原生
CSS 数值(px-4/p-3.5/grid-cols-[76px_1fr_1fr]/gap-2)搭建等价静态页面,用 headless Chromium
(playwright-core + @sparticuz/chromium,与 ~/render-env 同款依赖)真跑 getBoundingClientRect()
分别在 420/375/360 三个视口宽度下量得上表数字(与算式相差 ≤1.5px,来自边框 border-box 计算误差)。
这不是打开本功能真实 mockup 或线上页面测的数,是对已上线组件类名的等价复现渲染,如实标注方法与局限,
不冒充"已看过本功能真页面"。
⚠️ 定稿前仍需在真正画出来的效果图 / 落地后的真实页面上重新 getBoundingClientRect() 验证一遍
(见第七节验收项2;不预设具体数值,要求报实测值)。
$ find web/app -maxdepth 2 -name "page.tsx" | wc -l → 5 # 假象
$ find web/app -name "page.tsx" | wc -l → 91 # 真实
$ find web/app/h5 -name "page.tsx" | wc -l → 15
maxdepth 2 把 h5/family/[id]/exam/new/page.tsx 这类深层路由全滤掉了。报数必须写清查询范围。
web/app/h5/tablet/pane/panes/refractive-record/ · 27 个文件 / 8548 行
| 帧 | 文件 | 行数 | 职责 |
|---|---|---|---|
| 主壳/状态机 | refractive-record-pane.tsx |
1037 | 按 activePatient/record 切帧 |
| 帧1 | frame1-child-list.tsx |
287 | 儿童列表(落地态) |
| 帧2 | frame2-create-modal.tsx |
426 | 建档/编辑弹窗 |
| 帧3 | frame3-patient-profile.tsx |
426 | 档案详情 + 历次时间线 |
| 帧3b | frame3b-bind-selector.tsx |
217 | 绑定家长子席位 |
| 帧4 | frame4-record-view.tsx |
1613 | 记录查看 + AI分析 + 门店结论 + 推送 |
| 帧5 | frame5-record-entry.tsx |
783 | 记录录入 + AI识别 |
| — | trend-chart-dialog.tsx |
920 | 历次对比趋势图弹层 |
| — | advanced-vision-section.tsx |
506 | 高级视功能录入 |
| — | 其余组件 ×9 | 957 | conflict-notice(32)/sample-block-dialog(42)/history-drawer(92)/birth-date-field(119)/parent-readonly-dialog(125)/visit-date-field(126)/analysis-progress-panel(127)/vision-acuity-picker(134)/constrained-number-picker(160) |
| — | lib/ ×7 |
600 | api/gate/age/va-map/vision-format/clinical-data/render-bold |
| ~~dead~~ | ~~patient-rail.tsx + record-canvas.tsx~~ |
~~776~~ | 不搬(V4 已证) |
实际要搬 = 8548 − 776 = 7772 行
app/api/refractive_records.py prefix /api/h5/refractive-records,18 端点全 require_store_seat:
| 分组 | 端点 |
|---|---|
| 儿童 | GET /patients · POST /patients · POST /patients/avatar-upload · PATCH /patients/{id} |
| 绑定 | POST /patients/{id}/bind · DELETE /patients/{id}/bind |
| 时间线 | GET /patients/{id}/records |
| 记录 | POST /records · GET /records/{id} · PATCH /records/{id} · DELETE /records/{id} · POST /records/{id}/complete |
| 家长视图 | GET /records/{id}/parent-view |
| 对比 | GET /records/{id}/comparison |
| AI | POST /records/{id}/analysis · POST /ai-recognize |
| 结论 | PATCH /records/{id}/conclusion · POST /records/{id}/conclusion/push |
数据层:refractive_records + refractive_record_eyes + refractive_record_visual_functions 三表。
机构隔离:org_id 由 JWT 服务端推导,不接受客户端传(refractive_records.py:5 文件头声明)。
⚠️ 别搬错文件:app/api/vision_exams.py(router = APIRouter(prefix="/api/vision-exams", tags=["vision_exams"])——
tag 是英文 vision_exams,"视光档案"字样只出现在模块 docstring 第 2 行,不是 tag;无任何鉴权 Depends)
是另一套医生端/飞轮标注功能,与本功能无关。
web/app/h5/chat(主)+ web/app/h5/family(家长档案)md:max-w-[420px](页面容器口径,5 个文件真代码;另有 16 处不限 md: 前缀的其他元素级 max-w-[420px],两个口径不同,详见 V5)width=device-width, maximumScale:1, userScalable:false(禁双指缩放 · 微信 webview)web/app/h5/chat/lib/auth.ts(tablet/page.tsx:24 直接 import)#157c67 / #2fa37c / #eaf7f0 / #9bdcc8 / 边框 #e3eee7 / 必填红 #d1493f)→ D5 拍板本功能跟随此绿系web/tailwind.config.ts 的 shadcn 语义色在这些页面 0 使用,全内联 hexMobileTabBar.tsx 真实路径是 web/components/mobile/MobileTabBar.tsx,只被 web/app/mobile/layout.tsx import 使用,跟本功能所在的 web/app/h5/* 路由树无关)middleware.ts,无 UA 分流;平板/手机靠业务侧发不同链接新建 web/app/h5/store-refractive/ ← 路由技术 slug 待长官定(功能显示名已定「视光档案」,见 R2)
├─ layout.tsx 仿 family/layout.tsx(md:max-w-[420px] 手机框)
│ + 仿 chat/layout.tsx(viewport 禁双指缩放)
├─ page.tsx 帧1 儿童列表(落地态)
└─ [patientId]/… 帧3 档案 / 帧4 查看 / 帧5 录入
ls web/app/h5/ 已证不存在 store/refractive/refractive-record 目录(对照组 family 命中,查法有效)→ 命名空间可用。
入口插入点:web/app/h5/chat/page.tsx 非家长分支 2998-3129 内,新增
{featureFlags.enable_refractive_record && (<button …>视光档案</button>)},样式仿同排既有 chip。
排位:长官 9-15 已拍板 —— 排第一位(9-15 更正:原方案给的两个候选插入点"报告秒读组后"/
"量身定案后"均已作废,两者都不是"第一位",需重写为插到 2998-3129 分支最前面,即现有
"报告秒读" chip 之前)。这意味着现有 7 个 chip 依次后移一位,原排第一的"报告秒读"chip 让位到第二位,
需在实施时一并调整。
依据(本仓已有同类先例支持"核心功能排第一位"这个判断,9-15 grep 核实引用):
chat/page.tsx:3000-3001 注释原文——
「Phase 66.12(长官 2026-08-01)· 报告秒读排最前 —— chip 条横向溢出(实测内容 835px / 视口 375px),
排第 3 位时只露 28px,4L 报告解读是核心功能不能藏在横滑里。历史解读紧随其后。」
视光档案同样是不能被横滑藏起来的核心功能,故沿用同一条判断逻辑排到最前。
决策经过(如实记录,便于日后复盘):同一天(9-15)经历两次拍板——长官第一次拍板蓝系
(理由:跟平板端同功能视觉统一),随后长官自己推翻第一次拍板、把色系选择权交回本方案,
最终改定为绿系(D5,见第一节)。以下 4.2 全部令牌与配色都按最终定案的绿系写,不含中途蓝系方案。
决定性理由:门店手机端之所以要做,是因为有些门店没有平板;对这批目标用户
不存在一个需要对齐的平板端,他们的全部体验就是绿色的手机应用(从 chat 页绿色 chip 条进入)。
「两端切换不错位」服务的是本功能目标人群里基本不存在的用户。
另:本仓库为「拿平板/PC 色画手机端」已付过学费(见~/.claude/rules/prototype-mockup-standards.md§1.2)。
全部取值扒自 web/app/h5/family/[id]/exam/new/page.tsx(已上线的手机端长表单),一个色都不许自造:
| 元素 | 取值 | 出处 |
|---|---|---|
| 帧内页面背景 | #f8fbf9 |
exam/new/page.tsx 缩略图背景同款 |
| 卡片 | background:#fff; border:1px solid #e3eee7; border-radius:16px; padding:14px |
exam/new/page.tsx:48-58(Sect) |
| 分段标题 | 13.5px/800; color:#1f3a30 |
同上 |
| 字段标签 | 12px/700; color:#1f3a30 |
exam/new/page.tsx:60-71(Fld) |
| 必填星 | #d1493f |
同上 |
| 字段右上 note | 11px/500; color:#94a3b8 |
同上 |
| 输入框 | bg:#fff; border:#dbece3; radius:12px; 13px; color:#1f2a24; min-h:42px |
exam/new/page.tsx:38-39(INP) |
| AI 回填态输入框 | bg:#eaf7f0; border:#9bdcc8; color:#157c67; 700 |
exam/new/page.tsx:40-41(INP_AUTO) |
| 主按钮 / 药丸选中 | linear-gradient(100deg,#1f3a32,#2fa37c) + 白字 |
exam/new/page.tsx:74-96(Segs) |
| 次按钮 | #fff + 1px solid #e3eee7 + 文字 #4a6a5b |
同上 |
| 拍照按钮 | 68×68; radius:12px; border:1px dashed #9bdcc8;图标文字 #2fa37c |
exam/new/page.tsx:99-145(UpBlock) |
| 识别成功条 | border:#bfe4d3; bg:#eaf7f0; color:#157c67 |
exam/new/page.tsx:1036-1046 |
| OD/OS 列头 | 11px/800; color:#94a3b8; text-align:center |
exam/new/page.tsx:283-290 |
| 4L 四色卡 | L1 #d1493f / L2 #c99a2e+#fff3d9 / L3 #2f855a+#e6f6ec / L4 #7c6bb5 |
真机截图 vf-tablet-05-record-view-ai.png |
| 橙提示条 | border:#fde7c7; bg:#fff8ed; color:#9a4a00 |
全局规范 + frame4-record-view.tsx:1229,1233 |
🔴 严禁把平板端蓝系带进手机端(平板蓝 = #2563eb / #173a54 / #43617a / #eef4f9)。
🔴 复用 DateWheel 时必须显式传绿系 accent —— date-wheel.tsx:23 注释写着
「平板门店端『视光档案』是蓝色主题」,不传就会继承平板蓝。
绿色在本功能里同时是语义色,底色变绿会让这几处信号变钝:
| 语义绿 | 现状 | 补法(待 v2 落地) |
|---|---|---|
| 「该记录已完成」状态条 | #e6f6ec 底 #2f855a 字(frame4-record-view.tsx:1214) |
改用蓝做强对比,把蓝留给状态而不是底色 |
| AI 回填态输入框 | #eaf7f0 底 #157c67 字(exam/new/page.tsx:40-41) |
同上,改蓝描边/蓝字 |
| 4L 方案 L3 Long 绿卡 | #2f855a+#e6f6ec |
四卡各自带底色;绿底上是否仍可分辨 —— 必须真渲染看过再定,不许推断 |
mockup v1 用的仍是「全绿」口径,上述补法未实施。 看图时这三处的信号强弱请按「待优化」看待。
| 帧 | 平板端布局假设 | 手机端改法 |
|---|---|---|
| 帧5 录入 | 1fr 108px 108px ×10 处 |
→ grid-cols-[76px_1fr_1fr](D3 拍板) |
| 帧3 时间线卡 | 36px repeat(4,1fr) 四格横排(frame3-patient-profile.tsx:374,384,400) |
→ 2×2 四宫格,或纵向"指标:值"列表 |
| 帧4 查看 | 右侧 w-[380px] 固定侧卡(frame4-record-view.tsx:850)+ repeat(4,1fr)(:244) |
→ 侧卡降为纵向分段;四格 → 2×2 |
| 趋势图 | w-[92vw] max-w-[1100px] h-[86vh](trend-chart-dialog.tsx:661) |
→ 全屏 Sheet;指标列数按 420px 重排(信息架构要重做,本期最大单点) |
| 历史抽屉 | w-[520px](history-drawer.tsx:42) |
→ 底部 Sheet 全宽 |
| 家长只读弹层 | w-[560px](parent-readonly-dialog.tsx:57) |
→ 底部 Sheet 全宽 |
| 数字/视力选择器 | 两者均为 w-[420px](constrained-number-picker.tsx:94、vision-acuity-picker.tsx:70;无 380px——380px 实际出现在帧4侧卡 frame4-record-view.tsx:850〔已单列于上方〕与提示弹窗 sample-block-dialog.tsx:16〔不是选择器〕) |
→ 复用 field-sheet.tsx 的 OptionSheet |
| 高级视功能 | 1fr 1fr(advanced-vision-section.tsx:453) |
→ 保持两列(窄屏可接受)或降一列,实测定 |
| 左菜单 240/60px | 平板壳专属(web/app/h5/tablet/pane/panes/menu-sidebar.tsx:85,是页面级 pane 壳 TabletMenuSidebar 的一部分,不在 refractive-record/ 27 文件范围内) |
手机端不要,用返回 pill(家长端惯例) |
| 文件 | 行数 | 判定 | 证据 |
|---|---|---|---|
gate.ts |
11 | ✅ 直接 import | grep className/style/px/w-[ 0 命中 |
age.ts |
24 | ✅ 直接 import | 同上 |
va-map.ts |
45 | ✅ 直接 import | 同上;已有跨端先例:exam/new/page.tsx:29 已 import |
vision-format.ts |
42 | ✅ 直接 import | 同上 |
clinical-data.ts |
54 | ✅ 直接 import | 同上 |
render-bold.tsx |
22 | ✅ 直接 import | 输出 <strong>(加粗片段)与 <span>(其余片段)两种标签,9-15 二轮自检更正 v0.1 "仅 <span>" 的说法;核心判定不变——两者均无任何 className/style |
api.ts |
402 | 🔴 需先拆 | 依赖 seat-auth-recovery.ts 硬编码 /h5/tablet(:27/:49/:59) |
跨端 import 合法性:exam/new/page.tsx:26-28 注释原文称 va-map.ts 是
「门店端唯一事实源(严禁改动该文件/严禁复制新表)」→ import 纯逻辑文件是本仓既定模式。
而 menu-sidebar.tsx:4 的红线「平板独立组件 · 不碰移动端」只针对平板专属 UI 组件,两者不冲突。
另:render-bold.tsx:9-11 注释已记载「理想位置是 web/app/h5/lib/,但该目录不存在,本次不新建顶层共享目录」
→ 本方案沿用同一做法,不新建共享目录(除非长官另有指示)。
web/app/(dashboard)/admin/orgs/page.tsx:458
- 开启后,本机构席位可在平板端使用视光档案功能;默认关闭
+ 开启后,本机构席位可在平板端和手机端使用视光档案功能;默认关闭
settlement 两处(orgs/new/page.tsx:41、orgs/[orgId]/switches/page.tsx:68)文案未提"平板端",不用改(V11 已证)。
D2 拍板全量对等。按防丢失策略拆批,每批可独立 commit。
| 批 | 内容 | 预估行数 |
|---|---|---|
| 批0 | 拆 seat-auth-recovery 跳转参数化(V12 障碍)+ 建路由壳 + 入口 chip + 门禁 |
~250 |
| 批1 | 帧1 儿童列表 + 帧2 建档弹窗 | ~700 |
| 批2 | 帧3 档案详情 + 时间线(四格→2×2)+ 帧3b 绑定 | ~650 |
| 批3 | 帧5 录入(10 处三列改造)+ AI 识别 + 选择器 | ~1100 |
| 批4 | 帧4 查看 + AI 分析 + 门店结论 + 推送 | ~1600 |
| 批5 | 高级视功能(506)+ 各类弹层降级为 Sheet | ~900 |
| 批6 | 趋势对比图 420px 信息架构重做 | ~900 |
| 批7 | 测试(仿 web/__tests__/tablet-refractive-* 19 个文件命名法,前缀 mobile-refractive-) |
— |
| 批8 | 已读回执(D9 新增需求·字段+迁移+端点+两端 DTO 出参+两端 UI 展示,详见 §九) | ~150 |
批0-6 预估合计 6100 行,与 §3.1「实际要搬 7772 行」不是同一口径,差额 1672 行的来源:
api.ts(402 行,本次只改跳转参数化,不整体重写)外,import 复用(gate.ts11 + age.ts24 + va-map.ts45 + vision-format.ts42 +clinical-data.ts54 + render-bold.tsx22),批次估算里不计入"新写/改造"工作量。🔴 记账(9-15 第二轮自检更正,详见 §八 #14):此前版本在这里还写过一句"另一方面 §4.3 末行'左菜单
240/60px'混在 refractive-record-pane.tsx 整体 1037 行里,没有可单独 grep 出的行区间"——
这句话本身没有 grep 验证过。真跑 grep -n "240\|60px\|menu\|sidebar\|Menu\|Sidebar" refractive-record-pane.tsx
是 0 命中(该文件确认 1037 行,通篇不含 240/60px 或菜单字样)。240/60px 真实出处是
web/app/h5/tablet/pane/panes/menu-sidebar.tsx:85(width: collapsed ? 60 : 240)——这是平板
通用 pane 壳组件 TabletMenuSidebar,被 pane/page.tsx 引入,与 refractive-record-pane.tsx
是平级的两个不同文件,根本不在本方案 27 文件/8548 行的统计范围内,因此它与这条行数差额无关。
已删掉这句假解释。
| # | 事项 | 状态 |
|---|---|---|
| R1 | seat-auth-recovery.ts 参数化会动到平板端在用文件,是 D1「平板不动」的唯一例外 |
已核查通过并获长官批准可改(9-15) |
| R2 | 路由目录技术 slug 待定(store-refractive/refractive 等)——⚠️ 这只是 URL 路径的技术命名,功能显示名已定为「视光档案」(长官 9-15),两者不是一回事 |
待长官定(仅 slug,非显示名) |
| R3 | 入口 chip 排位 | 已拍板:排第一位(长官 9-15)——原排第一的"报告秒读"chip 后移一位,详见 §4.1 |
| R4 | 趋势图在 420px 宽内的信息架构,是本期唯一"要重新设计"的部分,不是照搬 | 出图时定 |
| R5 | UploadKind union 只有 4 类,视光档案若需更多影像类型要先扩 |
实施时定 |
| R6 | 本机不跑全量 e2e,最终验收需去 test 环境 | 遵云机铁律 |
| R7 | 取证时 test 门店账号 0034202607 被触发首登改密(密码沿用 814017,另填占位手机号 13800000000)→ 该账号无法再复现"首次登录改密"流程 |
已发生 · 已报备 |
| R8 | 绿系下「已完成」状态条 / AI 回填态 / L3 绿卡三处语义绿信号变钝,补法见 §4.2,mockup v1 未实施 | 待 v2 |
| R9 | 实施注意:手机端列表不展示手机号(D7),这是手机端与平板端的一处有意差异——实施时不要照抄平板 frame1-child-list.tsx:244({displayPhone && (...)} 分支)的手机号展示逻辑,手机端建档/搜索仍可用手机号,唯独列表行不渲染该字段 |
已定(9-15) |
grep -rn "1fr 108px 108px" <新建目录> --include="*.tsx" | grep -v "//" | wc -l = 0;grep -n "1fr 108px 108px" web/app/h5/tablet/pane/panes/refractive-record/frame5-record-entry.tsx | grep -v "//" | wc -l = 10。// 注释行——frame5-record-entry.tsx:116 是注释提及不是真实样式,裸 grep -c 会把它计进去变成 11;record-canvas.tsx(该文件另有 5 处同串命中)——V4 已证 record-canvas.tsx 是 dead code(0 处真实 JSX 渲染),不在迁移范围内,混进对照组会失真。getBoundingClientRect,报出 OD/OS 三列实测宽度(不预设具体数值——V13 的 134/111.5/104pxdocument.documentElement.scrollWidth > window.innerWidth(🔴 不得用"子元素 vs 父元素"docs/superpowers/plans/specs/ui-design-spec-dev2-20260824.md:264-265),grep -o "#[0-9a-fA-F]\{3,6\}" | sort -u 必须是 §4.2 全部令牌值(含绿系基底 + 必填红 #d1493f / 橙提示 #fde7c7,#fff8ed,#9a4a00 / 黄 4L #c99a2e,#fff3d9 / 紫 4L #7c6bb5 等语义色)的子集;#2563eb / #173a54 / #43617a / #eef4f9 命中数 = 0;对照组 #2fa37c 命中 > 0git diff --stat 对 web/app/h5/tablet/** 的改动只允许 seat-auth-recovery.ts 一处feature/store-mobile-refractive-archive(基线 origin/test @ 63972df0)的物理代码本节是防再犯的记账钩子。v0.1 交付时 13 处问题全部经本机 grep/wc/sed 真跑核实后修订,逐条如实列出,不美化、不省略。
| # | 问题 | 原文怎么写 | 真实情况 | 已改成什么 |
|---|---|---|---|---|
| 1 | V4 证据伪造 | grep -rn "<PatientRail\|<RecordCanvas" web/app/(0 命中) |
真跑是 1 命中(patient-rail.tsx:70 注释里字面写了 <PatientRail>,命中自己的 pattern) |
贴真实 1 命中输出 + 补一条排除注释后为 0 命中的命令 + 记账声明 |
| 2 | §3.1「其余组件 ×8|~800」 | ×8 个文件,约 800 行 | 真实是 9 个文件、957 行(conflict-notice 32 + sample-block-dialog 42 + history-drawer 92 + birth-date-field 119 + parent-readonly-dialog 125 + visit-date-field 126 + analysis-progress-panel 127 + vision-acuity-picker 134 + constrained-number-picker 160) | 改为「其余组件 ×9|957」,逐文件行数列出;表格相加 = 8548(已验) |
| 3 | §4.3 选择器宽度 | w-[420px] / w-[380px],无出处 |
两个选择器(constrained-number-picker.tsx:94、vision-acuity-picker.tsx:70)都是 420px,无 380px;380px 是 frame4-record-view.tsx:850(侧卡)与 sample-block-dialog.tsx:16(提示弹窗,不是选择器) |
改为「两者均为 w-[420px]」+ 补 file:line 出处 |
| 4 | §7 验收项1不可执行 | 对照组"平板端仍为 10",未给可跑命令 | 裸 grep -c 在 frame5 得 11(含注释),全目录得 16(含 dead code) |
改成可复制粘贴、真跑出 10 的命令,并注明排除注释行 + 排除 dead code 的理由 |
| 5 | §7 验收项4自相矛盾 | 色值必须是"§4.2 绿系清单的子集" | §4.2 本就含红 #d1493f、橙 #fde7c7/#fff8ed/#9a4a00、黄 #c99a2e/#fff3d9、紫 #7c6bb5 |
改为"§4.2 全部令牌值(含绿系基底 + 红/橙/黄/紫语义色)的子集" |
| 6 | max-w-[420px] 处数 |
两处均写 15 | 真实:任意元素 16 处 / 11 个文件;md: 前缀页面容器口径 7 处 grep 命中 / 5 个文件真代码(另 2 处是注释提及) |
V5 与 §3.3 均改为准确数字,并区分两种口径 |
| 7 | §3.2 vision_exams tag | 说 tag 也叫"视光档案" | 真实 tags=["vision_exams"](英文);"视光档案"只在 docstring 第 2 行 |
改为准确表述,注明 tag 是英文、"视光档案"字样出处是 docstring |
| 8 | §3.2 org_id 行号 | refractive_records.py:3 |
真实在第 5 行 | 改为 :5 |
| 9 | V12 sed 行号 | sed -n '7,8p' lib/api.ts |
第 7 行是空行,两行 import 真实在第 8-9 行 | 改为 sed -n '8,9p' |
| 10 | §3.3 MobileTabBar 路径 | 说 MobileTabBar.tsx 属于 web/app/mobile |
真实路径 web/components/mobile/MobileTabBar.tsx,只是被 web/app/mobile/layout.tsx import |
改为准确路径 + import 关系 |
| 11 | §5 分批合计与正文行数差 1672 未解释 | 批0-6 合计 6100 行,正文说 7772 行,无说明 | 差额部分可量化(lib 层 6 个文件 198 行零改写直接 import,不计入批次)、部分不可量化(手机端专属新增结构、平板专属 240/60px 菜单壳混在整体行数里拆不出来) | 在表格下方新增说明段,写清两个数字口径不同,不强行相减对账 |
| 12 | §4.2 色系决策措辞含糊 | "9-15 先拍板蓝系…随后长官交回选择权,改定绿系" | 表述基本对,但未点明"两次拍板都是长官本人、同一天" | 改为"同一天两次拍板,长官第一次拍板蓝系,长官自己推翻、交回选择权,最终定绿系" |
| 13 | 缺自检记录 | 无 | — | 新增本节(§八),逐条记账,防再犯 |
| 14 | §5 尾段 #11 修订本身引入一条假解释(第二轮自检发现并更正) | 第一轮修订把差额归因之一写成"§4.3 末行'左菜单 240/60px'混在 refractive-record-pane.tsx 整体 1037 行里,没有可单独 grep 出的行区间" |
这句解释没有跑过 grep——真跑 grep -n "240\|60px\|menu\|sidebar\|Menu\|Sidebar" refractive-record-pane.tsx 是 0 命中(1037 行核实无误,但不含 240/60px/菜单字样);240/60px 真实出处是 web/app/h5/tablet/pane/panes/menu-sidebar.tsx:85,是平板通用 pane 壳组件,跟 refractive-record-pane.tsx 是两个不同文件,根本不在本方案 27 文件/8548 行统计范围内 |
删掉这句假解释,§5 尾段改为如实说明差额不可量化的原因(手机端专属新增结构本就不存在于源文件),并在此记账:第一轮修订 #11 引入了一条未经 grep 验证的假解释,第二轮自检发现并更正 |
| 15 | 效果图帧②列表宽度实测方法论错误(9-15 二次自检发现并更正) | 效果图曾用截断手机号(···1299/···9977)拼进行2去做 360/375/390/414 四档宽度实测,测出"放得下"的结论 |
截断串比真实 11 位手机号短了 6-7 个字符,用它测宽度从一开始就量不出真实占用;9-15 用真实 11 位手机号数据结构(如 15612301299)在同一套四档场景重新真渲染,缺口 3~97px(414px 最短名缺 3px,360px 最长名缺 97px),四档全部放不下,与此前"放得下"的结论完全相反 |
手机号字段整段从列表行移除(D7),效果图 store-mobile-refractive-archive-mockup-*.html 帧②已改;本条记账证明"用截断/掩码数据测宽度"这条方法论本身不可靠,以后测宽度必须用未经处理的真实长度数据 |
| 16 | §〇「后端零改动」结论被新需求推翻(9-15 长官提「门店结论已读回执」后失效) | §〇 写"后端 18 个端点、鉴权体系、机构开关、数据字典四层全部可以白拿,零改动" | 长官 9-15 新增「门店结论已读回执」需求(D9)后,本期新增字段 store_conclusion_read_at(app/models/refractive_record.py)、一条 alembic migration(挂 demo_profile_data_fix1 head)、一个新 POST 端点(app/api/parent_patients.py)、门店端+家长端两处 DTO 出参改动(refractive_record_service.py:_record_dto + parent_patients.py:store_conclusion 字典)——不再是"后端零改动" |
§〇 已补更正说明(见上);新增 §九 详述后端改动范围与 file:line 依据;§五 补 批8;本行记账 |
长官原话:「门店推给家长的结论,现在推完像掉进黑洞。加个『已读/未读』,没读的店员去跟进。平板和手机端都要加。」
调研发现的关键事实(决策依据,9-15 grep 核实):
POST /api/h5/refractive-records/records/{id}/conclusion/push 对应的服务层函数
push_conclusion(app/services/refractive_record_service.py:2699-2715)实际只做两件事——
把 store_conclusion_text 快照成 store_conclusion_pushed_text、写 store_conclusion_pushed_at,
不调用任何通知渠道(微信模板消息 / 公众号 / 站内信全无):
$ grep -n "def push_conclusion" app/services/refractive_record_service.py
2699:async def push_conclusion(db: AsyncSession, org_id: int, record_id: UUID) -> dict[str, Any]:
$ grep -n "wechat\|notify\|模板消息" app/services/refractive_record_service.py
(0 命中)
# 对照组(证明查法有效——同一文件里确实能查到东西):
$ grep -n "pushed_at" app/services/refractive_record_service.py | wc -l
3
含义:家长完全是"主动进来才看得到",已读回执上线后大概率满屏未读——这是真实现状,如实写明,不粉饰。
仓库现状:全仓 0 个已读回执先例:
$ grep -rn "read_at\|is_read\|viewed_at" app/ web/ | wc -l
16
$ grep -rn "read_at\|is_read\|viewed_at" app/ web/ | grep -v "reviewed_at" | wc -l
0
# 对照组(证明这 16 处命中确实全部来自 reviewed_at 子串,不是查法漏了别的字段):
$ grep -rn "reviewed_at" app/ web/ | wc -l
16 # 与上面 16 完全相等 → 16 处命中全部是 reviewed_at,没有第二种已读字段
唯一沾边的 web/app/h5/components/voice-bar.tsx 未读红点
(:62 {!played && <span className="wx-voice__unread" aria-label="未读语音" />})
是纯前端 useState 变量 played(:23),刷新即丢,不落库、不跨会话持久,不是已读回执的先例。
原方案 §〇 写着「这活是纯前端,后端零改动」——加了已读回执之后不成立了。
§〇 已补更正说明(见文档开头),本期起本方案包含后端改动,范围见下。
字段:RefractiveRecord 新增 store_conclusion_read_at,类型 TIMESTAMP(timezone=True)、nullable=True,
语义「NULL = 未读」。理由:跟现有批5三个字段同款风格 ——
app/models/refractive_record.py:166-174 现有 store_conclusion_text(:166-168)、
store_conclusion_pushed_text(:169-171)、store_conclusion_pushed_at(:172-174)
全部走"有时间戳=发生过"的语义,不加 is_read 布尔列(多一列冗余、破坏风格一致)。
迁移:down_revision 挂在当前 refractive_records 所在分支的尾节点上。
$ alembic heads
34ec94466f0c (head)
demo_profile_data_fix1 (head)
phase79_001 (head)
⚠️ 仓库当前是三头分叉状态。已顺着 down_revision 链条逐个 grep/sed 核实(不是猜测):
batch5_ai_conclusion(新增 store_conclusion_* 三列的那次迁移,down_revision="seat_remark_name")
→ org_hero_image(down_revision="batch5_ai_conclusion",其自身注释明确记载
「当前 test 队尾实为两个未合并的 head:34ec94466f0c…与 batch5_ai_conclusion…
batch5_ai_conclusion 在 main 上存在,34ec94466f0c 不在 main 上(尚未晋级)」)
→ org_hero_image_toggle(down_revision="org_hero_image")
→ patient_is_sample_flag(down_revision="org_hero_image_toggle")
→ demo_profile_seed_data(down_revision="patient_is_sample_flag")
→ demo_profile_data_fix1(down_revision="demo_profile_seed_data",head)
即三个 head 里,demo_profile_data_fix1 是唯一挂在"含 store_conclusion_* 列这条分支"尾端的 head;
另两个 head 都不是同一条线——34ec94466f0c 是 org_hero_image.py 注释里明确记载的"未晋级 main 的孤头";
phase79_001 的 down_revision="phase63_001" 走的是 refractive_records 表更早创建时的另一条平行分支
(phase63_001_refractive_record.py,建表迁移本身,不含批5字段)。
新迁移应挂 down_revision = "demo_profile_data_fix1"。
POST /api/parent/patients/{patient_id}/records/{record_id}/conclusion/mark-readapp/api/parent_patients.py 路由前缀 :57 router = APIRouter(prefix="/api/parent/patients");:913 @router.post("/{patient_id}/records", status_code=201)、:1212 @router.post("/{patient_id}/records/from-report")),Depends(_require_parent)。⚠️ _require_parent 全仓有两处同名定义(app/api/parent_patients.py:65 与
app/api/parent_ocr.py:54,各自文件内独立定义、不是同一个函数、不共享)。
新端点放进 parent_patients.py,必须用该文件自己的 :65 版本(与同文件里其它家长端点保持依赖一致,
不跨文件 import 另一份)。
幂等写法 UPDATE ... SET store_conclusion_read_at = now() WHERE store_conclusion_read_at IS NULL
(只记第一次已读时间,后续重复调用不覆盖)
my_child_record_detail(GET /{patient_id}/records/{record_id},定义于app/api/parent_patients.py:607-608),家长端 store_conclusion 字典正是它在 :742-753 拼出来、:809 塞进响应体的。理由:GET 不应有副作用(缓存/预取/重试会误标)、并发会冲掉首次已读时间、$ sed -n '608,889p' app/api/parent_patients.py | grep -n "db.commit"
(0 命中——已验证:my_child_record_detail 当前确实是纯读、函数体内无任何 db.commit,
加已读回执时不应打破这个特性)
_record_dto(app/services/refractive_record_service.py:871-905)——现有字典末三个键就是store_conclusion_text/store_conclusion_pushed_text/store_conclusion_pushed_at(:903-905),store_conclusion_read_at家长端 store_conclusion 字典(app/api/parent_patients.py:750-753 字典字面量,:809 塞进响应体)
加 read_at
两端展示:
frame4-record-view.tsx:1446-1448 现有「已推送 · 时间」那行{record.store_conclusion_pushed_at ? (... 已推送 · {...} ...) : ...})之后追加| 文件 | 改什么 | 依据 file:line |
|---|---|---|
app/models/refractive_record.py |
新增 store_conclusion_read_at 列 |
紧挨现有 :166-174 三个批5字段之后追加,同款风格 |
migrations/versions/(新文件) |
新迁移,down_revision="demo_profile_data_fix1" |
见 §9.3 迁移小节 head 链条核实过程(batch5_ai_conclusion→org_hero_image→org_hero_image_toggle→patient_is_sample_flag→demo_profile_seed_data→demo_profile_data_fix1) |
app/api/parent_patients.py |
新增 POST /{patient_id}/records/{record_id}/conclusion/mark-read 端点 |
路径风格参照 :913/:1212;鉴权用本文件 :65 的 _require_parent(不用 parent_ocr.py:54 那份) |
app/api/parent_patients.py |
store_conclusion 字典加 read_at |
:750-753(字典字面量)、:809(塞进响应体) |
app/services/refractive_record_service.py |
_record_dto 加 store_conclusion_read_at |
:871-905(函数体),紧跟 :903-905 现有三字段之后 |
web/app/h5/tablet/pane/panes/refractive-record/frame4-record-view.tsx |
已推送行后加已读状态展示 | :1446-1448 |
| 手机端记录查看页(新建,对应平板"帧4",批4范围) | 同款已读状态展示 | 待实施时确认(页面尚未创建,不编造行号) |
已在 §五 工作量与分批表补一行「批8:已读回执(D9 新增需求·字段+迁移+端点+两端 DTO 出参+两端 UI 展示,详见 §九)|~150」。
依赖关系:字段/迁移/新端点/两端 DTO 出参/平板端 frame4 展示可独立先行,不阻塞其他批次;
手机端记录查看页的已读展示依赖批4(该页面本身要先存在)。