交付出件箱

← 返回出件箱

门店手机端「视光档案」实施方案 v0.1


〇、一句话结论

这活是纯前端。后端 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-D9 · 不再重议)

# 议题 拍板结果 对方案的约束
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) 门店能看见谁没读、自己跟进;微信推送另立项,不拖本期

二、物理 verify 证据表(13 维 · 每条带命令与原始输出 · 0 命中带对照组)

铁律:下任何结论前必须物理 grep,并带对照组证明查法有效。
下表中 4 条是证伪/纠偏 —— 说明这些结论如果凭印象写,方案就会派单出错。

V1 手机端功能入口结构 —— ❌ 证伪(方向性错误)

原假设:手机端有个类似平板 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>)} 块。

V2 家长端手机表单能不能直接拿来用 —— ✅ 证实(不能)

$ 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)。

V3 横屏布局硬伤精确计数 —— ✅ 证实

$ 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 处。

V4 dead code 判定 —— ✅ 证实(省掉 776 行)

$ 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 结论本身没错,错的是原证据命令贴的输出)。

V5 手机端宽度约定 —— ⚠️ 部分纠偏("360 设计基准"查无实据)

$ 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"(无出处)。

V6 后端可否零改动 —— ⚠️ 结论成立但措辞纠偏

$ 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"。

V7 手机端可复用组件真实签名 —— ✅ 证实(并补全)

// 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。

V8 机构开关在手机端是否已下发 —— ❌ 证伪(不是新增工作量)

$ 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 即可,零新增。

V9 用户可见文案盘点 —— ✅ 证实 + V11 纠正范围

$ 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 处。

V11 settlement 两处开关文案 —— ⚠️ 纠正一次范围性假 0

子代理只查了 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 处。

V12 真实物理障碍 —— 🔴 新发现(必须先拆)

平板端 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「平板端不动」的唯一例外,需长官知悉。

V13 双眼排布定案依据 —— ✅ 家长端已有上线解法(⚠️ 9-15 第二轮自检:算式漏了一层容器 padding,已重算,结论有变)

$ 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;不预设具体数值,要求报实测值)。

V10 附带纠错 —— ❌ 一条 find 假象

$ 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 这类深层路由全滤掉了。报数必须写清查询范围。


三、现状全貌

3.1 平板端视光档案(要照搬的源头)

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 行

3.2 后端(零改动)

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)
是另一套医生端/飞轮标注功能,与本功能无关。

3.3 手机端(落地目标)


四、适配方案

4.1 路由与入口

新建  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 报告解读是核心功能不能藏在横滑里。历史解读紧随其后。」
视光档案同样是不能被横滑藏起来的核心功能,故沿用同一条判断逻辑排到最前。

4.2 令牌口径(🔴 禁止自造色值 · D5 拍板走绿系)

决策经过(如实记录,便于日后复盘):同一天(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 注释写着
「平板门店端『视光档案』是蓝色主题」,不传就会继承平板蓝。

⚠️ 绿系的已知代价与补法(尚未落进 mockup v1,留给 v2)

绿色在本功能里同时是语义色,底色变绿会让这几处信号变钝:

语义绿 现状 补法(待 v2 落地)
「该记录已完成」状态条 #e6f6ec 底 #2f855a 字(frame4-record-view.tsx:1214) 改用蓝做强对比,把蓝留给状态而不是底色
AI 回填态输入框 #eaf7f0 底 #157c67 字(exam/new/page.tsx:40-41) 同上,改蓝描边/蓝字
4L 方案 L3 Long 绿卡 #2f855a+#e6f6ec 四卡各自带底色;绿底上是否仍可分辨 —— 必须真渲染看过再定,不许推断

mockup v1 用的仍是「全绿」口径,上述补法未实施。 看图时这三处的信号强弱请按「待优化」看待。

4.3 逐帧改造清单

帧 平板端布局假设 手机端改法
帧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(家长端惯例)

4.4 复用 vs 改写(lib 层)

文件 行数 判定 证据
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/,但该目录不存在,本次不新建顶层共享目录」
→ 本方案沿用同一做法,不新建共享目录(除非长官另有指示)。

4.5 文案改动(全仓仅 1 处)

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 行的来源:

🔴 记账(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)

七、验收标准(无证据 = 没做)

  1. 新建的手机端目录下 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 渲染),不在迁移范围内,混进对照组会失真。
  2. 真渲染跑 getBoundingClientRect,报出 OD/OS 三列实测宽度(不预设具体数值——V13 的 134/111.5/104px
    是"等价复现渲染"的推算值,不是本功能真页面的实测值,拿它当验收基准会在真机量出别的数字时被误判成
    "实现有问题"),并与平板写死的 108px 做对比说明(尤其关注窄屏是否比 108px 窄,见 V13)
  3. 溢出自查:判据 document.documentElement.scrollWidth > window.innerWidth(🔴 不得用"子元素 vs 父元素"
    口径——父元素被撑大时该判据会漏报,出处 docs/superpowers/plans/specs/ui-design-spec-dev2-20260824.md:264-265),
    命中数 = 0,报出手机帧宽度集合
  4. 色值自查:新建目录 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 命中 > 0
  5. 18 个后端端点在手机端真打通(去 test 环境,非云机)
  6. 入口双门:flag 关 / 无 seat token / 家长坐席 三种情况下入口均不显示
  7. DOM 闭合标签差值与改动前一致
  8. 平板端零回归:git diff --stat 对 web/app/h5/tablet/** 的改动只允许 seat-auth-recovery.ts 一处

附:本次调研的查证边界(如实声明)


八、v0.1 自检发现并修订的问题(9-15)

本节是防再犯的记账钩子。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 长官提)

9.1 需求与背景

长官原话:「门店推给家长的结论,现在推完像掉进黑洞。加个『已读/未读』,没读的店员去跟进。平板和手机端都要加。」

调研发现的关键事实(决策依据,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),刷新即丢,不落库、不跨会话持久,不是已读回执的先例。

9.2 🔴 这条需求改变了本期性质

原方案 §〇 写着「这活是纯前端,后端零改动」——加了已读回执之后不成立了。
§〇 已补更正说明(见文档开头),本期起本方案包含后端改动,范围见下。

9.3 技术方案(只写方案,不写代码)

$ 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"。

⚠️ _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
(只记第一次已读时间,后续重复调用不覆盖)

$ sed -n '608,889p' app/api/parent_patients.py | grep -n "db.commit" (0 命中——已验证:my_child_record_detail 当前确实是纯读、函数体内无任何 db.commit, 加已读回执时不应打破这个特性)

9.4 要动的文件清单表

文件 改什么 依据 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范围) 同款已读状态展示 待实施时确认(页面尚未创建,不编造行号)

9.5 分批建议

已在 §五 工作量与分批表补一行「批8:已读回执(D9 新增需求·字段+迁移+端点+两端 DTO 出参+两端 UI 展示,详见 §九)|~150」。

依赖关系:字段/迁移/新端点/两端 DTO 出参/平板端 frame4 展示可独立先行,不阻塞其他批次;
手机端记录查看页的已读展示依赖批4(该页面本身要先存在)。