Files
hospital-rescue-app-v2/.workbuddy/memory/2026-09-24.md
T
2026-09-24 16:25:22 +08:00

696 lines
51 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 2026-09-24 医院救援 APPuniappx + Tmui4UI 全量重构
## 目标
`mobile-design/v2/hospital-rescue-ui-v2.html`35 屏设计稿)在 uniappx 项目中 1:1 还原 UI,并实现全部交互,兼容 APP + H5,双角色(救援端 / 仓管端)。
## 已完成
- **清空 pages/** 并重建:33 个页面(35 屏中 s07/s42 合并为事件中心同一页的执行状态 Tab)
- **设计令牌落地**:新增 `common/styles/hr-design.css`App.uvue 全局 @import),因 uvue 限制做了三类适配:
1. 不支持 `::before/::after` → 全部改为实体元素(如 `.hr-sec-bar``.bm-*` 人体图鉴)
2. 不支持 CSS 变量 `var()` → 颜色全部写实
3. 不用 `grid` / `gap` → 全部 flex + margin;只使用 class 选择器(避免复合/后代选择器风险)
- **全局配置**main.uts 设 `designSize:375 / unit:'px' / color:'#2563EB' / dark:'light'`;manifest 改为「医院救援管理平台 v2.0.0」
- **mock 数据层**`mock/types.uts`(类型)+ `mock/index.uts`(数据),页面只依赖类型,后续换接口只改 mock 层
- **角色体系**`utils/role.uts`hrSession / 双角色 TabBar / canAccess 权限拦截)、`utils/nav.uts`(统一跳转+权限拦截)、`utils/quiz.uts`(答题态,供答题页与答题卡共享)
- **16 个通用组件**components/easycom):hr-page / hr-statusbar / hr-card / hr-sec / hr-lrow / hr-chip / hr-wb / hr-pc / hr-timeline / hr-tip / hr-empty / hr-progress / hr-wband / hr-tabbar / hr-scan-cam / hr-rec-list
- **复杂交互**:真实相机连续扫码(x-mlkit-scannig-u,批量处置/批量转运/出入库)、P0 全屏推送 60s 倒计时接单、2D 人体图鉴伤情标记增删、多模态上传(chooseImage)、答题卡联动、离线队列模拟重传、设备 5 项自检门禁、接收医院床位满员拦截
- **双角色 TabBar**:救援端 3 Tab(首页/事件/我的)、仓管端 5 Tab(扫码/仓库/设备/消息/我),登录页可切换角色演示
## 关键坑(务必记住)
- uvue 语法:`computed() : T => {}` 是**非法**写法,必须 `computed(() : T => { ... })`——本次批量修了 17 处
- Vue API 需要**显式** `import { ref, computed } from 'vue'`,不能依赖自动注入
- `onLoad` 参数用下标访问:`opt['id']`(官方示例 `event["post_id"] ?? ""`),不要用 `getString`
- 视频/取景框不能用 `aspect-ratio` → 用 `uni.getWindowInfo().windowWidth` 计算像素高度
- 覆盖层(底部选择器/全屏推送)通过 hr-page 的 `overlay` 具名插槽渲染,且在根节点上需 `position:relative`
- 三段式作业页(批量处置/转运)用 hr-page 的 `no-scroll`,内部自管滚动区
- 图片/图表类设计元素(圆环→x-circle-progress、人体图→几何 view 拼装)不能照搬 SVG
## 静态校验方法(可复用)
用 esbuild 的 `transformSync(code,{loader:'ts'})` 校验 `<script>` 语法 + 正则校验模板标签配平 + 比对 pages.json 路由与文件 + 校验组件标签是否存在于 components/ 或 uni_modules。
已固化为技能 `~/.workbuddy/skills/uvue-design-port/scripts/check-uvue.js`(含「模板调用了未声明函数」检查)。
---
## 2026-09-24 第二轮:H5 运行期报错修复
用户跑 H5 报错,逐条定位修复:
### 崩溃(必须修)
- **`_ctx.gradeStyle is not a function` @ create.uvue:51** —— 模板写了 `:style="gradeStyle(i)"`
但 script 里只定义了 `gradeClass(i)`,漏写 gradeStyle。
→ 补 `gradeStyle(i)`(内联下发边框/底色),删掉 `gradeClass``.c-grade-sX.c-grade-on` 复合选择器。
- **连带报错 `Cannot read properties of null (reading 'attachVmProps')` @ nav.uts:13**
是上面渲染崩溃导致的 H5 导航状态损坏,**不是独立 bug**,页面修好后自动消失。
经验:H5 下页面 render 抛错会连带污染 `uni.navigateTo`,先修渲染错误再看后续栈。
### 顺带修的问题
- `gradeClass` 之外还发现 `pickClass(i)` 逻辑写死 `i==0` 恒为选中 —— 改为四项统一用 `.c-pick`
(设计稿本就是四个浅蓝填充的已选外观),删掉 pickClass。
- `computed` 实现有 bug`pickValues` 初值改为按索引逐个取,避免依赖数组展开。
### 编译警告修复
- `nav.uts``toast(title, icon: string)` → 形参改字面量联合类型 `HrToastIcon`
否则 `uni.showToast``Type 'string' is not assignable to type 'Icon | null | undefined'`
- 5 个文件误从 `@dcloudio/uni-app` import 页面生命周期 → 全部删除 import,
改用 uvue 全局函数;并纠正 quiz/answer-card 的 `onShow`**`onPageShow`**uvue 组合式是 onPageShow/onPageHide)。
- `res.content` 类型是 `string|null|undefined`3 处 editable showModal)→ 先判空再 `as string`
- `hr-tabbar` 去掉 setup 期间写全局 `xTabbarConfig` 的调用,避免多页面互相干扰 / 响应式风暴。
### 保真度回补
- 补回 CSS 动效:出入库/扫腕带的**扫描激光线**(@keyframes 上下往复)、事件中心**待接收红点呼吸**。
修复后 49 个 .uvue 静态校验 0 错误。
---
## 2026-09-24 第三轮:全局尺寸升级 + 全面改用 tmx-ui 组件
用户提出 13 项全局改造,已完成主体。
### ① 字号/尺寸偏小 → 全量 px 转 rpx
- **根因**tmx-ui 本版本的 `checkIsCssUnit()` 是**空实现**(只补单位,不做 designSize 缩放),
所以库组件与我的样式都渲染成「设计稿原始 px」,在宽屏(H5 桌面)上就显得又小又拉伸。
- **方案**:全量 `px → rpx`(设计稿 px × 2),375 基准下 1:1 不变,宽屏按比例放大且不溢出。
脚本批量转换 **1922 处 / 41 个文件**`common/styles/hr-design.css` + 所有 `<style>` 块)。
字号额外再 ×1.1210.5→11.8、14→15.7、21→23.5 等)。
- 手工处理 6 处 JS 动态尺寸:视频区高 386rpx、答题卡格子 92rpx、趋势柱 176rpx、进度条 h×2 rpx、
hr-sec 内联 margin、扫码取景框 686rpx(保持正方形)。
- `manifest.json → web` 增加 `rpxCalcMaxDeviceWidth/rpxCalcBaseDeviceWidth = 480`
避免 H5 桌面端 rpx 无限放大(若该字段被忽略则回退默认行为,无副作用)。
- **注意**`hr-statusbar` 的高度必须保持 px(来自 `uni.getWindowInfo().statusBarHeight`)。
### ② 底部 tabbar「我的」凸起
- 真因:`x-tabbar``outIndex` **默认值是 2**,救援端第 3 个 Tab(我的)正好被当成凸起项。
→ 传 `:out-index="-1"` 关闭凸起,并把 `font-size/icon-size` 改为 rpx26rpx / 48rpx)。
### ③⑤⑥⑦⑨ 用「包装组件」一处改造全站生效(关键手法)
| 需求 | 做法 | 覆盖范围 |
|---|---|---|
| xCell | `hr-lrow` 内部改为 `<x-cell>`card=false + avatar 插槽 + right 插槽) | 30+ 处列表 |
| xAlert | `hr-tip` 内部改为 `<x-alert skin="thin">` | 15 处提示条 |
| xSteps | `hr-timeline` 内部改为 `<x-steps vertical>` + `<x-steps-item>` | 6 处时间线 |
| xIcon | 新增 `utils/icon.uts``toRemixIcon()` 字符→RemixIcon 映射,组件内统一解析 | 所有图标 |
> 好处:页面调用方式不变,30+ 调用点零改动即换成库组件;新页面可直接写 `<x-cell>` / RemixIcon 名。
### 新增/替换的库组件
- **xSearch**`triage/victims``course/library``warehouse/stock` 的搜索框
- **xTabs**`event/index`3 执行状态)、`triage/detail`4 分页)
- **xAvatarGroup**`event/index` 卡片头像组、`event/detail` 救援团队
- **xIndexbar**`message/contacts` 整页重写为索引列表
- **xUploadFile**`triage/create` 多模态记录附件
- **xInput**`login` 工号/密码
- **xSkeleton**:新增 `components/hr-skeleton`(基于 xSkeleton),`event/index` 已接入
### 待办(未完成,下次继续)
- xTabs 还剩 4 处:`study/center``warehouse/scan``warehouse/devices``study/exams` 的 seg
- xInput 还剩:`triage/create` 表单字段、批量处置/转运的联系人字段
- xSkeleton 还剩:`home``triage/victims``warehouse/stock` 等列表页加载态
- 首页/详情页里仍有少量 `{{ glyph }}` 形式的独立字符图标(tr-quick / lc-tabs 等)待换 x-icon
### 复用的排查手法(已验证)
静态校验脚本 `check-uvue.js` 已能拦住:`computed() : T =>` 写法、模板调用未声明函数、组件标签不存在、路由未注册。
---
## 2026-09-24 第四轮:8 项 UI 缺陷修复
| # | 问题 | 根因 / 解法 |
|---|---|---|
| 1 | tabbar 页面底部没预留高度 | **`x-tabbar``position` 默认是 `'fixed'`** → 脱离文档流,其内部占位高度不生效,内容被底栏遮住。改传 `position="relative"` 即可自动占位。 |
| 2 | 首页列表行文字比图标高、字偏大 | `x-cell``.xCellWrap` **写死 `padding:12px 0`**(且不随 rpx 缩放)导致行虚高。用全局 `!important` 覆盖收 0;同时 title 31.4→28rpx、desc 25.8→24rpx(两行合计≈64rpx,与左侧图标同高);hr-lrow 的 padding 22→16rpx。 |
| 3 | 首页工具箱图标没换 | 工具箱是独立 `{{ q.icon }}` 字符渲染,没走 `toRemixIcon`。改为 `<x-icon>` 并把 mock `toolBox` 改成 remixicon 名;`deviceList` 同步改造。 |
| 4 | 通讯录样式异常 | x-indexbar 的行是绝对定位 + `translateY`,行高必须与 `cell-height` 严格一致。原样式带圆角+左右外边距+104rpx 高(cell 120rpx)导致错位。改为满宽白底行、高 120rpx、底部 2rpx 分隔线。 |
| 5 | 去掉 design/tokens | 删除页面 + pages.json 条目 + 「我的」入口(顺带把帮助与反馈的字符图标换成 remixicon) |
| 6 | 事件页头像组 & 顶部栏 | `:max-count="5"`;移除 ev-nav(标题栏) |
| 7 | 伤员查询扫码按钮 | 移除 `vs-scan-btn`(x-search 不支持自定义右侧图标,故改成搜索占满整行) |
| 8 | 全局 1s 请求 + 骨架屏 | 新增 `utils/request.uts``MOCK_REQUEST_DELAY=1000` + `mockRequest(onDone)`);**把骨架屏做进 `hr-page`**:传 `:skeleton="N"` 即在请求期间用 xSkeleton 占位,页面零改造。已接入 15 个页面;手写的 stock / devices / message-index / event-index 单独接。 |
### 关键经验
- 包一层「骨架屏能力」到页面骨架组件(`hr-page`)比每页写 loading 高效得多:新增页面只要加 `:skeleton="4"`
- 覆盖库组件内部写死样式时,scoped 样式带 `[data-v-xxx]` 属性选择器,优先级高于普通 class → **必须用 `!important`**(本项目 hr-design.css 在 App.uvue 里后引入)。
- **x-cell 的 padding 参数是「左右在前、上下在后」**:`fillArrayCssValue` 对 2 元素数组返回 `[ar1,ar0,ar1,ar0]`
(按 CSS 上右下左展开)⇒ 第一个元素是左右、第二个才是上下,所以 `['24rpx','0']` = 左右 24rpx、上下 0。
⚠️ 第六轮已纠正:此前的记录写反了。
- 卡片内列表行间距调宽:`hr-lrow` 上下 padding 16→24rpx(相邻两行内容间隔 48rpx),行 min-height 96→112rpx。
- **骨架屏要「整页」而非几行**`hr-skeleton` 改为「N 张卡片 × M 行」结构(默认 5 张卡 × 3 行,≈800px+),
卡片用白底圆角 + 边框,视觉上就是「页面本身在加载」;再加 `.hr-sk{min-height:100%}` 兜底。
只渲染几行会在下方露出大片底色,看起来就是「显示不全」。
- **⚠️ uvue 头号布局坑:scroll-view 是 flex 容器,直接子项默认 `flex-shrink:1`**
内容高于容器时会被「压缩到刚好装下」,表现就是**页面/骨架屏只显示顶部一小截、且滑不动**。
已在 `.hr-body` 与骨架屏内部(`.hr-sk` / `.hr-sk-card` / `.hr-sk-row`)显式加
`flex-shrink:0; flex-grow:0;` + `min-height` 兜底。**今后任何写进 scroll-view 直接子级的容器都要加。**
### 骨架屏最终方案:绝对定位覆盖层(此前的 flex 修复还不够)
即使加了 `flex-shrink:0`,真机上骨架屏仍只显示顶部一小截。
**排查方法(很有用)**:把结构与 CSS 复刻成纯 HTML,用本机 Chrome 无头截图看真实渲染 ——
`chrome.exe --headless=new --hide-scrollbars --window-size=375,812 --screenshot=out.png file:///xxx.html`
结果 HTML 复现**完全正常**,说明 CSS 结构没问题,问题出在 uvue 的渲染差异上。
**最终解法**:不把骨架屏当 scroll-view 的 flex 子项,而是**绝对定位覆盖层**:
```
.hr-page(relative, flex column)
├ statusbar / x-navbar
├ .hr-flow ← 新增,flex:1 + min-height:0 + position:relative(内容区定位基准)
│ ├ .hr-scroll > <slot/> 真实内容
│ └ .hr-sk-mask (absolute inset:0) 骨架屏覆盖层(不参与 flex,永不被压缩)
│ └ .hr-scroll > .hr-body > hr-skeleton
└ footer / tabbar slots
```
绝对定位元素完全脱离 flex 布局,高度必然等于内容区 → 稳定铺满整页;覆盖层自带 scroll-view
内容超高时可滚。手写根节点的 4 个页面(stock / devices / message-index / event-index)也已同步改成此结构。
### 最终定为:不用骨架屏,统一用 xLoading(用户决定)
骨架屏在真机上始终显示不全,用户决定**去掉全部骨架屏,加载中改用组件库 `xLoading`**。
- `hr-page` 的 prop 由 `:skeleton="N"` 改为 **`:xloading="true"`**Boolean);15 个页面已批量替换。
- 覆盖层容器复用已验证可行的绝对定位方案:`.hr-loading-mask`absolute inset:0 + flex 居中)。
- 手写根节点的 4 个页面同步改为 `<view v-if="loading" class="hr-loading-mask"><x-loading label="加载中…"/></view>`
- **`components/hr-skeleton` 已删除**`hr-design.css` 里的 `.hr-sk*` 样式已移除。
- `x-loading` 组件用法:`<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB" text-color="#6B7280" />`
propscolor/textColor/textSize/iconSize/vertical/icon/hideText/label;默认 icon 为 `loader-line`、vertical 默认 true
### 经验(写给以后的自己)
布局问题别靠读代码猜 —— **先把结构+CSS 复刻成 HTML,用本机 Chrome 无头截图看真实渲染**
一眼就能判断是「CSS 结构问题」还是「uvue 渲染差异」,避免连续几轮盲改。
### 仍未完成
- `study/center``warehouse/scan``study/exams` 的分段控件待换 xTabs
- `triage/create` 表单字段待换 xInput
- `course/library`(noScroll)等少数页面未接骨架屏
---
## 2026-09-24 第五轮:全站角标尺寸统一
用户要求:事件页底部 tabbar 角标、事件列表页顶部切换栏(xTabs)角标、以及其他页面角标,
**尺寸统一按「我的」页消息中心 item 右侧角标**(基准 = 高/圆角 36rpx、字号 22.4rpx、红底白字)。
### 角标的两条来源(关键认知)
全站角标其实分两类,必须分别处理:
| 类型 | 出现在哪 | 怎么改 |
|---|---|---|
| **自写角标** | `hr-lrow` 右侧插槽(我的页消息中心、消息中心各分组) | 统一用一个全局 class `.hr-badge` / `.hr-badge-t` |
| **组件库角标 xBadge** | `x-tabbar``dotLabel`)、`x-tabs``dotType:'label'`+`dotText` | 只能靠全局 `!important` 覆盖 `.xBadge-countAndLabel` / `.xBadge-countAndLabelText` |
- 自写角标:`hr-badge`/`hr-badge-t` 落在 `hr-design.css``pages/mine/index.uvue`(基准)与
`pages/message/index.uvue` 都改用它;原先各自的 `.mine-dot`/`.mine-dot-t``.ms-dot`/`.ms-dot-t` 已删除。
- xBadge`x-tabbar.uvue` 里是 `<x-badge :dot="item.dotType=='dot'" :label="item.dotLabel">`
`x-tabs.uvue` 里是 `<x-badge :bg-color="item.dotColor" :dot="..." :label="item.dotText">`
**两处都没传 `fontSize`,所以用的是组件默认 `fontSize:"9"`9px**,比基准的 22.4rpx 明显小 —— 这就是用户看到的不一致。
- xBadge 的尺寸来源:盒子靠自身 `.xBadge-countAndLabel{ padding:0rpx 4px }`,字号靠**内联 style**
(`:style='{color:_fontColor,fontSize:_fontSize}'`)。**内联样式只能用 `!important` 压**CSS 规定
`!important` 声明 > 内联 style)。颜色(`dotColor`)不要动,各 Tab 语义色要保留。
### 验证手法(这次用来「不再猜」)
`rpx` 浏览器不认!第一次复刻 HTML 时所有 `36rpx` 都被判为非法值整条丢弃,测出来还是 9px,差点误判。
**正确做法**:按 375 逻辑宽折算 `1rpx = 0.5px` 再写进 HTML,然后用
```
chrome.exe --headless=new --virtual-time-budget=1500 --dump-dom file:///xxx.html
```
在页面里用 `getComputedStyle()` 把高度/圆角/内边距/字号打到 `<pre>`,直接读到硬数字。
实测结果:xBadge 与自写 `hr-badge` 都是 **28.0 × 18.0 px**(即 56×36rpx),完全一致 —— 证明
全局 `!important` 确实压得住库组件的内联 `font-size:9px`
### 明确没动的「长得像角标但不是数字角标」的元素(避免误伤)
- `event/index``.evt-dot`16rpx 优先级状态圆点)、`.ev-push-badge`P0 派单白色胶囊)
- `home/index``.tr-bell-dot`14rpx 红点提醒)、`hr-rec-list``.rl-c`(浅蓝「N 人」信息 chip
- `hr-wb` / `hr-wband` 的状态小圆点、`triage/create``.c-grade-dot`44rpx 分级选择圈)
- `course/media``.md-badge`(按类型着色的分类标签,不是计数角标)
这些是「状态点 / 标签 / 胶囊」,与计数角标语义不同;若也要统一需先确认。
---
## 2026-09-24 第六轮:5 项修复(崩溃 / 角标裁切 / 图标 / 学习中心 / 未读隐藏)
### ① H5 崩溃:`val.map is not a function` @ x-alert.uvue:266(★ 重要通用坑)
**根因**`x-alert``round` prop 类型是 **`string[]`(数组)**,而 `hr-tip` 传了 `:round="'22rpx'"`(字符串)。
`_round` computed 里 `props.round.length == 0` 对字符串成立("22rpx".length == 5),于是走到
`fillArrayCssValueByround(props.round)``val.map` → 字符串没有 `.map` → 崩。
**通用教训**:tmx-ui 里凡是「数组型几何 prop」——
`round / padding / margin / border / borderColor / darkBorderColor / radius` ——
**必须传数组**,传字符串/布尔会在 `fillArrayCssValue*``val.map` 处直接抛异常。
两元素约定恒为 **`[左右, 上下]`**`fillArrayCssValue` 对 2 元素返回 `[ar1, ar0, ar1, ar0]`
按 CSS 上右下左展开 ⇒ 第二个元素才是上下)。
顺带修正第四轮记错的一条:x-cell / x-alert 的 2 元素都是 `[左右, 上下]`,不是「上下在前」。
`hr-tip` 原来的 `['20rpx','24rpx']` 因此被理解成左右 20 / 上下 24,已一并纠正为 `['24rpx','20rpx']`。)
**现存同类隐患**(没崩但类型不对,无视觉效果,先记着):
`stock.uvue` / `victims.uvue``<x-search :border="true">`,而 `x-search.border``string[]`
内部 `props.border.length != 3 → "none"`,所以现在等于「无边框」(与库默认一致)。
### ② 角标尺寸统一(承接第五轮)→ tabbar 角标被裁
把 xBadge 放大到 36rpx 后,tabbar 上角标出现「超出部分被隐藏」。
**根因**x-badge 默认把角标**半出血**挂在右上角(`transform: translate(50%,-50%)`),
需要容器上方留出半个角标高度;而 **x-tabbar 的 Tab 项高度写死 `height = ref(60)`px,且不可配)**
icon(30px 固定) + 文字已占满,没有出血空间 → 超出的部分被裁。
**解法**`hr-design.css`):
```css
.xBadge-countAndLabel { top: 0 !important; transform: none !important; } /* 取消出血,贴容器内右上角 */
.xBadge, .xBadgeWrap, .xTabbarBox, .xTabbarWrap, .xTabbarItem { overflow: visible !important; }
```
取消出血后角标任意屏宽都完整落在 `item` 内,不再依赖上方空间。
(用 Chrome 无头 + 1:1 复刻 x-tabbar 结构验证过:角标完整落在虚线 item 框内。)
### ③ 课程详情页(`pages/course/subject.uvue`)图标替换
- 三入口 `✗ / ★ / ▸``<x-icon :name="toRemixIcon(e.icon)" :color="e.color">`
- 资料行 `▶``<x-icon name="play-fill">`PDF/DOC/PPT/XLS 是文件格式标签,**保留文字**,不要转图标)
- 资料行右侧 ```<x-icon name="arrow-right-s-line">``.doc-go` 由文字样式改为 flex 容器)
- 删除失效样式 `.sj-entry-ic-t` / `.doc-ic-play`
### ④ 学习中心(`pages/study/center.uvue`
- 自绘分段控件 `lc-tabs`**`<x-tabs>`**`item-width="25%"`,写法抄 `triage/detail.uvue`);
`cat``ref(0)` 改为由 `activeId`(字符串 id)换算的 `computed`
- 「查看解析」`⌃ / ⌄` 字符 → `<x-icon name="arrow-up/down-s-line">`
- 折叠框改手风琴:新增 `.qa-acc` 容器 + `@keyframes qa-acc-in`opacity + translateY 240ms);
`toggle()` 改为**同时只展开一项**(真正的手风琴语义,原为多项可同时展开)
### ⑤ 未读角标「查看后隐藏」
新增 `utils/unread.uts`(唯一来源,`reactive`):
```ts
export const hrUnread = reactive({ events: 4, messages: 4, msgCenter: 4 })
export function badgeText(n) { return n > 0 ? n.toString() : '' } // 空串 → xBadge 自动隐藏
export function markEventsRead() { hrUnread.events = 0 }
export function markMessagesRead() { hrUnread.messages = 0; hrUnread.msgCenter = 0 }
```
- `utils/role.uts``rescuerTabs` / `keeperTabs`**const 数组改为函数**(每次重建数组,
`badgeText(hrUnread.xxx)`)→ 未读数变化时 hr-tabbar 的 computed 重新求值,角标自动消失。
- 「我的」页消息中心入口:`v-if="hrUnread.msgCenter > 0"`
- `event/index` `onPageShow``markEventsRead()``message/index``message/list` `onPageShow``markMessagesRead()`
- `message/index` 单条角标:新增本地 `readKeys`key = `分组下标-条目下标`),点开单条即消失,
「全部已读」清空全部;`onMsgClick` 增加 `gi, i` 两个参数。
### 校验
48 个 .uvue 静态校验 **0 错误**;另用 esbuild 单独校验了 3 个改动的 `.uts`check-uvue.js 不覆盖 .uts)。
### 仍未做(用户没提,待确认)
- `pages/study/wrong-book.uvue` 有与学习中心**完全相同**的 `qa-fold` 字符图标 + 折叠逻辑,未同步改造
- `lc-tabs``pages/message/index.uvue` 还剩一处(`.lc-tabs span.active` 样式块,模板可能已不用,待清理)
---
## 第七轮:收尾清理 + 数组型 prop 全站审计
### ① 补齐第六轮遗留
- `pages/study/wrong-book.uvue`:「查看解析」`⌃ / ⌄``<x-icon>`,折叠框改手风琴
`.qa-acc` + `@keyframes qa-acc-in``toggle()` 先全收起再切换),与 `study/center.uvue` 完全对齐
- `pages/message/index.uvue`:删除孤儿样式块 `.lc-tabs span.active`(模板早已不用)
### ② 修复 `x-search.border` 误传(真实设计偏差)
`x-search.border``string[]`,且内部守卫是 `if (borderstr.length != 3) return "none"` ——
**恰好要 3 项 `[宽度, 线型, 颜色]`**,不做单位换算,原样拼进 inline style。
- `warehouse/stock.uvue``triage/victims.uvue`` :border="true"``:border="searchBorder"`
`const searchBorder : string[] = ['1px', 'solid', '#E5E7EB']`)。
原先布尔传参**不崩但永远无边框**,而设计稿 `.field` / `.srch` 都是 `1px solid #E5E7EB` → 属静默还原偏差。
同时删掉死属性 `border-color`(颜色是数组第 3 项,不是 prop)。
- `course/library.uvue`:删掉无效的 `:border="false"`(默认 `[]` 即 "none",等价且更诚实)。
- ⚠️ 别把 `round` 一起改:`x-search.round`**String**`round="24rpx"` 正确)。
- 顺带核对:`x-search._border` 不做 `fillArrayCssValue`,所以布尔**不会**触发 `val.map` 崩溃;
`x-alert.round/padding/margin``fillArrayCssValue*`,传布尔照样崩——_不同组件行为不同_。
### ③ 新增两个可复用校验脚本(放在隔离 node 工作区,不进仓库)
`C:/Users/Hyj61/.workbuddy/binaries/node/workspace/`
- `check-uvue-hr.js <项目根>`.uvue script 语法(esbuild+ 标签配平 + pages.json 路由 + **.uts 语法**
- `audit-array-props.js <项目根>`:读 tmx-ui 各组件声明的 `prop: string[],`,反查 pages/components
实际传参,命中布尔/数字/字符串即报出(本次扫出并修完 3 处)
⚠️ **校验脚本自身两个坑(踩过)**
1. `/<template>([\s\S]*?)<\/template>/` 惰性匹配会被内层 `<template v-slot:right>` 的闭合标签
**提前截断** → 满屏「未闭合标签 hr-page, template」假报错。
正确:**先抠掉 `<script>` / `<style>` 整块,再对剩余全文做标签配平**。
2. 遍历必须排除 `uni_modules/`,否则 49 个业务 .uvue 被算成 194 个,还会扫到
`x-svg-u` 等库内非常规写法 → 必然假报错。
### ④ 校验结果(基线更新)
`.uvue 49 / 路由 32 / .uts 11 / 错误 0``audit-array-props.js` → 未发现数组型 prop 误用。
> 上一轮记的「48 个 .uvue」是因为脚本漏扫了 `components/` 目录,**49 才是正确基线**。
### ⑤ 环境备注
本会话 shell 工具的 PATH 异常,直接跑 `ls/grep/node``command not found`
规避:命令前加
`export PATH="/c/Users/Hyj61/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/c/Users/Hyj61/.workbuddy/binaries/PortableGit/versions/1.2.0/mingw64/bin:$PATH"`
(或直接用 Glob / Grep / Read 专用工具)。
另:同一文件连续两次 Edit 时,第一次的改动可能被第二次覆盖,**改完要回读确认**。
---
## 第八轮:5 项 UI 缺陷修复(XCell 单行省略 / 顶部角标 / tabbar 角标 / 通讯录 / 上传误报)
### ① 所有 XCell 的 title 超出一行显示省略号
根因:库 `x-cell``.title` 写死 `lines:2`scoped 样式,`!important` 压字号压不掉行数)。
解法:`hr-lrow` 改用**默认插槽自渲染**标题 `<text class="hr-lrow-t">``lines:1` + `text-overflow:ellipsis`)。
已核对 x-cell 模板:默认插槽的 fallback 才是库自己的 `<text class="title">`,传入插槽即整体替换,不会双渲染。
x-cell 上的 `:title="title"` 保留无害,只是不再被渲染。)全站 30+ 处 hr-lrow 零改动生效。
### ② pages/event/index 顶部切换栏的角标去掉
根因:`tabs` computed 给 x-tabs 传了 `dotType:'label'`+`dotText`(数量)。
解法:tabs 只留标题;顺带删除 `.ev-tab-c*`/`.ev-tab-ct*` 整组角标胶囊样式。
### ③ 底部 tabbar 角标遮挡 icon(★ 本轮最有价值,连错两轮才修对)
**两个算错的前提:**
1. **角标实际宽 28px,不是 18px** —— `min-width:36rpx` 是「内容盒」最小值,还要加左右各
10rpx 内边距 ⇒ 18 + 10 = 28px。按 18px 推算定位必然偏左(上一轮 `right:-20rpx` 因此只压 58%)。
2. **库默认「半出血」本身就会盖住 icon 42%** —— x-tabbar 给 x-badge 传 `:offset="[0,5]"`
叠加 `translate(50%,-50%)` = 「角标中心 = 容器右上角」,28px 宽的角标有 10px 压在 icon 上。
用户最初反馈的「遮挡」正是这个;第六轮改成 `top:0;transform:none` 反而变 100% 全盖,是倒退。
**定稿(`hr-design.css`)=改「左锚定」,复刻设计稿 `.dot` 的 `left:calc(50% + 6px)`**
```css
.xBadge-countAndLabel { top:-4rpx !important; left:44rpx !important; right:auto !important; transform:none !important; }
.xBadge { padding:0 !important; } /* 去掉库为半出血预留的内边距 */
.xBadge,.xBadgeWrap,.xTabbarBox,.xTabbarWrap,.xTabbarItem { overflow:visible !important; }
```
- `left:44rpx = 22px`:定位基准 `.xBadgeWrap` 宽 = icon 盒宽(icon 24px + 左右各 4px = 32px,半宽 16),
其水平中心与 Tab 项中心重合(.xBadge/.xBadgeWrap/.xTabbarItem 三层都 align-items:center)⇒ 16+6=22px。
- 左锚定**与角标自身宽度无关**(两位数/三位数只向右生长,不会回头盖 icon)。
- `top:-4rpx`Tab 项 60px、内容约 53px、上下余量 ~3.5px,取 2px 后顶边仍在项内 ~1.5px。
**Chrome 无头实测(复刻真实 DOM + .xBadge padding:0375 逻辑宽):**
| 方案 | 角标压住 icon | 裁切 |
|---|---|---|
| 修复前 right:0 | 24.0×18.0**100%** | 无 |
| 错解 right:-20rpx | 14.0×16.058% | 无 |
| **定稿 left:44rpx3 Tab / 5 Tab / 两位数)** | **6.0×16.025%** | **无** |
| 库默认半出血 | 10.0×14.042% | 无 |
验证页 `.workbuddy/badge-fix-preview.html` + 截图 `badge-fix-preview.png`(都未进仓库)。
⚠️ 复刻页第一版有个 bug`tabbar(v).replace('class="xTabbar '+cls+'"', ...)` 把方案类名拼丢了,
A/B/C 测出来完全一样。**「三个方案结果完全一致」本身就是没区分度的信号**,先查脚本再下结论。
另外复刻页要补 `.xTabbar{width:100%}`,否则 item 收缩成 32px,裁切检查全失真。
### ④ pages/message/contacts 显示异常
根因:x-indexbar 的行是绝对定位 + `translateY`,行高必须与 `cell-height` 严格一致;
原实现 120rpx cell + 104rpx 行 + 圆角/外边距 → 错位。
解法:整页重写为静态分组卡片列表(指挥调度/协作医院/本队同事),弃用 x-indexbar
`blkText/blkStyle`(头像块主题色,内联下发避开复合选择器)、`extractPhone`(按「·」分段、
跳过「工号」、取纯数字≥4 位为可拨号)、`onCall`。对齐设计稿 s28。
### ⑤ pages/triage/create 选择文件无论多大都提示「超过大小」
根因:`:max-file-size="50"` 被 x-upload-file 当成 **50 字节**(该 prop 单位是字节,默认 30MB)。
解法:`const maxFileSize = 50*1024*1024` + `:max-file-size="maxFileSize"`
`onFileChange` 只 toast**不回写 fileList**(回写会触发库内 watch→covertList 把附件
重置成「已上传无大小」,列表错乱)。
### 校验
静态校验 49 .uvue / 32 路由 / 11 .uts / **0 错误**hr-design.css 花括号 171/171 平衡、注释未失配。
### 环境(本会话续)
- PATH 异常升级:`ls/sed/head/tail/which/grep/find` 都没有,但 **`cd/echo/pwd` 内建可用**、
重定向 `>` 可用、Chrome/node 可用绝对路径跑。
→ 测 DOM 用 `chrome --dump-dom > 文件`,再用 Read/Grep 读文件,**绕开 sed/grep 管道**。
- 无头 Chrome `--screenshot` 只截 `--window-size` 给的视口,看全页要把高给够(本次 420×1780)。
- node `-e` 调试脚本里别混用「原串下标」去切「去注释后的串」,下标会错位(本次踩过)。
---
## 第九轮:检伤建档 2D 人体图伤情标记(完整落地)
需求:点 2D 人体图部位 → xDrawer 弹窗(部位自动填、其余默认填充)→ 多点标记 + 走向可旋转 → 存 JSON 可回显。
### 三个落点
1. **`utils/injury.uts`(数据层,新建)**HrBodyPart / HrInjury(纯基础类型,可直接 JSON)、
9 个正/背热区(buildParts,互不重叠竖条)、13 性质 / 4 出血 / 6 尺寸 / 4 走向 选项与默认值、
`defaultInjuryDesc` 自动描述、`bleedingColor/bleedingBarColor`(红/橙/绿/灰四色语义)、
`sizeBarWidth`1cm≈9rpx,夹 44~132)、`newInjuryId/copyInjury/renumberInjuries/excludeInjury/
findInjury/injuriesOfSide/nextMarkAnchor`(同部位环序错开 8px)、
`injuriesToJson/injuriesFromJson`(逐字段 getString/getNumber 兜底,脏 JSON 不崩)。
2. **`components/hr-bodymap/hr-bodymap.uvue`(展示组件,新建)**props `side/parts/marks`
emit `part-tap(partId)/mark-tap(id)`;热区点亮 `.bmp-hit-on`;标记 = 76rpx 容器
(负 margin 居中,transform 留给旋转)+ `.bmp-bar` 长轴条(长=尺寸、rotate=走向)+ `.bmp-dot` 编号点(不旋转)。
3. **`pages/triage/create.uvue`(重写伤情标记区)**:两个 hr-bodymap(正/背)+ 概览行
`injurySummary` + 复制 JSON 到剪贴板)+ 标记列表(点行=编辑、✕ 走 showModal 确认删除);
x-drawer 表单:正/背分段切换、部位/性质/出血/尺寸/走向 chips、±45° 步进、走向实时预览条、
描述 textarea + 「按当前选项重生成」;编辑态多「删除这处标记」。
- 表单字段**全用顶层 ref**(uvue 嵌套对象响应式不可靠)。
- 自动描述用 `lastAutoDesc` 对比判断「用户是否手改过」:描述为空或等于上次自动值才覆盖。
- 存草稿 = `uni.setStorageSync(HR_DRAFT_KEY, injuriesToJson(marks))`onLoad 里 `injuriesFromJson` 回显。
- 编辑时部位没变则沿用原落点(防止「编辑一下位置就跳走」);变了才按 nextMarkAnchor 重算。
### 验证(三条腿,全部通过)
- 静态校验 `check-uvue-hr.js`**50 .uvue / 32 路由 / 12 .uts / 0 错误**(新增 hr-bodymap + injury.uts)。
- **数据层行为断言 `hr-injury-test.js`(新手法,很值)**esbuild 把 .uts 转 CJS 直接在 node 里跑。
关键是 mock `JSON.parseArray` 成「包了 getString/getNumber 的对象」(UTSJSONObject 语义:
取不到或类型不符返回 null)——直接 JSON.parse 的普通对象没有这些方法会假崩。
44 条断言全过:正背面命名方向、热区不重叠不越界、JSON 无损往返、缺字段/脏 JSON/越界坐标兜底、
同部位 9 点落点互异、重排返回新数组不改原数组。脚本在 `~/.workbuddy/binaries/node/workspace/`
- **视觉预览 `.workbuddy/bodymap-preview.html`+png**rpx 按 0.5 折算 px 复刻,
核对热区覆盖轮廓、标记横/竖/45° 长轴居中旋转、抽屉 chips 与走向预览,截图确认无误。
### x-drawer 使用要点(本次确认)
- `v-model:show` 可用(库内 x-picker 也这么用);bottom 时 `size` 就是内容高(默认 50%,本次 72%)。
- 自定义 footer**必须 `:show-footer="true"`** 再给 `<template v-slot:footer>`;此时确认按钮自己
`@click` 处理并自己把 show 置 false(用内置 footer 时 confirm 事件后会自动关)。
- H5 下 teleport 到 uni-page、APP 内联渲染,都安全;标题可用 `<template v-slot:title>` 自定义。
- `.hr-mask`absolute)与 drawerfixed zIndex 1100)同放 hr-page 的 overlay 插槽互不干扰。
- `:key="p.id + dSide"`:正/背面切换时 chips 强制重建,避免同 key 只改文本不刷新。
---
## 第十轮:App 端编译错误批量修复(CSS / UTS / 类型三类)
### ① App uvue CSS`@keyframes`/`animation`/`page`/`body` 全部不可用(★ 推翻旧结论)
构建日志:`[plugin:uni:app-uvue-css] ERROR: Selector '0%'/'75%'/'from'/'to'/'page'/'body' is not supported` +
`WARNING: 'animation' is not a standard property`
**第六/七轮记的「@keyframes + animation 可用」是错的**App 端 keyframe 选择器直接 ERROR。
- 5 处动效全部改为 **setInterval + ref 驱动内联样式**
| 动效 | 文件 | 实现 |
|---|---|---|
| 待接收红点呼吸 | `pages/event/index.uvue` | `pulseF` 25 帧×68ms=1.7s,前 19 帧扩散(0→16rpx、0.55→0),后 6 帧静默;`dotStyle(e)` 下发 box-shadow |
| 扫描线 ×2 | `pages/warehouse/scan.uvue``components/hr-scan-cam` | `laserT` 步长 **0.03125(=1/32)**32 帧×68ms≈2.2s 三角波;`laserStyle` 下发 top(rpx 定值,不用 %) |
| 手风琴展开 ×2 | `pages/study/center.uvue``pages/study/wrong-book.uvue` | `accT` 30ms/帧 0→1`accStyle(i)` 下发 opacity+translateY,完成后返回空串 |
- **步长必须取 1/帧数**:用 0.035 这类值永远差几 rpx 打不到端点(`hr-anim-test.js` 断言抓出来的)。
红点扩散边界是 `i > PULSE_RAMP`= 号要让峰值帧留在扩散段)。
- `App.uvue` 删掉 `page, body { background-color }`(页面底色由 `.hr-page`/根节点承担);
`#ifdef WEB` 里的 `body::-webkit-scrollbar` 保留(条件编译会剔除,不进 App 编译)。
### ② UTS「先使用后定义」收尾(24 处全清)
UTS **不提升函数声明**,调用点必须在定义之后。最后两处:`warehouse/scan.uvue`shortCode/nowTime/handleCode
提到 onScan 前)、`utils/role.uts`rescueHas/keeperHas 提到 canAccess 前)。扫描脚本 `check-uts-order.js` → 0。
### ③ 模板里直接调用 import 函数 → App 端崩(★ 新坑)
`pages/course/subject.uvue:48` `<x-icon :name="toRemixIcon(e.icon)">`
编译产物是 **`unref(toRemixIcon)(e.icon)`**(编译器分不清 import 的是 ref 还是函数),
Kotlin 报 `找不到名称"invoke"` / `Function invocation 'toRemixIcon(...)' expected`
**script 内定义的函数走 `_ctx.xxx` 没事;只有 import 的会这样。**
解法:页面里包一层 `function entryIcon(g : string) : string { return toRemixIcon(g) }`
已在 `utils/icon.uts` 头注释、`MEMORY.md`、skill 里三处记录;扫描脚本 `check-uts-tpl-import.js` → 0。
### ④ x-tabs 的 change 事件类型:下发 TABS_ITEM,不是 TABS_ITEM_INFO(★ 新坑)
`IllegalArgumentException: ...argument 1 has type TABS_ITEM_INFO, got TABS_ITEM`
库内部 `_list()` 把传入的 `TABS_ITEM_INFO[]` 转成 `TABS_ITEM[]``$emit('change', item, index)`
四个页面同错(event/index、warehouse/devices、triage/detail、study/center),
形参改 `item : TABS_ITEM`,判空改 `item.id == ''`(字段全是非可选,别再 `!` 断言)。
**教训**tmx-ui 凡「XXX_ITEM_INFO 传入、XXX_ITEM 下发」的组件,回调形参要用下发的类型,先读源码确认。
### 新增校验脚本(`~/.workbuddy/binaries/node/workspace/`
- `check-uvue-css.js`App 端 CSS 兼容性(识别 `#ifdef` 跳过非 App 分支,注释剥离要在条件编译判定**之后**)
- `check-uts-tpl-import.js`:模板调用 import 函数
- `hr-anim-test.js`:动效数值断言(端点命中 / 区间 / 周期 / 样式串格式),29 条全过
- 基线:50 .uvue / 32 路由 / 12 .uts / CSS ERROR 0 / 顺序 0 / 模板 import 0
### 环境备注
- Edit 偶发 `EBUSY: resource busy or locked`(同轮并行改多个文件时)→ 重试即可成功。
- Bash 里 `ls/tail` 仍不可用,管道尾接 `tail` 会把整条命令拖成 127 → 直接跑命令不加管道。
---
## 第十一轮:`background-image` App 端兼容(用户提问 → 全站修正)
用户问:**「APP 中不支持 background-image,如何解决?」** 查官方文档
`doc.dcloud.net.cn/uni-app-x/css/background-image` 后结论如下。
### 规则(两条完全不同的路)
| 场景 | App 端 | 解法 |
|---|---|---|
| `background-image: url(...)` 照片 | **完全不支持**(原生视图没有背景图概念) | ① `<image>` 组件 `position:absolute` 铺满容器(推荐);② `onReady` 后用 `DrawableContext``uniElement.getDrawableContext()`)原生绘制 |
| `linear-gradient(...)` 渐变 | **支持,但只有一种合法形态** | `linear-gradient(<关键字方向>, <色1>, <色2>)` |
渐变的三条硬约束:
1. **恰好 3 个参数**(方向 + 起色 + 止色);
2. **方向必须是关键字**`to right/left/top/bottom` 或四向对角线 `to bottom right`…(**`135deg`/`140deg`/`160deg` 在 App 端无效**,H5 能跑、App 静默失效);
3. **恰好 2 个颜色**,不能带 `%` 色标(`#fff 0%`/`#fff 55%` 非法)、不能 3 色。
另:`background-image` 优先级 > `background-color`**flatten/vapor 模式下 `background-image` 被完全禁用**(本项目未用 vapor,已 grep 确认)。
### 本项目实际情况:10 处「渐变」全部违规 → 已全改
原本全是角度 + 多色 / 百分比色标(H5 好看、App 不生效),统一改成 **`to bottom right` + 首尾两色**
| 文件 | 原 | 现 |
|---|---|---|
| `pages/login/index.uvue` | `140deg, #2563EB, #1D4ED8` | `to bottom right, #2563EB, #1D4ED8` |
| `pages/home/index.uvue` | `135deg, #2563EB 0%, #1D4ED8 55%, #1E40AF 100%` | `to bottom right, #2563EB, #1E40AF` |
| `pages/event/index.uvue` | `160deg, #7F1D1D 0%, #B91C1C 55%, #DC2626 100%` | `to bottom, #7F1D1D, #DC2626` |
| `pages/mine/index.uvue` | 角度+多色 | `to bottom right, #2563EB, #1E40AF` |
| `pages/course/practice.uvue` | `135deg, #2563EB, #1D4ED8` | `to bottom right, #2563EB, #1D4ED8` |
| `pages/course/subject.uvue` | 同上 | 同上 |
| `pages/event/detail.uvue` | `135deg, #1E3A8A 0%, #2563EB 72%` | `to bottom right, #1E3A8A, #2563EB` |
| `pages/triage/detail.uvue` | 角度+多色 | `to bottom right, #F59E0B, #D97706` |
| `pages/triage/batch-treat.uvue` | 角度 | `to bottom right, #2563EB, #1D4ED8` |
| `pages/triage/batch-transfer.uvue` | 角度 | `to bottom right, #2563EB, #1D4ED8` |
> 项目**没有**用照片背景,全部是渐变 → 无需 `<image>` 兜底,只需改渐变语法。
### 新增校验脚本
`~/.workbuddy/binaries/node/workspace/check-uvue-bg.js <项目根>`:扫 `<style>` 与内联 `:style` 里的
`background`/`background-image`,报四类问题(`url()` / 角度 / 非关键字方向 / 颜色数≠2 或带 `%`),跳过 `uni_modules`
**结果:52 文件 / ERROR 0**
---
## 第十二轮:hr-row + hr-chip 筛选 → xRadioButton 分段切换
用户要求「将页面的 hr-row 换成 xRadioButton 进行切换」。全站排查后确认这种「切换」共 **5 处**
且是同一套写法(`<scroll-view :scroll-x>``<view class="hr-row">` + `hr-chip v-for`):
| 页面 | 文案 |
|---|---|
| `pages/study/center.uvue` | 全部 / 心肺复苏 5 / 气道管理 4 / AED 3 |
| `pages/study/wrong-book.uvue` | 全部 12 / 心肺复苏 5 / 气道管理 4 / AED 3 |
| `pages/message/list.uvue` | 全部 5 / 派单通知 2 / 系统消息 3 |
| `pages/message/index.uvue` | 全部 9 / 库存预警 4 / 借用审批 3 / 系统 2 |
| `pages/triage/treat-records.uvue` | 全部 4 / 止血 1 / 循环 1 / 分级 2 |
`hr-chip` 另有 2 处是「错 N 次」红色标签,非切换,保留。)
### 改造要点
1. **`RADIO_BUTTON`interface.uts= `{ id:string, icon?, title, disabled? }`**change 下发 `(string, number)`
与 x-tabs 不同(那边是 TABS_ITEM 对象),**没有形参类型坑**;库自身 `x-color-view` 就是 `v-model` 用法,照抄安全。
2. 列表直接写字面量:`chipTabs : RADIO_BUTTON[] = [{ id:'0', title:'全部' } as RADIO_BUTTON, ...]`
`chipId = ref('0')` + `v-model`。原 `chip = ref(0)` 换成
`computed(() : number => { const i = parseInt(chipId.value); return isNaN(i) ? 0 : i })`
(3 处筛选逻辑依赖序号;center/wrong-book 本来就没用到 `chip`,直接删掉避免 unused)。
3. **删掉外层 `<scroll-view :scroll-x>`**xRadioButton 是等宽 `flex:1` 分段,塞进横向滚动容器会收窄、
滑块 `createSelectorQuery` 定位错。用 `<view style="margin-bottom:8rpx">` 包一层保间距。
4. **字号实测必须 22rpx**:等宽 4 段时每段文本可用宽 60.75px(375 基准:343 内容宽 轨道 4px 内距,
÷4,再减每段左右 12px padding)。无头 Chrome 实测 26rpx 加粗「心肺复苏 5」= 63.9px → **截断**
24rpx = 59.0px97%,跨机型太险);**22rpx = 54.1px89%)安全**。
测量页 `node-workspace/radiobtn-fit-test.html`canvas measureText + DOM scrollWidth 双重验证)。
5. 圆角单位坑:滑块圆角 = `parseInt(round) - parseInt(space)` 后**按 px 下发**(外层才走 rpx),
`round=22rpx / space=4rpx` → 内 18px、外 11px,滑块呈胶囊、轨道 22rpx 圆角,视觉成立。
6. 样式基线(延续设计稿「选中=蓝底白字」语义):`bg-color #E5E7EB / active-color #2563EB /
active-font-color #FFFFFF / font-color #6B7280 / text-style font-weight:bold`。
### 校验
`check-uvue-hr`50 .uvue / 32 路由 / 12 .uts)、`check-uts-order`、`check-uts-tpl-import`、
`check-uvue-css`、`check-uvue-bg`、`audit-array-props` 全部 0 错误。
预览 `.workbuddy/radiobutton-preview.html` + `.png`(改造前/后对比 + 5 组文案可点击滑动)。
---
## 第十三轮:事件中心只保留「进行中 / 历史」
用户要求 `/pages/event/index` 只保留进行中和历史 Tab,并删除「待接收」相关部分。
用户在选项中确认:**删待接收 Tab + 全部待接收相关逻辑**。
### 删除清单(pages/event/index.uvue
| 项 | 内容 |
|---|---|
| Tab | 3 → 2(进行中 / 历史),`item-width 33.33% → 50%``stageIds ['0','1','2'] → ['0','1']` |
| P0 全屏推送 | 整块模板(`ev-push`,60 秒倒计时 / 立即接收 / 稍后处理)+ `pushing`/`countdown`/`timer` 状态 + `stopTimer`/`onLater`/`openPush`/`onAccept` 四个函数 |
| 卡片 pending 分支 | `onCardClick` 里的 `stage=='pending' → openPush` 判断(现在统一 `navTo('/pages/event/detail')`);`ev-foot` 的 `v-if="e.stage != 'pending'"` 守卫 |
| 死样式 | `.ev-push*`12 条)+ 早前遗留的 `.ev-nav*` / `.ev-tabs*` / `.ev-tab*` / `.ev-avt*`(共 30+ 条),`{}` 11/11 平衡,模板与样式 100% 对应 |
| 文案修正 | 空态 sub 由「新的派单会通过全屏告警推送给你」→「分配给你的派单会出现在「进行中」,处置完成后归档到「历史」」 |
### 保留(用户未要求删)
- **紧急红点呼吸**`dotStyle` / `startPulse`setInterval 25 帧):触发只看 `urgent` 字段,与 stage 无关;
注释已从「待接收红点」改写为「紧急事件红点」,并注明触发条件(还顺手改了注释里 `pulseK`→`pulseF` 的笔误)。
- 「进行中」下方两行灰字说明(仅显示与我相关的派单 / 时间与处理状态由后台同步)。
- `.evt-urgent`urgent 卡片描边)。
### 连带修正(跨页,仅注释/文案)
- `pages/message/list.uvue` 文件头注释:「与事件中心的待接收流程同源」→「跳转到事件中心查看进行中的派单」。
### ⚠ 遗留(已告知,待用户决定)
1. `mock/index.uts` 里 e3(城东工业园化学品泄漏)/ e4(城南小学集体腹痛)仍是 `stage:'pending'`
两个 Tab 都不会显示它们;
2. `pages/message/list.uvue` 的 P0 消息 `action:'去接收 '` 仍 `navTo('/pages/event/index')`
跳过去后进行中 Tab 看不到该事件 —— **可选解法:把 e3/e4 改成 'ongoing'(消息跳转闭环)或删掉这两条 mock**。
### 校验
全量静态校验 0 错误(50 .uvue / 32 路由 / 12 .uts / CSS / 顺序 / 模板 import / background / 数组 prop)。
预览 `.workbuddy/event-index-preview.html` + `.png`Tab 可切换,复刻 2 Tab + 卡片 + 灰字说明)。
---
## 第十四轮 · 批量处置/批量转运:点击开启相机 + 接收医院 hr-sheet
### 1. hr-scan-cam:相机改为「点击开启」(两页共用,仅此两个消费方)
- `cameraOn` 默认 `true` → `false`:进页面**不再自动占用摄像头**。
- 整块取景区 `.cm-view` 加 `@click="openCamera"`;未开启时渲染 `.cm-idle` 居中引导
camera-line 图标 + 「点击开启摄像头」+ 副文案),取景框/扫描线随 `v-if="cameraOn"` 隐藏。
- 底部按钮文案「已暂停扫码」→「开启扫码」,图标 pause-line → play-circle-line(已核对 remixicon.uts 存在)。
- **扫描线定时器随相机启停**startLaser/stopLaser 改在 openCamera/toggleCamera 内调用,
删除 onMounted 里的 startLaser(不再空跑);import 同步去掉 onMounted。
- 脚本重排:激光相关函数移到 openCamera/toggleCamera 之前(UTS 不提升)。
### 2. batch-transferop-hos 内联单选 → hr-sheet 弹层
- 底部表单的医院 v-for 单选(op-hos* 7 条样式)→ 折叠字段 `.op-pick`(医院名 + 床位 + ⌄,点击整行 openHos)。
- hr-page overlay 插槽:`hr-mask` + `hr-sheet`(沿用 victims/create 同一套全局类;
`.hr-sheet-mask` 虽在 hr-design.css 里但全项目没人用,实际惯例是 hr-mask)。
- 弹层选项为两行式:医院名(os-name,选中蓝/满员灰)+ hospitalMeta(距现场 km · 约N分钟)+ 床位/满员红字 + ✓;
mock 新导入 hospitalMeta。
- pickHospital:满员 → toast「该医院急诊已满员,不可选择」且**不关弹层**;可用 → 选中并关弹层。
- 删除 hosClass 与 op-hos* 死样式(中途误复制过一份 hosOptTextClass,已清理);
batch-treat 仅改头部注释(组件级改动自动生效)。
### 校验 & 预览
- 全量静态校验 0 错误(50 .uvue / 62 uts 顺序 / CSS / 模板 import / background / 数组 prop)。
- 预览 `.workbuddy/batch-transfer-preview.html` + `.png`(无头 Chrome 880x2080 截图:
状态A 相机未开启 + 折叠字段 / 状态B 弹层展开,均正确)。
---
## 第十五轮 · 修 x-upload-file 运行期 NPEdefaultList 缺 request
### 报错
```
error: java.lang.NullPointerException: null cannot be cast to non-null type kotlin.Any
at uni_modules/tmx-ui/components/x-upload-file/x-upload-file.uvue:234:5 (return { ... })
```
触发点:`pages/triage/create.uvue` 的 `:default-list="fileList"`(项为 `{ name, url }`**没有 `request`**)。
### 根因
- `x-file-s/utssdk/interface.uts` 里 `xFileSListType.request?: any` 是**可选字段**
模板从头到尾也没读过 `request`(只原样透传给宿主),所以缺它是合法的。
- 但 `covertList()` 里写的是 `let requestdata = el.getAny("request")` →
`request: requestdata as any`。UTS 的 `any` = Kotlin **非空** `Any`
`as any` 就是 `as Any`,值为 null 时直接抛。
- 同一段里 `type`L228-229)、`name`L230-231)都做了 `== null` 兜底,**只有 `request` 漏了**
属于组件自身缺陷,不是调用方传错。
- 佐证 `getAny` 本身不抛:`tmx-ui/config/xConfig.uts:198` 就是 `if(cfg.getAny('theme')!=null)`
且栈指向 `return {` 字面量那一行而非 `let x = getAny(...)` 那行 → 是字面量里的 `as` 强转炸的。
### 改法(就地补丁,两个条件编译分支都改)
`uni_modules/tmx-ui/components/x-upload-file/x-upload-file.uvue` `covertList()`
```uts
// 原:let requestdata = el.getAny("request") → request: requestdata as any
let requestdata : any | null = el.getAny("request") // 类型留可空
// 字面量里:request: requestdata, // 去掉 as any 强转
```
补丁处已加「⚠ 本地补丁(2026-09-24)」标记注释。`#ifdef APP || MP-WEIXIN` 与 `#ifdef WEB`
两个 return 分支都改了(只改一个会剩下一个平台崩)。
### 校验
- `{ } / ( ) / [ ]` 计数配平(137/137、221/221、37/37);
- 全量静态校验 0 错误(check-uvue-hr / check-uts-order / check-uvue-css /
check-uts-tpl-import / check-uvue-bg)。
- ⚠ **本环境没有 HBuilderX,跑不起来真机**,需要用户重新编译验证。
### 顺带登记的隐患(未改,同类写法)
- `x-picker-selected.uvue:250/341/346` `el.getAny(props.idKey)!` —— 项缺 idKey 时会崩(`!` 断言)。
- `x-table.uvue` 模板里 `{{item.getAny(item2.key)}}` —— 列 key 与数据不匹配时可能崩。
- 这两处本项目当前未触发,先记着;用到时按同一套改法处理。
### 沉淀
- 技能 `uvue-design-port`:第三步新增第 18 条(`background-image` 三参两色关键字渐变,
之前只写在脚本表里、正文漏了)+ 第 19 条(`any` 非空 / `as any` 崩 / getAny 语义 / 栈定位技巧);
第四步新增「库组件运行期崩溃:就地打补丁的标准流程」(标记注释 + 双分支 + 补丁清单 + 只做结构自检);
description 补上「运行期 NPE」触发词。
- 项目 MEMORY.mduvue 硬约束加第 19 条 + 新增「uni_modules 本地补丁清单」表。