51 KiB
2026-09-24 医院救援 APP(uniappx + Tmui4)UI 全量重构
目标
按 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 限制做了三类适配:- 不支持
::before/::after→ 全部改为实体元素(如.hr-sec-bar、.bm-*人体图鉴) - 不支持 CSS 变量
var()→ 颜色全部写实 - 不用
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-appimport 页面生命周期 → 全部删除 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.12(10.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改为 rpx(26rpx / 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" />(props:color/textColor/textSize/iconSize/vertical/icon/hideText/label;默认 icon 为loader-line、vertical 默认 true)
经验(写给以后的自己)
布局问题别靠读代码猜 —— 先把结构+CSS 复刻成 HTML,用本机 Chrome 无头截图看真实渲染, 一眼就能判断是「CSS 结构问题」还是「uvue 渲染差异」,避免连续几轮盲改。
仍未完成
study/center、warehouse/scan、study/exams的分段控件待换 xTabstriage/create表单字段待换 xInputcourse/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):
.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):
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/indexonPageShow→markEventsRead();message/index、message/listonPageShow→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 处)
⚠️ 校验脚本自身两个坑(踩过):
/<template>([\s\S]*?)<\/template>/惰性匹配会被内层<template v-slot:right>的闭合标签 提前截断 → 满屏「未闭合标签 hr-page, template」假报错。 正确:先抠掉<script>/<style>整块,再对剩余全文做标签配平。- 遍历必须排除
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(★ 本轮最有价值,连错两轮才修对)
两个算错的前提:
- 角标实际宽 28px,不是 18px ——
min-width:36rpx是「内容盒」最小值,还要加左右各 10rpx 内边距 ⇒ 18 + 10 = 28px。按 18px 推算定位必然偏左(上一轮right:-20rpx因此只压 58%)。 - 库默认「半出血」本身就会盖住 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):
.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:0,375 逻辑宽):
| 方案 | 角标压住 icon | 裁切 |
|---|---|---|
| 修复前 right:0 | 24.0×18.0(100%) | 无 |
| 错解 right:-20rpx | 14.0×16.0(58%) | 无 |
| 定稿 left:44rpx(3 Tab / 5 Tab / 两位数) | 6.0×16.0(25%) | 无 |
| 库默认半出血 | 10.0×14.0(42%) | 无 |
验证页 .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 可回显。
三个落点
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 不崩)。components/hr-bodymap/hr-bodymap.uvue(展示组件,新建):propsside/parts/marks, emitpart-tap(partId)/mark-tap(id);热区点亮.bmp-hit-on;标记 = 76rpx 容器 (负 margin 居中,transform 留给旋转)+.bmp-bar长轴条(长=尺寸、rotate=走向)+.bmp-dot编号点(不旋转)。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 里跑。 关键是 mockJSON.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)与 drawer(fixed 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.uvuepulseF25 帧×68ms=1.7s,前 19 帧扩散(0→16rpx、0.55→0),后 6 帧静默;dotStyle(e)下发 box-shadow扫描线 ×2 pages/warehouse/scan.uvue、components/hr-scan-camlaserT步长 0.03125(=1/32),32 帧×68ms≈2.2s 三角波;laserStyle下发 top(rpx 定值,不用 %)手风琴展开 ×2 pages/study/center.uvue、pages/study/wrong-book.uvueaccT30ms/帧 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>) |
渐变的三条硬约束:
- 恰好 3 个参数(方向 + 起色 + 止色);
- 方向必须是关键字:
to right/left/top/bottom或四向对角线to bottom right…(135deg/140deg/160deg在 App 端无效,H5 能跑、App 静默失效); - 恰好 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 次」红色标签,非切换,保留。)
改造要点
RADIO_BUTTON(interface.uts)={ id:string, icon?, title, disabled? },change 下发(string, number)。 与 x-tabs 不同(那边是 TABS_ITEM 对象),没有形参类型坑;库自身x-color-view就是v-model用法,照抄安全。- 列表直接写字面量:
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)。 - 删掉外层
<scroll-view :scroll-x>:xRadioButton 是等宽flex:1分段,塞进横向滚动容器会收窄、 滑块createSelectorQuery定位错。用<view style="margin-bottom:8rpx">包一层保间距。 - 字号实测必须 22rpx:等宽 4 段时每段文本可用宽 60.75px(375 基准:343 内容宽 − 轨道 4px 内距,
÷4,再减每段左右 12px padding)。无头 Chrome 实测 26rpx 加粗「心肺复苏 5」= 63.9px → 截断;
24rpx = 59.0px(97%,跨机型太险);22rpx = 54.1px(89%)安全。
测量页
node-workspace/radiobtn-fit-test.html(canvas measureText + DOM scrollWidth 双重验证)。 - 圆角单位坑:滑块圆角 =
parseInt(round) - parseInt(space)后按 px 下发(外层才走 rpx),round=22rpx / space=4rpx→ 内 18px、外 11px,滑块呈胶囊、轨道 22rpx 圆角,视觉成立。 - 样式基线(延续设计稿「选中=蓝底白字」语义):
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文件头注释:「与事件中心的待接收流程同源」→「跳转到事件中心查看进行中的派单」。
⚠ 遗留(已告知,待用户决定)
mock/index.uts里 e3(城东工业园化学品泄漏)/ e4(城南小学集体腹痛)仍是stage:'pending', 两个 Tab 都不会显示它们;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-transfer:op-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 运行期 NPE(defaultList 缺 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():
// 原: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/346el.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.md:uvue 硬约束加第 19 条 + 新增「uni_modules 本地补丁清单」表。