Files
2026-09-24 16:25:22 +08:00

51 KiB
Raw Permalink Blame History

2026-09-24 医院救援 APPuniappx + Tmui4UI 全量重构

目标

mobile-design/v2/hospital-rescue-ui-v2.html35 屏设计稿)在 uniappx 项目中 1:1 还原 UI,并实现全部交互,兼容 APP + H5,双角色(救援端 / 仓管端)。

已完成

  • 清空 pages/ 并重建:33 个页面(35 屏中 s07/s42 合并为事件中心同一页的执行状态 Tab)
  • 设计令牌落地:新增 common/styles/hr-design.cssApp.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.utshrSession / 双角色 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 实现有 bugpickValues 初值改为按索引逐个取,避免依赖数组展开。

编译警告修复

  • nav.utstoast(title, icon: string) → 形参改字面量联合类型 HrToastIcon 否则 uni.showToastType 'string' is not assignable to type 'Icon | null | undefined'
  • 5 个文件误从 @dcloudio/uni-app import 页面生命周期 → 全部删除 import, 改用 uvue 全局函数;并纠正 quiz/answer-card 的 onShowonPageShowuvue 组合式是 onPageShow/onPageHide)。
  • res.content 类型是 string|null|undefined3 处 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-tabbaroutIndex 默认值是 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.utstoRemixIcon() 字符→RemixIcon 映射,组件内统一解析 所有图标

好处:页面调用方式不变,30+ 调用点零改动即换成库组件;新页面可直接写 <x-cell> / RemixIcon 名。

新增/替换的库组件

  • xSearchtriage/victimscourse/librarywarehouse/stock 的搜索框
  • xTabsevent/index3 执行状态)、triage/detail4 分页)
  • xAvatarGroupevent/index 卡片头像组、event/detail 救援团队
  • xIndexbarmessage/contacts 整页重写为索引列表
  • xUploadFiletriage/create 多模态记录附件
  • xInputlogin 工号/密码
  • xSkeleton:新增 components/hr-skeleton(基于 xSkeleton),event/index 已接入

待办(未完成,下次继续)

  • xTabs 还剩 4 处:study/centerwarehouse/scanwarehouse/devicesstudy/exams 的 seg
  • xInput 还剩:triage/create 表单字段、批量处置/转运的联系人字段
  • xSkeleton 还剩:hometriage/victimswarehouse/stock 等列表页加载态
  • 首页/详情页里仍有少量 {{ glyph }} 形式的独立字符图标(tr-quick / lc-tabs 等)待换 x-icon

复用的排查手法(已验证)

静态校验脚本 check-uvue.js 已能拦住:computed() : T => 写法、模板调用未声明函数、组件标签不存在、路由未注册。


2026-09-24 第四轮:8 项 UI 缺陷修复

# 问题 根因 / 解法
1 tabbar 页面底部没预留高度 x-tabbarposition 默认是 '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.utsMOCK_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-maskabsolute 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/centerwarehouse/scanstudy/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-tabbardotLabel)、x-tabsdotType:'label'+dotText 只能靠全局 !important 覆盖 .xBadge-countAndLabel / .xBadge-countAndLabelText
  • 自写角标:hr-badge/hr-badge-t 落在 hr-design.csspages/mine/index.uvue(基准)与 pages/message/index.uvue 都改用它;原先各自的 .mine-dot/.mine-dot-t.ms-dot/.ms-dot-t 已删除。
  • xBadgex-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}')。内联样式只能用 !importantCSS 规定 !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-dot16rpx 优先级状态圆点)、.ev-push-badgeP0 派单白色胶囊)
  • home/index.tr-bell-dot14rpx 红点提醒)、hr-rec-list.rl-c(浅蓝「N 人」信息 chip
  • hr-wb / hr-wband 的状态小圆点、triage/create.c-grade-dot44rpx 分级选择圈)
  • course/media.md-badge(按类型着色的分类标签,不是计数角标) 这些是「状态点 / 标签 / 胶囊」,与计数角标语义不同;若也要统一需先确认。

2026-09-24 第六轮:5 项修复(崩溃 / 角标裁切 / 图标 / 学习中心 / 未读隐藏)

① H5 崩溃:val.map is not a function @ x-alert.uvue:266(★ 重要通用坑)

根因x-alertround 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.borderstring[] 内部 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):

.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); catref(0) 改为由 activeId(字符串 id)换算的 computed
  • 「查看解析」⌃ / ⌄ 字符 → <x-icon name="arrow-up/down-s-line">
  • 折叠框改手风琴:新增 .qa-acc 容器 + @keyframes qa-acc-inopacity + translateY 240ms); toggle() 改为同时只展开一项(真正的手风琴语义,原为多项可同时展开)

⑤ 未读角标「查看后隐藏」

新增 utils/unread.uts(唯一来源,reactive):

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.utsrescuerTabs / keeperTabsconst 数组改为函数(每次重建数组, 读 badgeText(hrUnread.xxx))→ 未读数变化时 hr-tabbar 的 computed 重新求值,角标自动消失。
  • 「我的」页消息中心入口:v-if="hrUnread.msgCenter > 0"
  • event/index onPageShowmarkEventsRead()message/indexmessage/list onPageShowmarkMessagesRead()
  • message/index 单条角标:新增本地 readKeyskey = 分组下标-条目下标),点开单条即消失, 「全部已读」清空全部;onMsgClick 增加 gi, i 两个参数。

校验

48 个 .uvue 静态校验 0 错误;另用 esbuild 单独校验了 3 个改动的 .utscheck-uvue.js 不覆盖 .uts)。

仍未做(用户没提,待确认)

  • pages/study/wrong-book.uvue 有与学习中心完全相同qa-fold 字符图标 + 折叠逻辑,未同步改造
  • lc-tabspages/message/index.uvue 还剩一处(.lc-tabs span.active 样式块,模板可能已不用,待清理)

第七轮:收尾清理 + 数组型 prop 全站审计

① 补齐第六轮遗留

  • pages/study/wrong-book.uvue:「查看解析」⌃ / ⌄<x-icon>,折叠框改手风琴 .qa-acc + @keyframes qa-acc-intoggle() 先全收起再切换),与 study/center.uvue 完全对齐
  • pages/message/index.uvue:删除孤儿样式块 .lc-tabs span.active(模板早已不用)

② 修复 x-search.border 误传(真实设计偏差)

x-search.borderstring[],且内部守卫是 if (borderstr.length != 3) return "none" —— 恰好要 3 项 [宽度, 线型, 颜色],不做单位换算,原样拼进 inline style。

  • warehouse/stock.uvuetriage/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.roundStringround="24rpx" 正确)。
  • 顺带核对:x-search._border 不做 fillArrayCssValue,所以布尔不会触发 val.map 崩溃; 但 x-alert.round/padding/marginfillArrayCssValue*,传布尔照样崩——不同组件行为不同

③ 新增两个可复用校验脚本(放在隔离 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 / 错误 0audit-array-props.js → 未发现数组型 prop 误用。

上一轮记的「48 个 .uvue」是因为脚本漏扫了 components/ 目录,49 才是正确基线

⑤ 环境备注

本会话 shell 工具的 PATH 异常,直接跑 ls/grep/nodecommand 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:2scoped 样式,!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)=改「左锚定」,复刻设计稿 .dotleft:calc(50% + 6px)

.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:-4rpxTab 项 60px、内容约 53px、上下余量 ~3.5px,取 2px 后顶边仍在项内 ~1.5px。

Chrome 无头实测(复刻真实 DOM + .xBadge padding:0375 逻辑宽):

方案 角标压住 icon 裁切
修复前 right:0 24.0×18.0100%
错解 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(都未进仓库)。 ⚠️ 复刻页第一版有个 bugtabbar(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(红/橙/绿/灰四色语义)、 sizeBarWidth1cm≈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 + 「按当前选项重生成」;编辑态多「删除这处标记」。
    • 表单字段全用顶层 refuvue 嵌套对象响应式不可靠)。
    • 自动描述用 lastAutoDesc 对比判断「用户是否手改过」:描述为空或等于上次自动值才覆盖。
    • 存草稿 = uni.setStorageSync(HR_DRAFT_KEY, injuriesToJson(marks))onLoad 里 injuriesFromJson 回显。
    • 编辑时部位没变则沿用原落点(防止「编辑一下位置就跳走」);变了才按 nextMarkAnchor 重算。

验证(三条腿,全部通过)

  • 静态校验 check-uvue-hr.js50 .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+pngrpx 按 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-maskabsolute)与 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.uvuecomponents/hr-scan-cam laserT 步长 0.03125(=1/32)32 帧×68ms≈2.2s 三角波;laserStyle 下发 top(rpx 定值,不用 %)
    手风琴展开 ×2 pages/study/center.uvuepages/study/wrong-book.uvue accT 30ms/帧 0→1accStyle(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.uvueshortCode/nowTime/handleCode 提到 onScan 前)、utils/role.utsrescueHas/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(...)' expectedscript 内定义的函数走 _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.jsApp 端 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 后用 DrawableContextuniElement.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-colorflatten/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_BUTTONinterface.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.htmlcanvas 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-hr50 .uvue / 32 路由 / 12 .uts)、check-uts-ordercheck-uts-tpl-importcheck-uvue-csscheck-uvue-bgaudit-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-footv-if="e.stage != 'pending'" 守卫
死样式 .ev-push*12 条)+ 早前遗留的 .ev-nav* / .ev-tabs* / .ev-tab* / .ev-avt*(共 30+ 条),{} 11/11 平衡,模板与样式 100% 对应
文案修正 空态 sub 由「新的派单会通过全屏告警推送给你」→「分配给你的派单会出现在「进行中」,处置完成后归档到「历史」」

保留(用户未要求删)

  • 紧急红点呼吸dotStyle / startPulsesetInterval 25 帧):触发只看 urgent 字段,与 stage 无关; 注释已从「待接收红点」改写为「紧急事件红点」,并注明触发条件(还顺手改了注释里 pulseKpulseF 的笔误)。
  • 「进行中」下方两行灰字说明(仅显示与我相关的派单 / 时间与处理状态由后台同步)。
  • .evt-urgenturgent 卡片描边)。

连带修正(跨页,仅注释/文案)

  • 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 + .pngTab 可切换,复刻 2 Tab + 卡片 + 灰字说明)。


第十四轮 · 批量处置/批量转运:点击开启相机 + 接收医院 hr-sheet

1. hr-scan-cam:相机改为「点击开启」(两页共用,仅此两个消费方)

  • cameraOn 默认 truefalse:进页面不再自动占用摄像头
  • 整块取景区 .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.utsxFileSListType.request?: any可选字段 模板从头到尾也没读过 request(只原样透传给宿主),所以缺它是合法的。
  • covertList() 里写的是 let requestdata = el.getAny("request")request: requestdata as any。UTS 的 any = Kotlin 非空 Any as any 就是 as Any,值为 null 时直接抛。
  • 同一段里 typeL228-229)、nameL230-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()

// 原: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 本地补丁清单」表。