1
@@ -0,0 +1,10 @@
|
|||||||
|
/unpackage/
|
||||||
|
/docweb/node_modules/
|
||||||
|
/docweb/dist/
|
||||||
|
/docs/node_modules/
|
||||||
|
.DS_Store
|
||||||
|
/uni_modules/x-ocr-s/utssdk/app-ios/Resources/
|
||||||
|
*._
|
||||||
|
._.*
|
||||||
|
._.
|
||||||
|
._*
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version" : "1.0",
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"customPlaygroundType" : "device",
|
||||||
|
"playground" : "standard",
|
||||||
|
"type" : "uni-app:app-android"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>tabbar 角标遮挡 icon · 方案对照</title>
|
||||||
|
<style>
|
||||||
|
/* 375 逻辑宽:1rpx = 0.5px(浏览器不认 rpx,必须折算)。
|
||||||
|
不设 box-sizing,与 uvue view 的默认(content-box)一致 ——
|
||||||
|
这一点很关键:min-width 是「内容盒」最小值,角标实际宽 = 18 + 10 = 28px。 */
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: #F3F4F6;
|
||||||
|
color: #1F2937;
|
||||||
|
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 库组件 x-tabbar / x-badge 真实样式(抄自 uni_modules 源码) ===== */
|
||||||
|
.xTabbar {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xTabbarWrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xTabbarItem {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* view 在 uvue 上默认是 flex column */
|
||||||
|
.xBadge {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xBadgeWrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xBadge-countAndLabel {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 0rpx 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 角标尺寸 = hr-design.css 的 !important 覆盖值(36rpx 高 / 22.4rpx 字 / padding 10rpx) */
|
||||||
|
.xBadge-countAndLabel {
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xBadge-countAndLabelText {
|
||||||
|
font-size: 11.2px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hr-design.css:.xBadge { padding: 0 !important } —— 去掉库为「半出血」预留的内边距 */
|
||||||
|
.xBadge {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- A:库默认 + 取消出血(= 本轮修复前的状态,right:0) ---- */
|
||||||
|
.A .xBadge-countAndLabel {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- B:上一轮的错解(右移 20rpx)—— 假设角标宽 18px 才会成立,实际 28px ---- */
|
||||||
|
.B .xBadge-countAndLabel {
|
||||||
|
top: -2px;
|
||||||
|
right: -10px;
|
||||||
|
left: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- C:本轮定稿(左锚定 44rpx = 22px,宽度无关) ---- */
|
||||||
|
.C .xBadge-countAndLabel {
|
||||||
|
top: -2px;
|
||||||
|
left: 22px;
|
||||||
|
right: auto;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 库默认「半出血」(仅供对照,需要上方留白) */
|
||||||
|
.D .xBadge-countAndLabel {
|
||||||
|
top: 5px;
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
transform: translate(50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame {
|
||||||
|
width: 375px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, .1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame .tip {
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: #374151;
|
||||||
|
padding: 6px 12px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim {
|
||||||
|
width: 375px;
|
||||||
|
height: 76px;
|
||||||
|
background: #F9FAFB;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* item 框辅助线:用于判断角标是否被裁 */
|
||||||
|
.sim .xTabbarItem {
|
||||||
|
outline: 1px dashed #93C5FD;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 0 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #111827;
|
||||||
|
color: #A7F3D0;
|
||||||
|
font-size: 10.5px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h2>底部 tabbar 角标 · 定位方案对照(375 逻辑宽,1rpx = 0.5px)</h2>
|
||||||
|
<pre id="out">…</pre>
|
||||||
|
<div id="host"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var ICON_PX = 24; /* hr-tabbar 传 icon-size="48rpx" = 24px */
|
||||||
|
var FS = 13; /* hr-tabbar 传 font-size="26rpx" = 13px */
|
||||||
|
|
||||||
|
/* 按 x-tabbar 的真实结构生成一屏 tabbar:
|
||||||
|
.xTabbarItem > .xBadge > .xBadgeWrap > [角标, icon盒(30px,padding 0 4px), 文字] */
|
||||||
|
function tabbar(v) {
|
||||||
|
var items = v.count === 3
|
||||||
|
? [['首页', '#9CA3AF'], ['事件', '#2563EB'], ['我的', '#9CA3AF']]
|
||||||
|
: [['扫码', '#9CA3AF'], ['仓库', '#9CA3AF'], ['事件', '#2563EB'], ['消息', '#9CA3AF'], ['我', '#9CA3AF']];
|
||||||
|
var badgeIdx = v.count === 3 ? 1 : 2;
|
||||||
|
|
||||||
|
var out = [];
|
||||||
|
items.forEach(function(it, i) {
|
||||||
|
var badge = i === badgeIdx
|
||||||
|
? '<div class="xBadge-countAndLabel" style="background:#DC2626">' +
|
||||||
|
'<span class="xBadge-countAndLabelText" style="color:#fff">' + v.label + '</span></div>'
|
||||||
|
: '';
|
||||||
|
out.push(
|
||||||
|
'<div class="xTabbarItem" style="height:60px">' +
|
||||||
|
'<div class="xBadge">' +
|
||||||
|
'<div class="xBadgeWrap">' + badge +
|
||||||
|
/* icon 盒:库内写死 height:30px + padding:0px 4px */
|
||||||
|
'<div style="height:30px;padding:0px 4px">' +
|
||||||
|
'<span class="ic" style="display:block;width:' + ICON_PX + 'px;height:' + ICON_PX +
|
||||||
|
'px;background:' + it[1] + ';border-radius:6px"></span>' +
|
||||||
|
'</div>' +
|
||||||
|
/* 文字:库内内联 fontSize=26rpx + paddingTop:4px */
|
||||||
|
'<span class="lb" style="font-size:' + FS + 'px;color:' + it[1] + ';padding-top:4px">' +
|
||||||
|
it[0] + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>');
|
||||||
|
});
|
||||||
|
return '<div class="xTabbar ' + v.cls + '"><div class="xTabbarWrap">' + out.join('') + '</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var variants = [
|
||||||
|
{ cls: 'A', count: 3, label: '4', title: 'A 本轮修复前(right:0 + 取消出血)' },
|
||||||
|
{ cls: 'B', count: 3, label: '4', title: 'B 上一轮的错解(right:-20rpx,按角标宽 18px 推算)' },
|
||||||
|
{ cls: 'C', count: 3, label: '4', title: 'C 本轮定稿(left:44rpx,3 Tab)' },
|
||||||
|
{ cls: 'C', count: 5, label: '4', title: 'C 本轮定稿(left:44rpx,5 Tab 仓管端)' },
|
||||||
|
{ cls: 'C', count: 5, label: '99', title: 'C 本轮定稿(两位数角标,验证向右生长不回头)' },
|
||||||
|
{ cls: 'D', count: 3, label: '4', title: 'D 库默认「半出血」(对照:需要上方留白)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
variants.forEach(function(v, i) {
|
||||||
|
html += '<div class="frame"><div class="tip">' + v.title + '</div><div class="sim">' +
|
||||||
|
tabbar(v).replace('class="xTabbar ' + v.cls + '"', 'class="xTabbar ' + v.cls + '" id="tb' + i + '"') +
|
||||||
|
'</div></div>';
|
||||||
|
});
|
||||||
|
document.getElementById('host').innerHTML = html;
|
||||||
|
|
||||||
|
function rect(el) {
|
||||||
|
var r = el.getBoundingClientRect();
|
||||||
|
return { l: +r.left.toFixed(1), t: +r.top.toFixed(1), r: +r.right.toFixed(1), b: +r.bottom.toFixed(1), w: +r.width.toFixed(1), h: +r.height.toFixed(1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function report(v, i) {
|
||||||
|
var root = document.getElementById('tb' + i);
|
||||||
|
var badgeIdx = v.count === 3 ? 1 : 2;
|
||||||
|
var item = root.querySelectorAll('.xTabbarItem')[badgeIdx];
|
||||||
|
var wrap = rect(item.querySelector('.xBadgeWrap'));
|
||||||
|
var bx = rect(item.querySelector('.xBadge'));
|
||||||
|
var ic = rect(item.querySelector('.ic'));
|
||||||
|
var bd = rect(item.querySelector('.xBadge-countAndLabel'));
|
||||||
|
var it = rect(item);
|
||||||
|
|
||||||
|
var ox = Math.max(0, Math.min(ic.r, bd.r) - Math.max(ic.l, bd.l));
|
||||||
|
var oy = Math.max(0, Math.min(ic.b, bd.b) - Math.max(ic.t, bd.t));
|
||||||
|
var clipTop = Math.max(0, it.t - bd.t);
|
||||||
|
var clipBot = Math.max(0, bd.b - it.b);
|
||||||
|
var clipR = Math.max(0, bd.r - it.r);
|
||||||
|
|
||||||
|
return v.title + '\n' +
|
||||||
|
' item(60px) ' + JSON.stringify(it) + '\n' +
|
||||||
|
' .xBadgeWrap ' + JSON.stringify(wrap) + ' ← 角标的定位基准(半宽 ' + (wrap.w / 2).toFixed(1) + 'px)\n' +
|
||||||
|
' .xBadge ' + JSON.stringify(bx) + (bx.t < it.t - 0.5 || bx.b > it.b + 0.5 ? ' ⚠ 超出 item' : ' ✓ 在 item 内') + '\n' +
|
||||||
|
' icon ' + JSON.stringify(ic) + '\n' +
|
||||||
|
' 角标 ' + JSON.stringify(bd) + ' ← 实际宽 ' + bd.w + 'px\n' +
|
||||||
|
' ⇒ 角标压住 icon:' + ox.toFixed(1) + ' × ' + oy.toFixed(1) + ' px' +
|
||||||
|
'(横向占 icon 的 ' + (ox / ic.w * 100).toFixed(0) + '%)\n' +
|
||||||
|
' ⇒ 裁切检查:上溢 ' + clipTop.toFixed(1) + ' / 下溢 ' + clipBot.toFixed(1) + ' / 右溢 ' + clipR.toFixed(1) + ' px' +
|
||||||
|
((clipTop === 0 && clipBot === 0 && clipR === 0) ? ' ✓ 不会被裁' : ' ⚠ 有被裁风险');
|
||||||
|
}
|
||||||
|
|
||||||
|
var out = [];
|
||||||
|
variants.forEach(function(v, i) { out.push(report(v, i)); });
|
||||||
|
document.getElementById('out').textContent = out.join('\n\n');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,143 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><meta charset="utf-8"><style>
|
||||||
|
/* 375 逻辑宽:1rpx = 0.5px */
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin:0; padding:16px; width:375px; background:#F3F4F6;
|
||||||
|
font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif; color:#1F2937; }
|
||||||
|
h1 { font-size:15px; margin:0 0 4px; }
|
||||||
|
.sub { font-size:10px; color:#6B7280; margin-bottom:14px; line-height:1.6; }
|
||||||
|
.card { background:#fff; border-radius:9px; padding:14px 14px 10px; margin-bottom:14px;
|
||||||
|
box-shadow:0 1px 3px rgba(15,23,42,.08); }
|
||||||
|
.card h2 { font-size:12px; margin:0 0 12px; color:#374151; }
|
||||||
|
.tag { display:inline-block; font-size:10px; padding:1px 6px; border-radius:5px; margin-left:6px; }
|
||||||
|
.tag-bad { background:#FEF2F2; color:#DC2626; }
|
||||||
|
.tag-ok { background:#EFF6FF; color:#2563EB; }
|
||||||
|
|
||||||
|
/* ---- 库组件 x-badge 自身样式 ---- */
|
||||||
|
.xBadge-countAndLabel { position:absolute; z-index:3; border-radius:100px; padding:0px 4px;
|
||||||
|
display:flex; flex-direction:row; align-items:center; justify-content:center; }
|
||||||
|
.xBadge-countAndLabelText { line-height:1.5; text-align:center; }
|
||||||
|
|
||||||
|
/* ---- 改后:与基准一致(rpx 已折算)---- */
|
||||||
|
.xBadge-countAndLabel.fixed { min-width:18px; height:18px; padding:0 5px; border-radius:9px; }
|
||||||
|
.xBadge-countAndLabel.fixed .xBadge-countAndLabelText { font-size:11.2px; line-height:1; font-weight:bold; }
|
||||||
|
|
||||||
|
/* ---- 自写基准角标 .hr-badge ---- */
|
||||||
|
.hr-badge { min-width:18px; height:18px; padding-left:5px; padding-right:5px; border-radius:9px;
|
||||||
|
background-color:#DC2626; display:flex; flex-direction:row; align-items:center; justify-content:center; }
|
||||||
|
.hr-badge-t { font-size:11.2px; font-weight:bold; color:#fff; }
|
||||||
|
|
||||||
|
.row { display:flex; flex-direction:row; align-items:center; }
|
||||||
|
.grow { flex:1; }
|
||||||
|
|
||||||
|
.cmp { display:flex; flex-direction:row; align-items:flex-end; gap:34px; padding:6px 4px 2px; }
|
||||||
|
.cmp-item { display:flex; flex-direction:column; align-items:center; }
|
||||||
|
.cmp-item .lbl { font-size:10px; color:#6B7280; margin-top:8px; text-align:center; line-height:1.5; }
|
||||||
|
.stage { width:64px; height:40px; display:flex; align-items:center; justify-content:center; }
|
||||||
|
|
||||||
|
/* 列表行 */
|
||||||
|
.lrow { display:flex; flex-direction:row; align-items:center; padding:11px 0; border-bottom:1px solid #F3F4F6; }
|
||||||
|
.lrow:last-child { border-bottom:0; }
|
||||||
|
.lic { width:30px; height:30px; border-radius:8px; background:#FFF7ED; color:#D97706;
|
||||||
|
display:flex; align-items:center; justify-content:center; font-size:15px; margin-right:9px; }
|
||||||
|
.lt { font-size:14px; }
|
||||||
|
.ls { font-size:11px; color:#9CA3AF; margin-top:2px; }
|
||||||
|
.lval { font-size:15px; color:#D1D5DB; margin-left:6px; }
|
||||||
|
|
||||||
|
/* 底部 tabbar 预览 */
|
||||||
|
.tb { display:flex; flex-direction:row; background:#fff; border-top:1px solid #F3F4F6; border-radius:0 0 9px 9px; }
|
||||||
|
.tb-i { flex:1; display:flex; flex-direction:column; align-items:center; padding:8px 0 6px; position:relative; }
|
||||||
|
.tb-ic { width:17px; height:17px; border-radius:5px; background:#D1D5DB; }
|
||||||
|
.tb-ic.on { background:#2563EB; }
|
||||||
|
.tb-l { font-size:13px; color:#9CA3AF; margin-top:3px; }
|
||||||
|
.tb-l.on { color:#2563EB; }
|
||||||
|
.tb-b { position:absolute; top:2px; left:50%; margin-left:2px; }
|
||||||
|
.tb-badge-wrap { position:relative; }
|
||||||
|
.tb-badge-wrap .b { position:absolute; top:0; right:0; transform:translate(50%,-50%); }
|
||||||
|
|
||||||
|
/* 顶部 tabs 预览 */
|
||||||
|
.tabs { display:flex; flex-direction:row; background:#fff; border-radius:9px 9px 0 0; border-bottom:1px solid #F3F4F6; }
|
||||||
|
.tab-i { flex:1; display:flex; align-items:center; justify-content:center; height:44px; position:relative; }
|
||||||
|
.tab-t { font-size:14px; color:#6B7280; }
|
||||||
|
.tab-t.on { color:#2563EB; font-weight:bold; }
|
||||||
|
.tab-b { position:absolute; top:9px; left:50%; margin-left:16px; }
|
||||||
|
.tab-b .b { position:absolute; top:0; left:0; transform:translate(0,-50%); }
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>角标尺寸统一 · 效果对照</h1>
|
||||||
|
<div class="sub">基准 = 「我的」页 · 消息中心 item 右侧角标(高 36rpx / 圆角 18rpx / 字号 22.4rpx)· 375 逻辑宽按 1rpx = 0.5px 渲染</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>① 角标特写:改前 / 改后 / 基准</h2>
|
||||||
|
<div class="cmp">
|
||||||
|
<div class="cmp-item">
|
||||||
|
<div class="stage">
|
||||||
|
<div style="position:relative">
|
||||||
|
<div class="xBadge-countAndLabel" style="background:#DC2626;left:0;top:0;position:relative">
|
||||||
|
<span class="xBadge-countAndLabelText" style="color:#fff;font-size:9px">4</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lbl">改前 · xBadge 默认<span class="tag tag-bad">偏小</span><br>26 × 13.5 px</div>
|
||||||
|
</div>
|
||||||
|
<div class="cmp-item">
|
||||||
|
<div class="stage">
|
||||||
|
<div style="position:relative">
|
||||||
|
<div class="xBadge-countAndLabel fixed" style="background:#DC2626;left:0;top:0;position:relative">
|
||||||
|
<span class="xBadge-countAndLabelText" style="color:#fff">4</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lbl">改后 · 组件库角标<span class="tag tag-ok">已统一</span><br>28 × 18 px</div>
|
||||||
|
</div>
|
||||||
|
<div class="cmp-item">
|
||||||
|
<div class="stage"><div class="hr-badge"><span class="hr-badge-t">4</span></div></div>
|
||||||
|
<div class="lbl">基准 · 列表行自写角标<br>28 × 18 px</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="padding:0;overflow:hidden">
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab-i"><span class="tab-t on">进行中</span>
|
||||||
|
<div class="tab-b"><div class="xBadge-countAndLabel fixed b" style="background:#6B7280"><span class="xBadge-countAndLabelText" style="color:#fff">2</span></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-i"><span class="tab-t">待接收</span>
|
||||||
|
<div class="tab-b"><div class="xBadge-countAndLabel fixed b" style="background:#DC2626"><span class="xBadge-countAndLabelText" style="color:#fff">4</span></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-i"><span class="tab-t">历史</span>
|
||||||
|
<div class="tab-b"><div class="xBadge-countAndLabel fixed b" style="background:#6B7280"><span class="xBadge-countAndLabelText" style="color:#fff">7</span></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:10px 14px 4px;font-size:12px;color:#374151">② 事件列表页 · 顶部切换栏(x-tabs 角标)<span class="tag tag-ok">已统一</span></div>
|
||||||
|
<div class="tb" style="border-radius:0">
|
||||||
|
<div class="tb-i"><div class="tb-ic"></div><span class="tb-l">首页</span></div>
|
||||||
|
<div class="tb-i">
|
||||||
|
<div class="tb-badge-wrap"><div class="tb-ic on"></div>
|
||||||
|
<div class="xBadge-countAndLabel fixed b" style="background:#DC2626"><span class="xBadge-countAndLabelText" style="color:#fff">4</span></div>
|
||||||
|
</div>
|
||||||
|
<span class="tb-l on">事件</span>
|
||||||
|
</div>
|
||||||
|
<div class="tb-i"><div class="tb-ic"></div><span class="tb-l">我的</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:8px 14px 12px;font-size:12px;color:#374151">③ 事件页 · 底部 tabbar(x-tabbar 角标)<span class="tag tag-ok">已统一</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>④ 与基准同源的两处列表行角标</h2>
|
||||||
|
<div class="lrow">
|
||||||
|
<div class="lic">✉</div>
|
||||||
|
<div class="grow"><div class="lt">消息中心</div><div class="ls">4 条未读</div></div>
|
||||||
|
<div class="hr-badge"><span class="hr-badge-t">4</span></div>
|
||||||
|
<span class="lval">›</span>
|
||||||
|
</div>
|
||||||
|
<div class="lrow">
|
||||||
|
<div class="lic">⚠</div>
|
||||||
|
<div class="grow"><div class="lt">事件告警</div><div class="ls">新的派单会推送给你</div></div>
|
||||||
|
<div class="hr-badge"><span class="hr-badge-t">4</span></div>
|
||||||
|
<span class="lval">›</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
|
After Width: | Height: | Size: 104 KiB |
@@ -0,0 +1,291 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>批量转运 · 点击开启相机 / 接收医院 hr-sheet 预览</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
background: #EEF2F7; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
color: #1F2937; padding: 28px 24px 40px;
|
||||||
|
}
|
||||||
|
h1 { font-size: 18px; font-weight: 700; margin-bottom: 6px; }
|
||||||
|
.sub { font-size: 13px; color: #6B7280; margin-bottom: 22px; }
|
||||||
|
.row { display: flex; gap: 28px; align-items: flex-start; flex-wrap: wrap; }
|
||||||
|
.col { display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.cap { font-size: 12.5px; color: #475569; font-weight: 600; }
|
||||||
|
.cap span { color: #2563EB; }
|
||||||
|
.phone {
|
||||||
|
width: 375px; height: 812px; background: #F3F4F6; border-radius: 26px;
|
||||||
|
overflow: hidden; position: relative; box-shadow: 0 14px 40px rgba(15,23,42,.16);
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
height: 88px; background: #fff; display: flex; align-items: flex-end;
|
||||||
|
padding: 0 16px 12px; position: relative; flex: none;
|
||||||
|
}
|
||||||
|
.nav-t { position: absolute; left: 0; right: 0; bottom: 12px; text-align: center; font-size: 16px; font-weight: 700; }
|
||||||
|
.nav-r { margin-left: auto; font-size: 14.6px; color: #6B7280; }
|
||||||
|
|
||||||
|
/* ---------- 顶部取景区 ---------- */
|
||||||
|
.cm { background: #0B1220; padding: 8px 14px 9px; flex: none; }
|
||||||
|
.cm-view {
|
||||||
|
height: 108px; border-radius: 14px; overflow: hidden; position: relative;
|
||||||
|
background: #10192A; display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.cm-idle { display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||||||
|
.cm-idle svg { display: block; }
|
||||||
|
.cm-idle-t { font-size: 14px; font-weight: 700; color: #E5E7EB; margin-top: 7px; }
|
||||||
|
.cm-idle-s { font-size: 11.2px; color: rgba(148,163,184,.92); margin-top: 4px; }
|
||||||
|
.cm-cnt {
|
||||||
|
position: absolute; left: 10px; top: 10px; background: rgba(17,24,39,.62);
|
||||||
|
border-radius: 7px; padding: 3px 8px; font-size: 11.8px; font-weight: 700; color: #fff;
|
||||||
|
}
|
||||||
|
.cm-cnt b { color: #60A5FA; }
|
||||||
|
.cm-frame { width: 86px; height: 86px; border-radius: 16px; background: rgba(255,255,255,.03); position: relative; }
|
||||||
|
.cm-cf { position: absolute; width: 22px; height: 22px; border: 3px solid #60A5FA; border-radius: 7px; }
|
||||||
|
.cm-cf.tl { left: 0; top: 0; border-right-width: 0; border-bottom-width: 0; }
|
||||||
|
.cm-cf.tr { right: 0; top: 0; border-left-width: 0; border-bottom-width: 0; }
|
||||||
|
.cm-cf.bl { left: 0; bottom: 0; border-right-width: 0; border-top-width: 0; }
|
||||||
|
.cm-cf.br { right: 0; bottom: 0; border-left-width: 0; border-top-width: 0; }
|
||||||
|
.cm-laser { position: absolute; left: 7px; right: 7px; top: 60px; height: 2px; border-radius: 2px; background: #93C5FD; }
|
||||||
|
.cm-acts { display: flex; margin-top: 9px; }
|
||||||
|
.cm-act {
|
||||||
|
flex: 1; height: 34px; border-radius: 9px; background: rgba(255,255,255,.10);
|
||||||
|
display: flex; align-items: center; justify-content: center; margin-right: 8px;
|
||||||
|
}
|
||||||
|
.cm-act:last-child { margin-right: 0; }
|
||||||
|
.cm-act.on { background: rgba(16,185,129,.20); }
|
||||||
|
.cm-act-t { font-size: 12.9px; font-weight: 700; color: rgba(255,255,255,.9); margin-left: 5px; }
|
||||||
|
.cm-act-t.on { color: #6EE7B7; }
|
||||||
|
|
||||||
|
/* ---------- 中部记录列表 ---------- */
|
||||||
|
.rec { flex: 1; padding: 8px 14px 0; overflow: hidden; }
|
||||||
|
.rec-h { display: flex; align-items: center; margin-bottom: 7px; }
|
||||||
|
.rec-h-t { font-size: 13.4px; font-weight: 700; }
|
||||||
|
.rec-h-n { font-size: 11.8px; color: #6B7280; margin-left: 6px; }
|
||||||
|
.rec-h-c { margin-left: auto; font-size: 11.8px; font-weight: 700; color: #DC2626; }
|
||||||
|
.rec-i {
|
||||||
|
background: #fff; border-radius: 12px; padding: 9px 12px; margin-bottom: 7px;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
.wb { width: 4px; height: 26px; border-radius: 2px; margin-right: 9px; }
|
||||||
|
.rec-n { font-size: 13.4px; font-weight: 700; margin-right: 6px; }
|
||||||
|
.rec-c { font-size: 10.8px; color: #9CA3AF; margin-top: 2px; }
|
||||||
|
.lv { font-size: 10px; font-weight: 700; color: #fff; padding: 1px 6px; border-radius: 8px; }
|
||||||
|
.rec-x { margin-left: auto; font-size: 15px; color: #D1D5DB; }
|
||||||
|
|
||||||
|
/* ---------- 底部表单 ---------- */
|
||||||
|
.form { background: #fff; border-top: 1px solid #E5E7EB; border-radius: 16px 16px 0 0; padding: 11px 16px 14px; position: relative; flex: none; }
|
||||||
|
.lb { font-size: 12.9px; font-weight: 700; color: #6B7280; margin-bottom: 6px; }
|
||||||
|
.lb.g { margin-top: 9px; }
|
||||||
|
.pick {
|
||||||
|
display: flex; align-items: center; background: #F9FAFB; border: 1px solid #E5E7EB;
|
||||||
|
border-radius: 10px; padding: 10px 12px;
|
||||||
|
}
|
||||||
|
.pick-n { flex: 1; font-size: 14px; font-weight: 700; }
|
||||||
|
.pick-b { font-size: 11.2px; font-weight: 700; color: #059669; margin-right: 6px; }
|
||||||
|
.pick-b.no { color: #DC2626; }
|
||||||
|
.seg { display: flex; background: #E5E7EB; border-radius: 10px; padding: 2px; }
|
||||||
|
.seg-i { flex: 1; height: 34px; display: flex; align-items: center; justify-content: center; border-radius: 8px; }
|
||||||
|
.seg-i.on { background: #fff; }
|
||||||
|
.seg-t { font-size: 14.6px; color: #6B7280; }
|
||||||
|
.seg-t.on { color: #2563EB; font-weight: 700; }
|
||||||
|
.foot { display: flex; padding-right: 70px; }
|
||||||
|
.foot-c { flex: 1; margin-right: 8px; }
|
||||||
|
.foot-c:last-child { margin-right: 0; }
|
||||||
|
.field { background: #F9FAFB; border: 1px solid #E5E7EB; border-radius: 10px; padding: 8px 12px; }
|
||||||
|
.field-l { font-size: 10.6px; color: #9CA3AF; }
|
||||||
|
.field-i { font-size: 13.4px; font-weight: 600; margin-top: 2px; }
|
||||||
|
.send {
|
||||||
|
position: absolute; right: 16px; bottom: 16px; width: 56px; height: 56px; border-radius: 28px;
|
||||||
|
background: linear-gradient(to bottom right, #2563EB, #1D4ED8); display: flex; align-items: center;
|
||||||
|
justify-content: center; box-shadow: 0 6px 18px rgba(37,99,235,.42);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- hr-sheet ---------- */
|
||||||
|
.mask { position: absolute; inset: 0; background: rgba(0,0,0,.45); }
|
||||||
|
.sheet {
|
||||||
|
position: absolute; left: 0; right: 0; bottom: 0; background: #fff;
|
||||||
|
border-radius: 18px 18px 0 0; padding: 14px 16px 20px;
|
||||||
|
}
|
||||||
|
.sheet-h { display: flex; align-items: center; margin-bottom: 10px; }
|
||||||
|
.sheet-h-b { font-size: 16.8px; font-weight: 700; }
|
||||||
|
.sheet-h-x { margin-left: auto; font-size: 18px; color: #9CA3AF; }
|
||||||
|
.opt { display: flex; align-items: center; padding: 13px 0; border-bottom: 1px solid #F3F4F6; }
|
||||||
|
.opt.last { border-bottom: 0; }
|
||||||
|
.opt-txt { flex: 1; display: flex; flex-direction: column; }
|
||||||
|
.opt-n { font-size: 15px; font-weight: 700; color: #1F2937; }
|
||||||
|
.opt-n.on { color: #2563EB; }
|
||||||
|
.opt-n.dim { color: #6B7280; }
|
||||||
|
.opt-m { font-size: 11.2px; color: #9CA3AF; margin-top: 3px; }
|
||||||
|
.opt-b { font-size: 11.2px; font-weight: 700; color: #059669; margin-right: 6px; }
|
||||||
|
.opt-b.no { color: #DC2626; }
|
||||||
|
.tick { color: #2563EB; font-size: 15px; font-weight: 700; }
|
||||||
|
|
||||||
|
.notes { margin-top: 24px; max-width: 780px; }
|
||||||
|
.notes h2 { font-size: 14px; margin-bottom: 8px; }
|
||||||
|
.notes ul { font-size: 13px; color: #475569; line-height: 1.9; padding-left: 18px; }
|
||||||
|
.notes code { background: #E2E8F0; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>批量转运 · 顶部点击开启相机 + 接收医院 hr-sheet 弹层</h1>
|
||||||
|
<div class="sub">设计基准 375 × 812。左:进入页面(相机未启动,接收医院为折叠字段);右:点击「接收医院」整行后唤起底部弹层。</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- ============ 状态 A:相机未开启 ============ -->
|
||||||
|
<div class="col">
|
||||||
|
<div class="cap">状态 A · 初始态 — 取景区显示 <span>「点击开启摄像头」</span>,不再自动占有摄像头</div>
|
||||||
|
<div class="phone">
|
||||||
|
<div class="nav"><span class="nav-r">历史</span><span class="nav-t">批量转运</span></div>
|
||||||
|
|
||||||
|
<div class="cm">
|
||||||
|
<div class="cm-view">
|
||||||
|
<div class="cm-idle">
|
||||||
|
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="#60A5FA" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M9 3h6l1.5 2H20a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h3.5L9 3z"/>
|
||||||
|
<circle cx="12" cy="12" r="3.4"/>
|
||||||
|
</svg>
|
||||||
|
<div class="cm-idle-t">点击开启摄像头</div>
|
||||||
|
<div class="cm-idle-s">对准伤员腕带二维码,自动连续识别</div>
|
||||||
|
</div>
|
||||||
|
<div class="cm-cnt">已识别 <b>4</b> 人</div>
|
||||||
|
</div>
|
||||||
|
<div class="cm-acts">
|
||||||
|
<div class="cm-act">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M10 8.5l6 3.5-6 3.5z" fill="#fff" stroke="none"/></svg>
|
||||||
|
<span class="cm-act-t">开启扫码</span>
|
||||||
|
</div>
|
||||||
|
<div class="cm-act">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 9h.01M11 9h.01M15 9h.01M7 13h.01M11 13h.01M15 13h.01M7 17h10"/></svg>
|
||||||
|
<span class="cm-act-t">手动输入编号</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rec">
|
||||||
|
<div class="rec-h">
|
||||||
|
<span class="rec-h-t">已扫码伤员</span><span class="rec-h-n">4 / 12</span>
|
||||||
|
<span class="rec-h-c">清空</span>
|
||||||
|
</div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#DC2626"></div><div><div style="display:flex;align-items:center"><span class="rec-n">王建国</span><span class="lv" style="background:#DC2626">危重</span></div><div class="rec-c">男 · 52 岁 · TRIAGE-20260922-0001</div></div><span class="rec-x">✕</span></div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#F59E0B"></div><div><div style="display:flex;align-items:center"><span class="rec-n">李华</span><span class="lv" style="background:#F59E0B">重伤</span></div><div class="rec-c">男 · 35 岁 · TRIAGE-20260922-0003</div></div><span class="rec-x">✕</span></div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#10B981"></div><div><div style="display:flex;align-items:center"><span class="rec-n">陈小梅</span><span class="lv" style="background:#10B981">轻伤</span></div><div class="rec-c">女 · 28 岁 · TRIAGE-20260922-0006</div></div><span class="rec-x">✕</span></div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#F59E0B"></div><div><div style="display:flex;align-items:center"><span class="rec-n">赵国强</span><span class="lv" style="background:#F59E0B">重伤</span></div><div class="rec-c">男 · 41 岁 · TRIAGE-20260922-0008</div></div><span class="rec-x">✕</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
<div class="lb">接收医院</div>
|
||||||
|
<div class="pick">
|
||||||
|
<span class="pick-n">协和医院 · 急诊创伤中心</span>
|
||||||
|
<span class="pick-b">床位 2</span>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="lb g">转运方式</div>
|
||||||
|
<div class="seg">
|
||||||
|
<div class="seg-i on"><span class="seg-t on">救护车 🚑</span></div>
|
||||||
|
<div class="seg-i"><span class="seg-t">直升机 🚁</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="lb g">随车联系人</div>
|
||||||
|
<div class="foot">
|
||||||
|
<div class="foot-c"><div class="field"><div class="field-l">姓名</div><div class="field-i">刘伟</div></div></div>
|
||||||
|
<div class="foot-c"><div class="field"><div class="field-l">联系电话</div><div class="field-i">13800006621</div></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="send"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ 状态 B:弹层展开 ============ -->
|
||||||
|
<div class="col">
|
||||||
|
<div class="cap">状态 B · 点击「接收医院」— 唤起 <span>hr-sheet</span>(名称 + 距离,满员置灰不可选)</div>
|
||||||
|
<div class="phone">
|
||||||
|
<div class="nav"><span class="nav-r">历史</span><span class="nav-t">批量转运</span></div>
|
||||||
|
|
||||||
|
<div class="cm">
|
||||||
|
<div class="cm-view">
|
||||||
|
<div class="cm-frame">
|
||||||
|
<div class="cm-cf tl"></div><div class="cm-cf tr"></div><div class="cm-cf bl"></div><div class="cm-cf br"></div>
|
||||||
|
<div class="cm-laser"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cm-cnt">已识别 <b>6</b> 人</div>
|
||||||
|
</div>
|
||||||
|
<div class="cm-acts">
|
||||||
|
<div class="cm-act on">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#6EE7B7" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2"/><path d="M4 12h16"/></svg>
|
||||||
|
<span class="cm-act-t on">连续扫码中</span>
|
||||||
|
</div>
|
||||||
|
<div class="cm-act">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 9h.01M11 9h.01M15 9h.01M7 13h.01M11 13h.01M15 13h.01M7 17h10"/></svg>
|
||||||
|
<span class="cm-act-t">手动输入编号</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rec">
|
||||||
|
<div class="rec-h">
|
||||||
|
<span class="rec-h-t">已扫码伤员</span><span class="rec-h-n">6 / 12</span>
|
||||||
|
<span class="rec-h-c">清空</span>
|
||||||
|
</div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#DC2626"></div><div><div style="display:flex;align-items:center"><span class="rec-n">王建国</span><span class="lv" style="background:#DC2626">危重</span></div><div class="rec-c">男 · 52 岁 · TRIAGE-20260922-0001</div></div><span class="rec-x">✕</span></div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#F59E0B"></div><div><div style="display:flex;align-items:center"><span class="rec-n">李华</span><span class="lv" style="background:#F59E0B">重伤</span></div><div class="rec-c">男 · 35 岁 · TRIAGE-20260922-0003</div></div><span class="rec-x">✕</span></div>
|
||||||
|
<div class="rec-i"><div class="wb" style="background:#10B981"></div><div><div style="display:flex;align-items:center"><span class="rec-n">陈小梅</span><span class="lv" style="background:#10B981">轻伤</span></div><div class="rec-c">女 · 28 岁 · TRIAGE-20260922-0006</div></div><span class="rec-x">✕</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form">
|
||||||
|
<div class="lb">接收医院</div>
|
||||||
|
<div class="pick">
|
||||||
|
<span class="pick-n">协和医院 · 急诊创伤中心</span>
|
||||||
|
<span class="pick-b">床位 2</span>
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="lb g">转运方式</div>
|
||||||
|
<div class="seg">
|
||||||
|
<div class="seg-i on"><span class="seg-t on">救护车 🚑</span></div>
|
||||||
|
<div class="seg-i"><span class="seg-t">直升机 🚁</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="lb g">随车联系人</div>
|
||||||
|
<div class="foot">
|
||||||
|
<div class="foot-c"><div class="field"><div class="field-l">姓名</div><div class="field-i">刘伟</div></div></div>
|
||||||
|
<div class="foot-c"><div class="field"><div class="field-l">联系电话</div><div class="field-i">13800006621</div></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="send"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 弹层 -->
|
||||||
|
<div class="mask"></div>
|
||||||
|
<div class="sheet">
|
||||||
|
<div class="sheet-h">
|
||||||
|
<span class="sheet-h-b">选择接收医院</span>
|
||||||
|
<span class="sheet-h-x">✕</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt">
|
||||||
|
<div class="opt-txt"><span class="opt-n on">协和医院 · 急诊创伤中心</span><span class="opt-m">距现场 6.2 km · 约 14 分钟</span></div>
|
||||||
|
<span class="opt-b">床位 2</span><span class="tick">✓</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt">
|
||||||
|
<div class="opt-txt"><span class="opt-n">市第一人民医院 · 急诊</span><span class="opt-m">距现场 8.9 km · 约 21 分钟</span></div>
|
||||||
|
<span class="opt-b">床位 5</span>
|
||||||
|
</div>
|
||||||
|
<div class="opt last">
|
||||||
|
<div class="opt-txt"><span class="opt-n dim">同仁医院 · 急诊</span><span class="opt-m">距现场 4.1 km · 但急诊已满</span></div>
|
||||||
|
<span class="opt-b no">满员 ✕</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="notes">
|
||||||
|
<h2>改动说明</h2>
|
||||||
|
<ul>
|
||||||
|
<li><code>hr-scan-cam</code>:<code>cameraOn</code> 默认 <code>false</code>,取景区新增 <code>@click="openCamera"</code>;未开启时展示居中引导「点击开启摄像头」,不再进页面即占用摄像头。</li>
|
||||||
|
<li>底部按钮文案由「已暂停扫码」改为「开启扫码」(图标 <code>play-circle-line</code>),点击即可开启/暂停。</li>
|
||||||
|
<li>扫描线定时器改为随相机开关启停(<code>openCamera</code> / <code>toggleCamera</code> 内 <code>startLaser</code> / <code>stopLaser</code>),不再 <code>onMounted</code> 空跑。</li>
|
||||||
|
<li><code>op-hos</code> 内联单选列表 → 折叠字段「医院名 + 床位数 + ⌄」,点击整行唤起 <code>hr-mask</code> + <code>hr-sheet</code>(与 伤员查询 / 检伤建档 同一套类)。</li>
|
||||||
|
<li>弹层选项补上 <code>hospitalMeta</code>(距现场km · 约N分钟),满员医院名置灰、床位红字,点击弹 toast 且不关闭弹层。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 151 KiB |
@@ -0,0 +1,271 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>检伤建档 · 2D 人体图鉴 & 伤情抽屉(静态预览)</title>
|
||||||
|
<!--
|
||||||
|
目的:无构建环境下预览 create.uvue 的伤情标记区与 xDrawer 表单。
|
||||||
|
尺寸换算:rpx → px 按 0.5(375 逻辑宽),即 1:1 还原真机视觉。
|
||||||
|
数据/公式与 utils/injury.uts、components/hr-bodymap 一一对应。
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; background: #F3F4F6; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; color: #1F2937; }
|
||||||
|
.stage { width: 375px; padding: 12px 11px 24px; }
|
||||||
|
.card { background: #fff; border-radius: 11px; padding: 12px; margin-bottom: 14px; }
|
||||||
|
.sec { display: flex; flex-direction: row; align-items: center; font-size: 14px; font-weight: bold; margin: 4px 0 8px; }
|
||||||
|
.sec-bar { width: 3px; height: 13px; border-radius: 2px; background: #2563EB; margin-right: 6px; }
|
||||||
|
.sec-more { margin-left: auto; font-size: 11.5px; color: #6B7280; font-weight: normal; }
|
||||||
|
|
||||||
|
/* ---------- 2D 人体图鉴(几何值 = hr-design.css / hr-bodymap 的一半) ---------- */
|
||||||
|
.bm-row { display: flex; flex-direction: row; }
|
||||||
|
.bm-col { flex: 1; background: #FAFBFC; border: 1px dashed #E5E7EB; border-radius: 12px; padding: 10px 6px 8px; align-items: center; position: relative; margin-right: 10px; display: flex; flex-direction: column; }
|
||||||
|
.bm-col-last { margin-right: 0; }
|
||||||
|
.bm-figure { width: 96px; height: 184px; position: relative; }
|
||||||
|
.bm-head { position: absolute; left: 33px; top: 0; width: 30px; height: 30px; border-radius: 15px; border: 2.5px solid #CBD5E1; }
|
||||||
|
.bm-torso { position: absolute; left: 26px; top: 33px; width: 44px; height: 62px; border-radius: 14px; border: 2.5px solid #CBD5E1; }
|
||||||
|
.bm-arm-l { position: absolute; left: 8px; top: 36px; width: 12px; height: 58px; border-radius: 6px; border: 2.5px solid #CBD5E1; }
|
||||||
|
.bm-arm-r { position: absolute; left: 76px; top: 36px; width: 12px; height: 58px; border-radius: 6px; border: 2.5px solid #CBD5E1; }
|
||||||
|
.bm-leg-l { position: absolute; left: 28px; top: 96px; width: 13px; height: 84px; border-radius: 7px; border: 2.5px solid #CBD5E1; }
|
||||||
|
.bm-leg-r { position: absolute; left: 55px; top: 96px; width: 13px; height: 84px; border-radius: 7px; border: 2.5px solid #CBD5E1; }
|
||||||
|
.bmp-spine { position: absolute; left: 47px; top: 39px; width: 2px; height: 54px; border-radius: 1px; background: #E2E8F0; }
|
||||||
|
.bmp-hit { position: absolute; border-radius: 5px; border: 1px dashed #EDF0F4; }
|
||||||
|
.bmp-hit-on { border: 1px solid #BFDBFE; background: #EFF6FF; }
|
||||||
|
.bmp-mark { position: absolute; width: 38px; height: 38px; margin-left: -19px; margin-top: -19px; display: flex; flex-direction: row; align-items: center; justify-content: center; }
|
||||||
|
.bmp-bar { position: absolute; top: 13px; height: 12px; border-radius: 6px; opacity: .8; }
|
||||||
|
.bmp-dot { width: 22px; height: 22px; border-radius: 11px; border: 1.5px solid #fff; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.bmp-dot-t { font-size: 11.5px; font-weight: bold; color: #fff; }
|
||||||
|
.bm-tag { font-size: 11.8px; color: #9CA3AF; margin-top: 7px; }
|
||||||
|
.c-sum { display: flex; align-items: center; justify-content: space-between; margin-top: 11px; font-size: 11.8px; }
|
||||||
|
.c-sum-t { color: #6B7280; }
|
||||||
|
.c-sum-json { color: #2563EB; }
|
||||||
|
.c-mklist { margin-top: 8px; }
|
||||||
|
.c-mk { display: flex; align-items: center; background: #F9FAFB; border: 1px solid #E5E7EB; border-radius: 11px; padding: 10px 12px; margin-bottom: 8px; }
|
||||||
|
.c-mk-main { display: flex; align-items: center; flex: 1; }
|
||||||
|
.c-mk-no { width: 22px; height: 22px; border-radius: 11px; display: flex; align-items: center; justify-content: center; margin-right: 10px; flex: 0 0 22px; }
|
||||||
|
.c-mk-no-t { font-size: 12.3px; font-weight: bold; color: #fff; }
|
||||||
|
.c-mk-t { font-size: 14px; font-weight: bold; color: #1F2937; }
|
||||||
|
.c-mk-s { font-size: 11.8px; color: #9CA3AF; margin-top: 2px; }
|
||||||
|
.c-mk-side { border-radius: 5px; padding: 2px 7px; margin-left: 6px; }
|
||||||
|
.c-mk-side-t { font-size: 10.6px; color: #6B7280; }
|
||||||
|
.c-mk-del { width: 20px; height: 20px; border-radius: 10px; background: #F3F4F6; display: flex; align-items: center; justify-content: center; margin-left: 8px; color: #6B7280; font-size: 11px; }
|
||||||
|
.c-addmark { display: flex; align-items: center; justify-content: center; height: 40px; border: 1.5px dashed #C7CDD6; border-radius: 11px; background: #fff; font-size: 12.9px; color: #6B7280; }
|
||||||
|
|
||||||
|
/* ---------- 伤情录入抽屉 ---------- */
|
||||||
|
.drawer { background: #fff; border-radius: 12px 12px 0 0; padding: 0 16px; }
|
||||||
|
.d-head { display: flex; align-items: center; width: 100%; padding-top: 3px; margin-bottom: 4px; }
|
||||||
|
.d-head-dot { width: 22px; height: 22px; border-radius: 11px; display: flex; align-items: center; justify-content: center; margin-right: 8px; flex: 0 0 22px; }
|
||||||
|
.d-head-dot-t { font-size: 11.5px; font-weight: bold; color: #fff; }
|
||||||
|
.d-head-t { font-size: 15px; font-weight: bold; }
|
||||||
|
.d-head-s { font-size: 10.6px; color: #9CA3AF; margin-top: 2px; }
|
||||||
|
.d-blk { margin-bottom: 12px; }
|
||||||
|
.d-blk-h { display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
.d-fl { font-size: 12.9px; font-weight: bold; }
|
||||||
|
.d-regen { font-size: 11.4px; color: #2563EB; }
|
||||||
|
.d-chips { display: flex; flex-wrap: wrap; margin-top: 7px; }
|
||||||
|
.d-chip { display: flex; align-items: center; border-radius: 7px; padding: 6px 10px; margin-right: 7px; margin-bottom: 7px; font-size: 12px; }
|
||||||
|
.d-chip-z { border: 1px solid #E5E7EB; background: #fff; color: #4B5563; }
|
||||||
|
.d-chip-on { border: 1px solid #2563EB; background: #2563EB; color: #fff; font-weight: bold; }
|
||||||
|
.d-chip-dot { width: 8px; height: 8px; border-radius: 4px; margin-right: 5px; }
|
||||||
|
.d-side { display: flex; border: 1px solid #E5E7EB; border-radius: 7px; overflow: hidden; }
|
||||||
|
.d-side-i { padding: 4px 11px; font-size: 12px; }
|
||||||
|
.d-rot { display: flex; align-items: center; }
|
||||||
|
.d-rot-b { width: 40px; height: 26px; border: 1px solid #E5E7EB; border-radius: 7px; background: #fff; display: flex; align-items: center; justify-content: center; font-size: 11.4px; color: #4B5563; }
|
||||||
|
.d-rot-v { width: 42px; font-size: 12.9px; font-weight: bold; color: #2563EB; text-align: center; }
|
||||||
|
.d-prev { width: 100%; height: 66px; background: #FAFBFC; border: 1px dashed #E5E7EB; border-radius: 9px; position: relative; margin-top: 8px; }
|
||||||
|
.d-prev-bar { position: absolute; top: 27px; height: 12px; border-radius: 6px; opacity: .85; }
|
||||||
|
.d-prev-dot { position: absolute; left: 50%; top: 50%; width: 22px; height: 22px; border-radius: 11px; margin-left: -11px; margin-top: -11px; border: 1.5px solid #fff; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.d-input { width: 100%; height: 36px; background: #F9FAFB; border: 1px solid #E5E7EB; border-radius: 8px; padding: 0 10px; font-size: 13.2px; color: #1F2937; }
|
||||||
|
.d-ta { width: 100%; height: 88px; background: #F9FAFB; border: 1px solid #E5E7EB; border-radius: 9px; padding: 9px 10px; font-size: 13.2px; color: #1F2937; margin-top: 7px; }
|
||||||
|
.d-foot { display: flex; align-items: center; padding: 10px 0 14px; }
|
||||||
|
.d-foot-cancel { flex: 0 0 110px; margin-right: 9px; }
|
||||||
|
.d-btn { height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold; }
|
||||||
|
.d-b-ghost { background: #fff; border: 1px solid #E5E7EB; color: #1F2937; }
|
||||||
|
.d-b-pri { background: #2563EB; color: #fff; flex: 1; }
|
||||||
|
.lbl { font-size: 11px; color: #9CA3AF; margin: 10px 0 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="stage">
|
||||||
|
|
||||||
|
<div class="sec"><view class="sec-bar"></view>伤情标记(2D 人体图鉴)<span class="sec-more">+ 手动添加</span></div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="bm-row">
|
||||||
|
<div class="bm-col">
|
||||||
|
<div class="bm-figure" id="figFront"></div>
|
||||||
|
<div class="bm-tag">正面 · 点部位即标记</div>
|
||||||
|
</div>
|
||||||
|
<div class="bm-col bm-col-last">
|
||||||
|
<div class="bm-figure" id="figBack"></div>
|
||||||
|
<div class="bm-tag">背面 · 点部位即标记</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="c-sum">
|
||||||
|
<span class="c-sum-t" id="sum"></span>
|
||||||
|
<span class="c-sum-json">查看 JSON ›</span>
|
||||||
|
</div>
|
||||||
|
<div class="c-mklist" id="mklist"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec"><view class="sec-bar"></view>伤情录入抽屉(xDrawer · 底部弹出)</div>
|
||||||
|
<div class="drawer">
|
||||||
|
<div class="d-head">
|
||||||
|
<div class="d-head-dot" id="dDot"><span class="d-head-dot-t" id="dNo">4</span></div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="d-head-t">新增伤情</div>
|
||||||
|
<div class="d-head-s">部位已自动填入,其余按默认值预填,可直接保存</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="dBody"></div>
|
||||||
|
<div class="d-foot">
|
||||||
|
<div class="d-btn d-b-ghost d-foot-cancel">取消</div>
|
||||||
|
<div class="d-btn d-b-pri">保存标记</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lbl">※ 预览为 create.uvue 的静态还原:绿色虚线框是热区(实际运行不可见,这里画出来核对是否覆盖人体轮廓)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ============ 与 utils/injury.uts 保持一致的数据与公式 ============ */
|
||||||
|
function buildParts(side) {
|
||||||
|
const flip = side === 'front';
|
||||||
|
const armViewLeft = flip ? '右上肢' : '左上肢';
|
||||||
|
const armViewRight = flip ? '左上肢' : '右上肢';
|
||||||
|
const legViewLeft = flip ? '右下肢' : '左下肢';
|
||||||
|
const legViewRight = flip ? '左下肢' : '右下肢';
|
||||||
|
return [
|
||||||
|
{ id: 'head', name: '头部', x: 33, y: 0, w: 34, h: 15, mx: 50, my: 8 },
|
||||||
|
{ id: 'neck', name: '颈部', x: 38, y: 15, w: 24, h: 8, mx: 50, my: 19 },
|
||||||
|
{ id: 'chest', name: '胸部', x: 26, y: 23, w: 48, h: 15, mx: 50, my: 30 },
|
||||||
|
{ id: 'abdomen', name: '腹部', x: 26, y: 38, w: 48, h: 10, mx: 50, my: 43 },
|
||||||
|
{ id: 'pelvis', name: '骨盆', x: 26, y: 48, w: 48, h: 10, mx: 50, my: 53 },
|
||||||
|
{ id: 'armVL', name: armViewLeft, x: 5, y: 18, w: 19, h: 36, mx: 14, my: 33 },
|
||||||
|
{ id: 'armVR', name: armViewRight, x: 76, y: 18, w: 19, h: 36, mx: 86, my: 33 },
|
||||||
|
{ id: 'legVL', name: legViewLeft, x: 26, y: 58, w: 19, h: 42, mx: 35, my: 79 },
|
||||||
|
{ id: 'legVR', name: legViewRight, x: 55, y: 58, w: 19, h: 42, mx: 65, my: 79 }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const EDGE = { 活动性出血: ['#DC2626', '#FCA5A5'], 渗血: ['#F59E0B', '#FCD34D'], 已止血: ['#10B981', '#6EE7B7'], 无出血: ['#9CA3AF', '#E5E7EB'] };
|
||||||
|
function sizeBarWidth(size) { // 与 injury.uts 同算法
|
||||||
|
const m = /^(\d+)/.exec(size);
|
||||||
|
if (!m) return 56;
|
||||||
|
let w = 40 + parseInt(m[1], 10) * 9;
|
||||||
|
return Math.round(Math.max(44, Math.min(132, w)));
|
||||||
|
}
|
||||||
|
function defaultDesc(part, nature, size, bleeding) {
|
||||||
|
const tail = { 开放性外伤: '创缘不整,未见异物', 裂伤: '创缘整齐,深及皮下', 擦伤: '表皮剥脱,创面污染',
|
||||||
|
挫伤: '局部肿胀压痛,无开放伤口', 刺伤: '创口小、伤道深,需探查异物', 切割伤: '创缘整齐,注意肌腱神经损伤',
|
||||||
|
烧伤: '创面潮红,需记录面积与深度', 挤压伤: '肢体肿胀张力高,警惕骨筋膜室综合征', 闭合性骨折: '畸形、反常活动、骨擦感',
|
||||||
|
开放性骨折: '骨折端外露,已覆盖并固定', 关节脱位: '关节畸形、弹性固定、活动受限', 贯通伤: '可见入口与出口,警惕深部脏器损伤',
|
||||||
|
动物咬伤: '创面污染重,需清创并评估狂犬病暴露' }[nature] || '待补充伤情描述';
|
||||||
|
return part + ' ' + nature + (size ? '(' + size + ')' : '') + ',' + bleeding + ',' + tail + '。';
|
||||||
|
}
|
||||||
|
/* 与页面示例数据一致(demoMark 三处) */
|
||||||
|
let marks = [
|
||||||
|
{ id: 'd1', no: 1, side: 'front', partId: 'head', partName: '头部', rot: 0, nature: '开放性外伤', size: '4×2cm', bleeding: '活动性出血' },
|
||||||
|
{ id: 'd2', no: 2, side: 'front', partId: 'armVL', partName: '右上肢', rot: 90, nature: '闭合性骨折', size: '5×3cm', bleeding: '已止血' },
|
||||||
|
{ id: 'd3', no: 3, side: 'back', partId: 'legVL', partName: '左下肢', rot: 45, nature: '挤压伤', size: '8×4cm', bleeding: '渗血' }
|
||||||
|
];
|
||||||
|
const partsOf = (s) => buildParts(s);
|
||||||
|
marks.forEach(m => {
|
||||||
|
const p = partsOf(m.side).find(x => x.id === m.partId);
|
||||||
|
m.x = p.mx; m.y = p.my;
|
||||||
|
m.desc = defaultDesc(m.partName, m.nature, m.size, m.bleeding);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ============ 渲染 ============ */
|
||||||
|
const SKELETON = '<div class="bm-head"></div><div class="bm-torso"></div><div class="bm-arm-l"></div><div class="bm-arm-r"></div><div class="bm-leg-l"></div><div class="bm-leg-r"></div>';
|
||||||
|
function renderFigure(el, side, showHit) {
|
||||||
|
const parts = partsOf(side);
|
||||||
|
const ms = marks.filter(m => m.side === side);
|
||||||
|
let h = SKELETON;
|
||||||
|
if (side === 'back') h += '<div class="bmp-spine"></div>';
|
||||||
|
if (showHit) {
|
||||||
|
parts.forEach(p => {
|
||||||
|
const on = ms.some(m => m.partId === p.id);
|
||||||
|
h += `<div class="bmp-hit${on ? ' bmp-hit-on' : ''}" title="${p.name}" style="left:${p.x}%;top:${p.y}%;width:${p.w}%;height:${p.h}%;border-color:${on ? '' : '#22C55E'};border-style:${on ? 'solid' : 'dashed'};"></div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ms.forEach(m => {
|
||||||
|
const w = sizeBarWidth(m.size), half = Math.round(w / 2);
|
||||||
|
const [dot, bar] = EDGE[m.bleeding];
|
||||||
|
h += `<div class="bmp-mark" style="left:${m.x}%;top:${m.y}%;">
|
||||||
|
<div class="bmp-bar" style="width:${w / 2}px;margin-left:-${half / 2}px;left:50%;background:${bar};transform:rotate(${m.rot}deg)"></div>
|
||||||
|
<div class="bmp-dot" style="background:${dot}"><span class="bmp-dot-t">${m.no}</span></div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
el.innerHTML = h;
|
||||||
|
}
|
||||||
|
renderFigure(document.getElementById('figFront'), 'front', true);
|
||||||
|
renderFigure(document.getElementById('figBack'), 'back', true);
|
||||||
|
|
||||||
|
document.getElementById('sum').textContent =
|
||||||
|
'共 ' + marks.length + ' 处标记,其中 ' + marks.filter(m => m.bleeding === '活动性出血').length + ' 处活动性出血';
|
||||||
|
|
||||||
|
document.getElementById('mklist').innerHTML = marks.map(m => `
|
||||||
|
<div class="c-mk">
|
||||||
|
<div class="c-mk-main">
|
||||||
|
<div class="c-mk-no" style="background:${EDGE[m.bleeding][0]}"><span class="c-mk-no-t">${m.no}</span></div>
|
||||||
|
<div style="flex:1">
|
||||||
|
<div class="c-mk-t">${m.partName} · ${m.nature}</div>
|
||||||
|
<div class="c-mk-s">${m.size} · ${m.bleeding} · 走向 ${m.rot}°</div>
|
||||||
|
</div>
|
||||||
|
<div class="c-mk-side" style="background:${m.side === 'back' ? '#F3F4F6' : '#EFF6FF'}"><span class="c-mk-side-t">${m.side === 'back' ? '背面' : '正面'}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="c-mk-del">✕</div>
|
||||||
|
</div>`).join('') + `
|
||||||
|
<div class="c-addmark">+ 点人体图部位快速添加,或点此手动添加</div>`;
|
||||||
|
|
||||||
|
/* ---- 抽屉:模拟「点了正面图左臂(伤员左上肢)」后的自动填充态 ---- */
|
||||||
|
const dSide = 'front', dPartId = 'armVR', dNature = '开放性外伤', dBleeding = '渗血', dSize = '4×2cm', dRot = 135;
|
||||||
|
const partName = partsOf(dSide).find(p => p.id === dPartId).name;
|
||||||
|
const chip = (t, on, dot) => `<span class="d-chip ${on ? 'd-chip-on' : 'd-chip-z'}">${dot ? `<i class="d-chip-dot" style="background:${dot}"></i>` : ''}${t}</span>`;
|
||||||
|
const natures = ['开放性外伤', '裂伤', '擦伤', '挫伤', '刺伤', '切割伤', '烧伤', '挤压伤', '闭合性骨折', '开放性骨折', '关节脱位', '贯通伤', '动物咬伤'];
|
||||||
|
const bleedings = ['活动性出血', '渗血', '已止血', '无出血'];
|
||||||
|
const sizes = ['1×1cm', '2×2cm', '4×2cm', '5×3cm', '8×4cm', '>10cm'];
|
||||||
|
|
||||||
|
const bw = sizeBarWidth(dSize), bhalf = Math.round(bw / 2);
|
||||||
|
document.getElementById('dDot').style.background = EDGE[dBleeding][0];
|
||||||
|
document.getElementById('dBody').innerHTML = `
|
||||||
|
<div class="d-blk">
|
||||||
|
<div class="d-blk-h">
|
||||||
|
<span class="d-fl">部位</span>
|
||||||
|
<span class="d-side">
|
||||||
|
<span class="d-side-i" style="background:#2563EB;color:#fff">正面</span>
|
||||||
|
<span class="d-side-i" style="background:#fff;color:#6B7280">背面</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-chips">${partsOf(dSide).map(p => chip(p.name, p.id === dPartId)).join('')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-blk">
|
||||||
|
<span class="d-fl">伤口性质</span>
|
||||||
|
<div class="d-chips">${natures.map(n => chip(n, n === dNature)).join('')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-blk">
|
||||||
|
<span class="d-fl">出血情况(决定标记颜色)</span>
|
||||||
|
<div class="d-chips">${bleedings.map(b => chip(b, b === dBleeding, EDGE[b][0])).join('')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-blk">
|
||||||
|
<span class="d-fl">尺寸 / 面积</span>
|
||||||
|
<div class="d-chips">${sizes.map(s => chip(s, s === dSize)).join('')}</div>
|
||||||
|
<input class="d-input" value="${dSize}">
|
||||||
|
</div>
|
||||||
|
<div class="d-blk">
|
||||||
|
<div class="d-blk-h">
|
||||||
|
<span class="d-fl">伤口走向</span>
|
||||||
|
<span class="d-rot"><span class="d-rot-b">−45°</span><span class="d-rot-v">${dRot}°</span><span class="d-rot-b">+45°</span></span>
|
||||||
|
</div>
|
||||||
|
<div class="d-prev">
|
||||||
|
<div class="d-prev-bar" style="width:${bw / 2}px;margin-left:-${bhalf / 2}px;left:50%;background:${EDGE[dBleeding][1]};transform:rotate(${dRot}deg)"></div>
|
||||||
|
<div class="d-prev-dot" style="background:${EDGE[dBleeding][0]}"><span class="bmp-dot-t">4</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="d-chips">${[0, 45, 90, 135].map(r => chip(r + '°', r === dRot)).join('')}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-blk">
|
||||||
|
<div class="d-blk-h"><span class="d-fl">详情描述</span><span class="d-regen">↻ 按当前选项重生成</span></div>
|
||||||
|
<textarea class="d-ta">${defaultDesc(partName, dNature, dSize, dBleeding)}</textarea>
|
||||||
|
</div>`;
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 248 KiB |
@@ -0,0 +1,109 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>事件中心 · 进行中 / 历史</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin:0; padding:20px 16px 28px; background:#EDEFF3;
|
||||||
|
font-family:-apple-system,"PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif; }
|
||||||
|
h1 { font-size:15px; font-weight:600; color:#1F2937; margin:0 0 4px; }
|
||||||
|
.sub { font-size:12px; color:#6B7280; margin:0 0 8px; }
|
||||||
|
.removed { font-size:11.5px; color:#A32D2D; background:#FCEBEB; border:1px solid #F09595;
|
||||||
|
border-radius:8px; padding:8px 10px; margin:0 0 16px; line-height:1.7; }
|
||||||
|
.phone { width:375px; background:#F3F4F6; border-radius:18px; overflow:hidden;
|
||||||
|
box-shadow:0 6px 22px rgba(15,23,42,.14); }
|
||||||
|
.sbar { height:20px; background:#FFFFFF; }
|
||||||
|
.tabs { display:flex; flex-direction:row; background:#FFFFFF; height:44px; }
|
||||||
|
.tab { flex:1; display:flex; align-items:center; justify-content:center; position:relative;
|
||||||
|
font-size:14px; color:#6B7280; cursor:pointer; user-select:none; }
|
||||||
|
.tab.on { color:#2563EB; font-weight:bold; }
|
||||||
|
.tab.on::after { content:""; position:absolute; bottom:6px; width:30px; height:2.5px;
|
||||||
|
border-radius:2px; background:#2563EB; }
|
||||||
|
.body { padding:12px 16px 22px 16px; }
|
||||||
|
.card { background:#FFFFFF; border:1px solid #E5E7EB; border-radius:13px;
|
||||||
|
padding:12px 13px; margin-bottom:9px; }
|
||||||
|
.top { display:flex; flex-direction:row; align-items:center; }
|
||||||
|
.dot { width:8px; height:8px; border-radius:4px; margin:0 8px; flex-shrink:0; }
|
||||||
|
.name { flex:1; font-size:15.1px; font-weight:bold; color:#1F2937; margin-right:8px; }
|
||||||
|
.pc { height:21px; padding:0 8px; border-radius:6px; font-size:11.8px; font-weight:bold;
|
||||||
|
color:#FFFFFF; display:flex; align-items:center; }
|
||||||
|
.meta { font-size:12.9px; color:#6B7280; margin-top:6px; }
|
||||||
|
.meta2 { font-size:12.9px; color:#6B7280; margin-top:1px; }
|
||||||
|
.foot { display:flex; flex-direction:row; align-items:center; margin-top:9px; }
|
||||||
|
.avts { display:flex; flex-direction:row; }
|
||||||
|
.avt { width:22px; height:22px; border-radius:7px; border:1.5px solid #FFFFFF;
|
||||||
|
display:flex; align-items:center; justify-content:center; color:#FFFFFF;
|
||||||
|
font-size:10px; margin-left:-6px; }
|
||||||
|
.avt:first-child { margin-left:0; }
|
||||||
|
.sp { flex:1; }
|
||||||
|
.go { display:flex; align-items:center; font-size:12.9px; color:#2563EB; font-weight:bold; }
|
||||||
|
.chev { width:6px; height:6px; border-right:1.5px solid #2563EB; border-top:1.5px solid #2563EB;
|
||||||
|
transform:rotate(45deg); margin-left:3px; }
|
||||||
|
.note { text-align:center; font-size:11.8px; color:#9CA3AF; margin-top:8px; }
|
||||||
|
.note2 { text-align:center; font-size:11.8px; color:#9CA3AF; margin-top:1px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>事件中心 · 只保留「进行中 / 历史」</h1>
|
||||||
|
<p class="sub">Tab 由 3 个变 2 个(各占 50%),点标签可切换查看</p>
|
||||||
|
<p class="removed">已移除:「待接收」Tab | P0 全屏推送整块(60 秒倒计时 / 立即接收 / 稍后处理)| 卡片 pending 分支与 footer 守卫 | 空态与注释里对「全屏告警推送」的描述</p>
|
||||||
|
|
||||||
|
<div class="phone">
|
||||||
|
<div class="sbar"></div>
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab on" id="t0">进行中</div>
|
||||||
|
<div class="tab" id="t1">历史</div>
|
||||||
|
</div>
|
||||||
|
<div class="body" id="list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var data = [
|
||||||
|
{ tab:0, name:'京港澳高速 K120 多车追尾', kind:'大型交通事故', place:'京港澳高速 K120 · 距您 3.2 km',
|
||||||
|
pc:'P0', pcc:'#DC2626', dot:'#DC2626', go:'详情', members:[['张','#2563EB'],['李','#059669'],['王','#D97706']] },
|
||||||
|
{ tab:0, name:'阳光养老院 · 老人跌倒', kind:'一般救援', place:'阳光养老院 3 号楼 · 距您 1.1 km',
|
||||||
|
pc:'P2', pcc:'#2563EB', dot:'#10B981', go:'详情', members:[['陈','#059669']] },
|
||||||
|
{ tab:1, name:'幸福小区 · 老人心梗', kind:'一般救援', place:'幸福小区 6 号楼 · 距您 2.4 km',
|
||||||
|
pc:'P1', pcc:'#F59E0B', dot:'#F59E0B', go:'已完成', members:[['张','#2563EB']] },
|
||||||
|
{ tab:1, name:'滨江路 · 电动车相撞', kind:'交通事故', place:'滨江路与建设街路口 · 已完成',
|
||||||
|
pc:'P2', pcc:'#2563EB', dot:'#9CA3AF', go:'已完成', members:[['李','#059669']] },
|
||||||
|
{ tab:1, name:'第一中学 · 学生运动损伤', kind:'一般救援', place:'第一中学操场 · 已完成',
|
||||||
|
pc:'P2', pcc:'#2563EB', dot:'#9CA3AF', go:'已完成', members:[['陈','#94A3B8']] },
|
||||||
|
{ tab:1, name:'老城街区 · 煤气泄漏排查', kind:'危化品事故', place:'老城街区 12 巷 · 已完成',
|
||||||
|
pc:'P1', pcc:'#F59E0B', dot:'#9CA3AF', go:'已完成', members:[['王','#D97706']] },
|
||||||
|
{ tab:1, name:'火车站 · 旅客突发晕厥', kind:'一般救援', place:'火车站候车大厅 · 已完成',
|
||||||
|
pc:'P2', pcc:'#2563EB', dot:'#9CA3AF', go:'已完成', members:[['张','#2563EB']] }
|
||||||
|
];
|
||||||
|
var cur = 0;
|
||||||
|
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<'); }
|
||||||
|
function render(){
|
||||||
|
var html = '';
|
||||||
|
data.filter(function(d){ return d.tab === cur; }).forEach(function(d){
|
||||||
|
var av = d.members.map(function(m){
|
||||||
|
return '<div class="avt" style="background-color:'+m[1]+'">'+esc(m[0])+'</div>';
|
||||||
|
}).join('');
|
||||||
|
html += '<div class="card">'
|
||||||
|
+ '<div class="top"><div class="dot" style="background-color:'+d.dot+'"></div>'
|
||||||
|
+ '<div class="name">'+esc(d.name)+'</div>'
|
||||||
|
+ '<div class="pc" style="background-color:'+d.pcc+'">'+d.pc+'</div></div>'
|
||||||
|
+ '<div class="meta">'+esc(d.kind)+'</div>'
|
||||||
|
+ '<div class="meta2">现场:'+esc(d.place)+'</div>'
|
||||||
|
+ '<div class="foot"><div class="avts">'+av+'</div><div class="sp"></div>'
|
||||||
|
+ '<div class="go">'+esc(d.go)+'<div class="chev"></div></div></div>'
|
||||||
|
+ '</div>';
|
||||||
|
});
|
||||||
|
if (cur === 0) {
|
||||||
|
html += '<div class="note">仅显示与我相关的派单</div>'
|
||||||
|
+ '<div class="note2">时间与处理状态由后台同步,APP 端不展示</div>';
|
||||||
|
}
|
||||||
|
document.getElementById('list').innerHTML = html;
|
||||||
|
document.getElementById('t0').className = 'tab' + (cur === 0 ? ' on' : '');
|
||||||
|
document.getElementById('t1').className = 'tab' + (cur === 1 ? ' on' : '');
|
||||||
|
}
|
||||||
|
document.getElementById('t0').onclick = function(){ cur = 0; render(); };
|
||||||
|
document.getElementById('t1').onclick = function(){ cur = 1; render(); };
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,695 @@
|
|||||||
|
# 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 限制做了三类适配:
|
||||||
|
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.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` 的分段控件待换 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: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 可回显。
|
||||||
|
|
||||||
|
### 三个落点
|
||||||
|
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)与 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.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.0px(97%,跨机型太险);**22rpx = 54.1px(89%)安全**。
|
||||||
|
测量页 `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-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()`:
|
||||||
|
```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.md:uvue 硬约束加第 19 条 + 新增「uni_modules 本地补丁清单」表。
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# 医院救援 APP(hospital-rescue-app-v2)· 项目长期约定
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
- **uni-app x(uvue + uts)**,不是 uni-app(vue)。页面后缀 `.uvue`,脚本 `<script lang="uts" setup>`。
|
||||||
|
- UI 组件库 **Tmui4(uni_modules/tmx-ui)**,全局配置在 `main.uts`(designSize 375 / unit px / 主色 #2563EB / dark light)。
|
||||||
|
- easycom 自动扫描 `components/组件名/组件名.uvue`(本项目自研组件统一 `hr-` 前缀)。
|
||||||
|
|
||||||
|
## 设计规范
|
||||||
|
- 设计稿基准 **375 × 812**,`mobile-design/v2/hospital-rescue-ui-v2.html` 为唯一视觉依据(35 屏)。
|
||||||
|
- 设计稿 px = uvue px(uni-app x px 为逻辑像素),不要乘 2。
|
||||||
|
- 主色 #2563EB;四色腕带分级:危重 #DC2626 / 重伤 #F59E0B / 轻伤 #10B981 / 死亡 #1F2937。
|
||||||
|
- 底 #F3F4F6、卡片 #FFF、分割线 #F3F4F6 / #E5E7EB、文本 #1F2937 / #6B7280 / #9CA3AF。
|
||||||
|
|
||||||
|
## 角标(xBadge)几何 — 已定稿,别再改尺寸
|
||||||
|
- 全站角标基准(用户指定):高/圆角 36rpx、字号 22.4rpx、红底白字;`.xBadge-countAndLabel` 用 `!important` 覆盖。
|
||||||
|
- **角标实际宽 28px,不是 18px**:`min-width:36rpx` 是内容盒最小值,还要加左右各 10rpx 内边距。
|
||||||
|
- 定位走 `hr-design.css` 的「左锚定」:`top:-4rpx; left:44rpx; right:auto; transform:none`(复刻设计稿
|
||||||
|
`left:calc(50%+6px)`),配 `.xBadge{padding:0!important}` + 全链 `overflow:visible!important`。
|
||||||
|
实测只压 icon 6px(25%),3/5 Tab 与两位数都成立;**别改回 right 定位或库默认半出血**(会盖住 icon 42%~100%)。
|
||||||
|
- `x-tabbar` 传 `:offset="[0,5]"`,Tab 项高度写死 60px(内容约 53px,上下余量仅 ~3.5px)。
|
||||||
|
|
||||||
|
|
||||||
|
## uvue 硬约束(写样式前必读)
|
||||||
|
1. **不支持** `::before` / `::after` → 装饰一律用实体元素
|
||||||
|
2. **不支持** CSS 变量 `var()` → 颜色写实
|
||||||
|
3. **不使用** `display:grid`、`gap` → flex + margin
|
||||||
|
4. **只使用** class 选择器(不写后代选择器);**复合选择器 `.a.b` 也不要用** → 选中态改用 JS 计算 style 字符串内联下发
|
||||||
|
5. **Vue API 必须显式 import**:`import { ref, computed } from 'vue'`
|
||||||
|
6. `computed` 有返回类型时必须写 `computed(() : T => {})`,不能写 `computed() : T => {}`
|
||||||
|
7. **模板里调用的函数必须在 script 中定义**,否则运行期崩 `_ctx.xxx is not a function`(编译期不一定报错)
|
||||||
|
8. **页面生命周期是 uvue 全局函数,不要从 `@dcloudio/uni-app` import**;
|
||||||
|
显示/隐藏要用 `onPageShow` / `onPageHide`(不是 onShow/onHide)
|
||||||
|
9. `onLoad` 取参用下标 `opt['id']`;`res.content` 是 `string|null|undefined` 要先判空
|
||||||
|
10. `uni.showToast` 的 `icon` 是字面量联合类型('none'|'success'|'error'|'loading'…),封装时形参别写 `string`
|
||||||
|
11. 不用 `aspect-ratio`;尺寸用 windowWidth 计算或用固定值
|
||||||
|
12. 不用内联 `<svg>`;图形用 view/border-radius/x-circle-progress 等替代
|
||||||
|
13. **不支持** `@keyframes` / `animation`(App 端编译期直接 ERROR,之前记的「可用」是错的);
|
||||||
|
也不支持 `page` / `body` 元素选择器(页面底色由 `.hr-page` 或页面根节点承担)
|
||||||
|
→ 动效一律改 `setInterval` + `ref` 驱动内联样式(参见 event/index 红点、warehouse/scan 与
|
||||||
|
hr-scan-cam 扫描线、study/center 与 wrong-book 手风琴);扫描步长取 1/帧数 才能精确命中端点
|
||||||
|
14. **模板里不能直接调用 import 进来的函数**:编译成 `unref(fn)(x)`,App 端 Kotlin 报
|
||||||
|
`找不到名称"invoke"` / `Function invocation 'xxx(...)' expected`(#error18)
|
||||||
|
→ 在页面 script 里包一层本文件定义的 wrapper 再给模板用
|
||||||
|
15. **UTS 不提升函数声明**:被调用的函数必须写在调用点之前(computed 回调里最容易踩)
|
||||||
|
→ 扫描脚本 `check-uts-order.js`
|
||||||
|
16. **`background-image` 在 App 端**:`url(...)` 图片**完全不支持**(原生视图无背景图概念)
|
||||||
|
→ 铺底图/照片用 `<image>` 绝对定位铺满,或 `DrawableContext` 原生绘制;
|
||||||
|
`linear-gradient` **支持但只有一种合法形态**:`linear-gradient(<关键字方向>, <色1>, <色2>)`
|
||||||
|
—— **必须 3 参数、方向必须是关键字(`to bottom right` 等,不是 `135deg`)、恰好 2 个颜色
|
||||||
|
(不能带 `%` 色标、不能 3 色)**;flatten/vapor 模式下 `background-image` 直接失效。
|
||||||
|
→ 扫描脚本 `check-uvue-bg.js`
|
||||||
|
19. **UTS 里 `any` = Kotlin 非空 `Any`**,null 强转必崩:
|
||||||
|
`java.lang.NullPointerException: null cannot be cast to non-null type kotlin.Any`
|
||||||
|
→ 想表达可空必须写 `any | null`;**不要对可空值写 `as any`**(等于 `as Any`);
|
||||||
|
`UTSJSONObject.getAny(k)` 键不存在返回 null(不抛),调用方要自己兜;
|
||||||
|
结构体可选字段(`field?: any`)本就是可空,赋值时别强转。
|
||||||
|
排查技巧:栈指向哪个字面量 `return {` 那一行 → 就是字面量里某个 `as` 强转炸了。
|
||||||
|
|
||||||
|
## uni_modules 本地补丁清单(升级插件会被覆盖,务必回归)
|
||||||
|
| 文件 | 位置 | 现象 / 改法 |
|
||||||
|
|---|---|---|
|
||||||
|
| `uni_modules/tmx-ui/components/x-upload-file/x-upload-file.uvue` | `covertList()`(标记「⚠ 本地补丁(2026-09-24)」) | `defaultList` 项不带 `request` 时 `el.getAny("request") as any` 抛 `null cannot be cast to non-null type kotlin.Any`;改为 `let requestdata : any | null = el.getAny("request")` + 去掉 `as any`(APP/MP 与 WEB 两个 return 分支都改) |
|
||||||
|
|
||||||
|
|
||||||
|
## 分段切换(xRadioButton)— 全站 5 处已统一
|
||||||
|
- 原「`hr-row` + `hr-chip` 横向滚动」筛选全部换成 `x-radio-button`:study/center、study/wrong-book、
|
||||||
|
message/list、message/index、triage/treat-records。
|
||||||
|
- `RADIO_BUTTON = { id:string, icon?, title, disabled? }`;**change 下发 `(string, number)`**,
|
||||||
|
与 x-tabs 不同、无类型坑。`v-model` 绑 string id,业务要序号时用
|
||||||
|
`chip = computed(() : number => { const i = parseInt(chipId.value); return isNaN(i) ? 0 : i })`。
|
||||||
|
- 列表直接写字面量数组(项目惯例 `{ id:'0', title:'x' } as RADIO_BUTTON`),别再用 string[] + v-for 拼。
|
||||||
|
- **必须去掉外层 `<scroll-view :scroll-x>`**:组件是等宽 `flex:1` 分段,放进横向 scroll-view 会收窄、滑块定位错。
|
||||||
|
- 圆角坑:滑块圆角 = `parseInt(round) - parseInt(space)` 且**按 px 下发**(只有外层 round 跟 rpx),
|
||||||
|
round/space 传 rpx 时内外单位不一致 → 现配 `height=64rpx space=4rpx round=22rpx`(滑块呈胶囊,视觉 OK)。
|
||||||
|
- **字号必须 ≤22rpx**:等宽 4 段每段文本可用宽仅 60.75px,26rpx 加粗「心肺复苏 5」=63.9px 会截断
|
||||||
|
(无头 Chrome 实测),22rpx=54.1px(89%)安全;改文案先跑 `node-workspace/radiobtn-fit-test.html`。
|
||||||
|
- 样式基线:`bg-color #E5E7EB / active-color #2563EB / active-font-color #FFFFFF / font-color #6B7280`。
|
||||||
|
|
||||||
|
## 事件中心(pages/event/index)
|
||||||
|
- 用户定稿:**只保留「进行中 / 历史」两个 Tab(各占 50%)**,原「待接收」Tab 与 P0 全屏推送
|
||||||
|
(60 秒倒计时 / 立即接收 / 稍后处理 / 卡片 pending 分支)已整体移除——别按旧设计稿加回来。
|
||||||
|
- 紧急红点呼吸(dotStyle / startPulse)保留,触发只看数据的 `urgent` 字段(与 stage 无关)。
|
||||||
|
- ⚠ 遗留(已告知用户,待决定):mock 里 e3/e4 仍是 `stage:'pending'`(页面上不可见);
|
||||||
|
message/list 的「去接收 ›」仍跳事件中心,但进行中 Tab 看不到对应事件 —— 需把这两条改 'ongoing' 或删除。
|
||||||
|
|
||||||
|
## 三段式扫码作业页(批量处置 / 批量转运)
|
||||||
|
- `hr-scan-cam` **默认不启动相机**(cameraOn=false):点击整块取景区 `openCamera()` 才开启,
|
||||||
|
未开启时显示居中引导「点击开启摄像头」;底部按钮文案 开启扫码 ⇄ 连续扫码中。
|
||||||
|
扫描线定时器随相机开关启停(openCamera/toggleCamera 里 startLaser/stopLaser),不要 onMounted 空跑。
|
||||||
|
- 批量转运「接收医院」是折叠字段 `.op-pick`(医院名+床位+⌄),点击整行唤起
|
||||||
|
`hr-mask + hr-sheet` 弹层(与 victims/create 同一套类);选项为「医院名 + hospitalMeta 距离」两行,
|
||||||
|
满员医院名置灰(os-name-dim)+ 床位红字,点它弹 toast 且**不关闭弹层**,选可用医院才关。
|
||||||
|
|
||||||
|
## 目录约定
|
||||||
|
```
|
||||||
|
components/ 自研通用组件(hr- 前缀,easycom)
|
||||||
|
common/styles/hr-design.css 全局设计系统(App.uvue 里 @import)
|
||||||
|
mock/types.uts 数据模型类型
|
||||||
|
mock/index.uts mock 数据(换接口只改这里)
|
||||||
|
utils/role.uts 会话 + 双角色 TabBar + 页面权限
|
||||||
|
utils/nav.uts 统一跳转(含权限拦截)
|
||||||
|
utils/quiz.uts 答题状态(跨页共享)
|
||||||
|
pages/ 页面(按模块分子目录:home/event/mine/course/study/triage/warehouse/message/design)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 双角色
|
||||||
|
- 救援人员端 rescuer:Tab 首页 / 事件 / 我的;工号以 `1` 开头
|
||||||
|
- 仓管人员端 keeper:Tab 扫码 / 仓库 / 设备 / 消息 / 我;工号以 `2` 开头
|
||||||
|
- 新增页面务必在 `utils/role.uts` 的 `canAccess` 里登记角色归属
|
||||||
|
|
||||||
|
## 页面骨架用法
|
||||||
|
```vue
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="标题">
|
||||||
|
<template v-slot:right>…导航右侧…</template>
|
||||||
|
<view class="hr-body">…内容…</view>
|
||||||
|
<template v-slot:footer>…底部固定条…</template>
|
||||||
|
<template v-slot:tabbar><hr-tabbar :active="0"></hr-tabbar></template>
|
||||||
|
<template v-slot:overlay>…全屏/底部弹层…</template>
|
||||||
|
</hr-page>
|
||||||
|
```
|
||||||
|
- 三段式作业页(相机+列表+表单)用 `:no-scroll="true"`
|
||||||
|
- 需要自定义头部的页面(伤员详情)直接手写根 view + hr-statusbar
|
||||||
|
|
||||||
|
## 数据来源
|
||||||
|
当前全部走 `mock/index.uts`;对接接口时替换 mock 取数实现,页面层不改。
|
||||||
|
|
||||||
|
## 伤情标记(2D 人体图鉴)
|
||||||
|
- 数据层 `utils/injury.uts`:HrInjury 纯基础类型可直接 JSON;`injuriesToJson/injuriesFromJson` 存草稿与回显(接后端只换这两个)。
|
||||||
|
- 部位 id 按视野命名(armVL=图左臂)、name 按伤员左右(正面 armVL→右上肢);9 个热区互不重叠。
|
||||||
|
- 展示组件 `components/hr-bodymap`(正/背面,背面有脊柱线);业务/弹窗在 `pages/triage/create.uvue`(x-drawer 表单:部位自动填 + 其余默认填充 + 描述自动生成)。
|
||||||
|
- 标记 = 编号圆点(不旋转,色=出血情况)+ 长轴条(长=尺寸、rotate=走向);改数组必须重建再赋值。
|
||||||
|
- x-drawer 要点:`v-model:show`;bottom 时 `size`=内容高;自定义 footer 需 `:show-footer="true"` + `<template v-slot:footer>`(自己关弹窗)。
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>xRadioButton 分段切换预览</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin:0; padding:22px 16px 30px; background:#F3F4F6;
|
||||||
|
font-family:-apple-system,"PingFang SC","Microsoft YaHei","Helvetica Neue",sans-serif; }
|
||||||
|
h1 { font-size:15px; font-weight:600; color:#1F2937; margin:0 0 4px; }
|
||||||
|
.sub { font-size:12px; color:#6B7280; margin:0 0 18px; }
|
||||||
|
.case { margin-bottom:18px; }
|
||||||
|
.case-t { font-size:12px; color:#6B7280; margin-bottom:7px; }
|
||||||
|
.phone { width:375px; padding:0 16px; }
|
||||||
|
.track { display:flex; flex-direction:row; height:32px; padding:2px; border-radius:11px;
|
||||||
|
background:#E5E7EB; overflow:visible; }
|
||||||
|
.wrap { position:relative; flex:1; height:100%; }
|
||||||
|
.btns { position:absolute; z-index:5; left:0; top:0; width:100%; height:100%; display:flex; flex-direction:row; }
|
||||||
|
.item { flex:1; display:flex; align-items:center; justify-content:center; padding:0 12px;
|
||||||
|
font-size:11px; font-weight:bold; white-space:nowrap; overflow:hidden;
|
||||||
|
text-overflow:ellipsis; cursor:pointer; user-select:none; position:relative; z-index:2;
|
||||||
|
color:#6B7280; }
|
||||||
|
.item.on { color:#FFFFFF; }
|
||||||
|
.bg { position:absolute; left:0; top:0; height:100%; background:#2563EB; border-radius:18px;
|
||||||
|
transition:transform .35s cubic-bezier(.18,.89,.32,1); z-index:1; }
|
||||||
|
.old { display:flex; flex-direction:row; padding-bottom:2px; }
|
||||||
|
.chip { display:flex; align-items:center; height:28px; padding:0 11px; border-radius:9px;
|
||||||
|
font-size:13.4px; font-weight:bold; color:#6B7280; background:#FFFFFF;
|
||||||
|
border:1px solid #E5E7EB; margin-right:7px; }
|
||||||
|
.chip.on { background:#2563EB; border-color:#2563EB; color:#FFFFFF; }
|
||||||
|
.note { font-size:11px; color:#9CA3AF; margin-top:6px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>xRadioButton 分段切换(替换 hr-row + hr-chip)</h1>
|
||||||
|
<p class="sub">5 处筛选控件已改造:学习中心 / 错题本 / 消息中心(救护端) / 消息中心(物资端) / 处置记录</p>
|
||||||
|
|
||||||
|
<div class="case">
|
||||||
|
<div class="case-t">改造前 · hr-row + hr-chip(横向滚动,可左右拖)</div>
|
||||||
|
<div class="phone">
|
||||||
|
<div class="old">
|
||||||
|
<div class="chip on">全部 12</div>
|
||||||
|
<div class="chip">心肺复苏 5</div>
|
||||||
|
<div class="chip">气道管理 4</div>
|
||||||
|
<div class="chip">AED 3</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="case">
|
||||||
|
<div class="case-t">改造后 · x-radio-button(等宽分段,滑块跟随;点击可试)</div>
|
||||||
|
<div class="phone" id="ph"></div>
|
||||||
|
<div class="note">轨道 64rpx / 内间隙 4rpx / 圆角 22rpx / 字号 22rpx 加粗 · 底色 #E5E7EB ↦ 选中 #2563EB</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="case">
|
||||||
|
<div class="case-t">其它四处(默认选中第 1 项,点击可试)</div>
|
||||||
|
<div class="phone" id="ph2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var sets = [
|
||||||
|
['全部 12','心肺复苏 5','气道管理 4','AED 3'],
|
||||||
|
['全部 5','派单通知 2','系统消息 3'],
|
||||||
|
['全部 9','库存预警 4','借用审批 3','系统 2'],
|
||||||
|
['全部 4','止血 1','循环 1','分级 2']
|
||||||
|
];
|
||||||
|
function build(host, labels, i0){
|
||||||
|
var t = document.createElement('div'); t.className = 'track';
|
||||||
|
var w = document.createElement('div'); w.className = 'wrap';
|
||||||
|
var b = document.createElement('div'); b.className = 'btns';
|
||||||
|
var bg = document.createElement('div'); bg.className = 'bg';
|
||||||
|
bg.style.width = (100/labels.length) + '%';
|
||||||
|
labels.forEach(function(s, i){
|
||||||
|
var it = document.createElement('div');
|
||||||
|
it.className = 'item' + (i === i0 ? ' on' : '');
|
||||||
|
it.textContent = s;
|
||||||
|
it.onclick = function(){ select(i); };
|
||||||
|
b.appendChild(it);
|
||||||
|
});
|
||||||
|
function select(i){
|
||||||
|
bg.style.transform = 'translateX(' + (i*100) + '%)';
|
||||||
|
var items = b.querySelectorAll('.item');
|
||||||
|
for (var k=0;k<items.length;k++){
|
||||||
|
items[k].className = 'item' + (k === i ? ' on' : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.appendChild(b); w.appendChild(bg); t.appendChild(w); host.appendChild(t);
|
||||||
|
select(i0);
|
||||||
|
}
|
||||||
|
build(document.getElementById('ph'), sets[0], 0);
|
||||||
|
sets.slice(1).forEach(function(s){
|
||||||
|
var c = document.createElement('div');
|
||||||
|
c.style.marginTop = '10px';
|
||||||
|
document.getElementById('ph2').appendChild(c);
|
||||||
|
build(c, s, 0);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,391 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>uvue 动效改写验证 · 红点呼吸 / 扫描线 / 手风琴</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 32px 24px 64px;
|
||||||
|
background: #F3F4F6;
|
||||||
|
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
margin: 0 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #6B7280;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
background: #FFFBEB;
|
||||||
|
border: 1px solid #FDE68A;
|
||||||
|
color: #92400E;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
flex: 1 1 300px;
|
||||||
|
min-width: 300px;
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #E5E7EB;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 18px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h2 {
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.src {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9CA3AF;
|
||||||
|
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage {
|
||||||
|
height: 210px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 1. 红点 ---- */
|
||||||
|
.evt-card {
|
||||||
|
width: 210px;
|
||||||
|
background: #FFFBFB;
|
||||||
|
border: 1px solid #FECACA;
|
||||||
|
border-radius: 13px;
|
||||||
|
padding: 12px 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 8px;
|
||||||
|
background-color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pri {
|
||||||
|
background: #DC2626;
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 2. 扫描线 ---- */
|
||||||
|
.sc-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 178px;
|
||||||
|
background: #0F172A;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-frame {
|
||||||
|
width: 93px;
|
||||||
|
height: 93px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf {
|
||||||
|
position: absolute;
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border: 3px solid #10B981;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-tl {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-right-width: 0;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-tr {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-bl {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border-right-width: 0;
|
||||||
|
border-top-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-br {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-left-width: 0;
|
||||||
|
border-top-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 8.4px;
|
||||||
|
right: 8.4px;
|
||||||
|
height: 2px;
|
||||||
|
background: #10B981;
|
||||||
|
box-shadow: 0 0 8px rgba(16, 185, 129, .9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-tip {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, .78);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 3. 手风琴 ---- */
|
||||||
|
.qa-card {
|
||||||
|
width: 250px;
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #E5E7EB;
|
||||||
|
border-left: 4px solid #2563EB;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
box-shadow: 0 2px 10px rgba(15, 23, 42, .06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-stem {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-fold {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-btn {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-analysis {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #F9FAFB;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.readout {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9CA3AF;
|
||||||
|
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>uvue App 端动效改写 · 效果验证</h1>
|
||||||
|
<p class="sub">下方三个动效与 App 里 <b>完全相同</b> 的算法在跑(定时器逐帧推进 → 下发内联 style),
|
||||||
|
只是把 rpx 折算成 px(1rpx = 0.5px @375)。</p>
|
||||||
|
<div class="warn">
|
||||||
|
<b>背景</b>:App 端 uvue 的 CSS 编译器不接受 <code>@keyframes</code>(0%/75%/from/to 选择器直接报
|
||||||
|
ERROR)与 <code>animation</code> 属性,
|
||||||
|
因此这 3 处动效已全部改为 <code>setInterval</code> + <code>ref</code> 驱动内联样式的等价实现。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<!-- 1 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>① 待接收红点「呼吸」</h2>
|
||||||
|
<p class="src">pages/event/index.uvue · evtPulse</p>
|
||||||
|
<div class="stage" style="background:#F9FAFB">
|
||||||
|
<div class="evt-card">
|
||||||
|
<div class="evt-top">
|
||||||
|
<div class="evt-dot" id="dot"></div>
|
||||||
|
<span class="evt-name">车祸伤 · 多人受伤</span>
|
||||||
|
<span class="pri">P0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="readout" id="dotOut">半径 0rpx · 透明度 0.55</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>② 取景框扫描线</h2>
|
||||||
|
<p class="src">pages/warehouse/scan.uvue · hrScanMove</p>
|
||||||
|
<div class="stage">
|
||||||
|
<div class="sc-box">
|
||||||
|
<div class="sc-frame">
|
||||||
|
<div class="cf cf-tl"></div>
|
||||||
|
<div class="cf cf-tr"></div>
|
||||||
|
<div class="cf cf-bl"></div>
|
||||||
|
<div class="cf cf-br"></div>
|
||||||
|
<div class="sc-line" id="line"></div>
|
||||||
|
</div>
|
||||||
|
<div class="sc-tip">将条码对准框内 · 自动识别</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="readout" id="lineOut">top 60rpx → 313rpx · 32 帧 × 68ms = 2.18s</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3 -->
|
||||||
|
<div class="card">
|
||||||
|
<h2>③ 手风琴展开入场</h2>
|
||||||
|
<p class="src">pages/study/center.uvue · qa-acc-in</p>
|
||||||
|
<div class="stage" style="background:#F9FAFB">
|
||||||
|
<div class="qa-card">
|
||||||
|
<div class="qa-stem">成人胸外按压的深度应为?</div>
|
||||||
|
<div class="qa-fold"><span class="qa-btn" id="accBtn">点击展开解析</span></div>
|
||||||
|
<div id="accHost"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="readout" id="accOut">opacity 0 → 1 · translateY -16rpx → 0 · 240ms</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ============ ① 红点呼吸:与 pages/event/index.uvue 完全同算法 ============ */
|
||||||
|
const PULSE_FRAMES = 25
|
||||||
|
const PULSE_RAMP = 19
|
||||||
|
let pulseF = 0
|
||||||
|
const dotEl = document.getElementById('dot')
|
||||||
|
const dotOut = document.getElementById('dotOut')
|
||||||
|
|
||||||
|
/* 改写前的 CSS 版本,用作「应该长什么样」的参照 */
|
||||||
|
const RING_MAX_PX = 16 * 0.5 // 16rpx
|
||||||
|
function renderPulse() {
|
||||||
|
const i = pulseF
|
||||||
|
let r = 0, a = 0
|
||||||
|
if (i > PULSE_RAMP) { r = 0; a = 0 } else {
|
||||||
|
const k = i / PULSE_RAMP
|
||||||
|
r = Math.round(k * 16)
|
||||||
|
a = Math.round(55 * (1 - k))
|
||||||
|
}
|
||||||
|
dotEl.style.boxShadow = '0 0 0 ' + (r * 0.5).toFixed(1) + 'px rgba(220,38,38,0.' + a + ')'
|
||||||
|
dotOut.textContent = '半径 ' + r + 'rpx · 透明度 0.' + a + ' · 第 ' + i + '/25 帧'
|
||||||
|
const nf = pulseF + 1
|
||||||
|
pulseF = nf >= PULSE_FRAMES ? 0 : nf
|
||||||
|
}
|
||||||
|
setInterval(renderPulse, 68)
|
||||||
|
renderPulse()
|
||||||
|
|
||||||
|
/* ============ ② 扫描线:与 pages/warehouse/scan.uvue 完全同算法 ============ */
|
||||||
|
const LASER_MIN = 60, LASER_SPAN = 253
|
||||||
|
let laserT = 0.0
|
||||||
|
const lineEl = document.getElementById('line')
|
||||||
|
const lineOut = document.getElementById('lineOut')
|
||||||
|
function renderLaser() {
|
||||||
|
laserT = laserT + 0.03125
|
||||||
|
if (laserT > 1) { laserT = laserT - 1 }
|
||||||
|
const k = laserT < 0.5 ? laserT * 2 : (1 - laserT) * 2
|
||||||
|
const top = Math.round(LASER_MIN + k * LASER_SPAN)
|
||||||
|
lineEl.style.top = (top * 0.5).toFixed(1) + 'px'
|
||||||
|
lineOut.textContent = 'top ' + top + 'rpx (区间 60 → 313) · 32 帧 × 68ms = 2.18s'
|
||||||
|
}
|
||||||
|
setInterval(renderLaser, 68)
|
||||||
|
renderLaser()
|
||||||
|
|
||||||
|
/* ============ ③ 手风琴:与 pages/study/center.uvue 完全同算法 ============ */
|
||||||
|
let accIdx = -1, accT = 1, accTimer = 0, accOpen = false
|
||||||
|
const accHost = document.getElementById('accHost')
|
||||||
|
const accBtn = document.getElementById('accBtn')
|
||||||
|
const accOut = document.getElementById('accOut')
|
||||||
|
|
||||||
|
function stopAcc() {
|
||||||
|
if (accTimer != 0) { clearInterval(accTimer); accTimer = 0 }
|
||||||
|
}
|
||||||
|
function runAcc() {
|
||||||
|
stopAcc()
|
||||||
|
accIdx = 0
|
||||||
|
accT = 0
|
||||||
|
accTimer = setInterval(() => {
|
||||||
|
const t = accT + 0.16
|
||||||
|
if (t >= 1) { accT = 1; stopAcc(); renderAcc(); return }
|
||||||
|
accT = t
|
||||||
|
renderAcc()
|
||||||
|
}, 30)
|
||||||
|
}
|
||||||
|
function renderAcc() {
|
||||||
|
if (!accOpen) { accHost.innerHTML = ''; accOut.textContent = '已收起'; return }
|
||||||
|
if (accT >= 1) {
|
||||||
|
accHost.innerHTML = '<div class="qa-analysis">按压深度 5~6 cm,频率 100~120 次/分;每次按压后让胸廓完全回弹。</div>'
|
||||||
|
accOut.textContent = 'opacity 1 · translateY 0 · 动画完成'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const t = accT
|
||||||
|
const y = Math.round(-16 * (1 - t))
|
||||||
|
accHost.innerHTML = '<div class="qa-analysis" style="opacity:' + t +
|
||||||
|
';transform:translateY(' + (y * 0.5).toFixed(1) + 'px)">按压深度 5~6 cm,频率 100~120 次/分;每次按压后让胸廓完全回弹。</div>'
|
||||||
|
accOut.textContent = 'opacity ' + t.toFixed(2) + ' · translateY ' + y + 'rpx'
|
||||||
|
}
|
||||||
|
accBtn.addEventListener('click', () => {
|
||||||
|
accOpen = !accOpen
|
||||||
|
accBtn.textContent = accOpen ? '收起解析' : '点击展开解析'
|
||||||
|
if (accOpen) { runAcc() } else { stopAcc(); accIdx = -1; accT = 1; renderAcc() }
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="uni.app.UNIB73859A">
|
||||||
|
<application>
|
||||||
|
<provider android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="xShareS.FileProvider" android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script lang="uts">
|
||||||
|
export default {
|
||||||
|
onLaunch: function () {
|
||||||
|
// #ifdef APP||WEB
|
||||||
|
uni.loadFontFace({
|
||||||
|
global: true,
|
||||||
|
family: 'remixicon',
|
||||||
|
source: "url('/static/tmui4xLibs/static/remixicon.ttf')",
|
||||||
|
success() {
|
||||||
|
uni.setStorageSync("loadedFontBytmx", "true")
|
||||||
|
},
|
||||||
|
fail(_error : any) {
|
||||||
|
uni.setStorageSync("loadedFontBytmx", "")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
},
|
||||||
|
onShow: function () { },
|
||||||
|
onHide: function () { },
|
||||||
|
onLastPageBackPress: function () { },
|
||||||
|
onExit: function () { },
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* rpx单位 */
|
||||||
|
/* @import url(./uni_modules/tmx-ui/scss/uvue.min.css); */
|
||||||
|
/* px单位84kb */
|
||||||
|
@import url(./uni_modules/tmx-ui/scss/uvuePx.min.css);
|
||||||
|
/* 医院救援平台设计系统 · 全局样式 */
|
||||||
|
@import url(./common/styles/hr-design.css);
|
||||||
|
|
||||||
|
/* 字体图标,仅小程序端引用 */
|
||||||
|
/* #ifdef MP-WEIXIN */
|
||||||
|
@import url(./uni_modules/tmx-ui/scss/remixicon.min.css);
|
||||||
|
/* #endif */
|
||||||
|
|
||||||
|
/*
|
||||||
|
统一页面底色
|
||||||
|
⚠ App 端(uvue)CSS 只允许 class 选择器,不支持 page / body 元素选择器:
|
||||||
|
这里不再写 `page, body { background-color: #F3F4F6; }`,
|
||||||
|
页面底色统一由各页根节点承担 —— 组件页用 hr-page 的 page-bg,
|
||||||
|
自绘页直接写 `style="background-color:#F3F4F6"`。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* #ifdef WEB */
|
||||||
|
body::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div::-webkit-scrollbar {
|
||||||
|
width: 10rpx;
|
||||||
|
height: 10rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
div::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
}
|
||||||
|
/* #endif */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# x-ui
|
||||||
|
|
||||||
|
#### Description
|
||||||
|
x-ui基于uniappx ui库,核心工具库等原生开发。
|
||||||
|
|
||||||
|
#### Software Architecture
|
||||||
|
Software architecture description
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
|
||||||
|
1. xxxx
|
||||||
|
2. xxxx
|
||||||
|
3. xxxx
|
||||||
|
|
||||||
|
#### Instructions
|
||||||
|
|
||||||
|
1. xxxx
|
||||||
|
2. xxxx
|
||||||
|
3. xxxx
|
||||||
|
|
||||||
|
#### Contribution
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create Feat_xxx branch
|
||||||
|
3. Commit your code
|
||||||
|
4. Create Pull Request
|
||||||
|
|
||||||
|
|
||||||
|
#### Gitee Feature
|
||||||
|
|
||||||
|
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
||||||
|
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
||||||
|
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
||||||
|
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
||||||
|
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
||||||
|
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# x-ui
|
||||||
|
|
||||||
|
#### 介绍
|
||||||
|
x-ui基于uniappx ui库,核心工具库等原生开发。
|
||||||
|
|
||||||
|
#### 软件架构
|
||||||
|
软件架构说明
|
||||||
|
|
||||||
|
|
||||||
|
#### 安装教程
|
||||||
|
|
||||||
|
1. xxxx
|
||||||
|
2. xxxx
|
||||||
|
3. xxxx
|
||||||
|
|
||||||
|
#### 使用说明
|
||||||
|
|
||||||
|
1. xxxx
|
||||||
|
2. xxxx
|
||||||
|
3. xxxx
|
||||||
|
|
||||||
|
#### 参与贡献
|
||||||
|
|
||||||
|
1. Fork 本仓库
|
||||||
|
2. 新建 Feat_xxx 分支
|
||||||
|
3. 提交代码
|
||||||
|
4. 新建 Pull Request
|
||||||
|
|
||||||
|
|
||||||
|
#### 特技
|
||||||
|
|
||||||
|
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
|
||||||
|
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
|
||||||
|
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
|
||||||
|
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
|
||||||
|
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
|
||||||
|
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
2D 人体图鉴(单面)
|
||||||
|
- 人体轮廓沿用 hr-design.css 里的 .bm-* 几何(192rpx × 368rpx),不要再改用 SVG
|
||||||
|
- 热区:9 个不重叠的可点部位,点一下就是「在该部位新增标记」
|
||||||
|
- 标记:编号圆点(不旋转,保证数字可读)+ 伤口长轴条(长度=尺寸、角度=走向、颜色=出血)
|
||||||
|
-->
|
||||||
|
<view class="bm-figure">
|
||||||
|
|
||||||
|
<!-- ===== 人体轮廓(装饰,不响应点击) ===== -->
|
||||||
|
<view class="bm-head"></view>
|
||||||
|
<view class="bm-torso"></view>
|
||||||
|
<view class="bm-arm-l"></view>
|
||||||
|
<view class="bm-arm-r"></view>
|
||||||
|
<view class="bm-leg-l"></view>
|
||||||
|
<view class="bm-leg-r"></view>
|
||||||
|
<!-- 背面加一条脊柱线,让正/背面一眼能分辨 -->
|
||||||
|
<view v-if="side == 'back'" class="bmp-spine"></view>
|
||||||
|
|
||||||
|
<!-- ===== 可点部位热区 ===== -->
|
||||||
|
<view v-for="(p, i) in parts" :key="p.id" :class="hitClass(p)" :style="hitStyle(p)"
|
||||||
|
@click="onHit(p.id)">
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ===== 伤情标记 ===== -->
|
||||||
|
<view v-for="(m, i) in marks" :key="m.id" class="bmp-mark" :style="markStyle(m)" @click="onMark(m.id)">
|
||||||
|
<!-- 伤口长轴:先画在下方,编号圆点盖在上面 -->
|
||||||
|
<view class="bmp-bar" :style="barStyle(m)"></view>
|
||||||
|
<view class="bmp-dot" :style="dotStyle(m)">
|
||||||
|
<text class="bmp-dot-t">{{ m.no }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { HrInjury, HrBodyPart, sizeBarWidth, bleedingColor, bleedingBarColor } from '@/utils/injury.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2D 人体图鉴
|
||||||
|
* 只负责「画」和「命中」,业务(弹窗、表单、JSON)全部交给页面,
|
||||||
|
* 这样同一套图能复用到批量检伤、伤情详情等页面。
|
||||||
|
*/
|
||||||
|
type HrBodyMapProps = {
|
||||||
|
/** 当前视野:front 正面 / back 背面 */
|
||||||
|
side : string
|
||||||
|
/** 该视野的部位热区表(来自 utils/injury.uts 的 partsOf) */
|
||||||
|
parts : HrBodyPart[]
|
||||||
|
/** 该视野要显示的标记(页面先按 side 过滤好再传进来) */
|
||||||
|
marks : HrInjury[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<HrBodyMapProps>(), {
|
||||||
|
side : 'front',
|
||||||
|
parts : () : HrBodyPart[] => [] as HrBodyPart[],
|
||||||
|
marks : () : HrInjury[] => [] as HrInjury[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
/** 点击部位热区 */
|
||||||
|
(e : 'part-tap', partId : string) : void
|
||||||
|
/** 点击已有标记 */
|
||||||
|
(e : 'mark-tap', id : string) : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/* ---------- 热区 ---------- */
|
||||||
|
|
||||||
|
function hitStyle(p : HrBodyPart) : string {
|
||||||
|
return 'left:' + p.x + '%;top:' + p.y + '%;width:' + p.w + '%;height:' + p.h + '%;'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 该部位是否已有标记(用于把热区点亮,一眼看出伤在哪) */
|
||||||
|
function hasMark(partId : string) : boolean {
|
||||||
|
for (let i = 0; i < props.marks.length; i++) {
|
||||||
|
if (props.marks[i].partId == partId) { return true }
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitClass(p : HrBodyPart) : string {
|
||||||
|
return hasMark(p.id) ? 'bmp-hit bmp-hit-on' : 'bmp-hit'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHit(partId : string) : void {
|
||||||
|
emits('part-tap', partId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 标记 ---------- */
|
||||||
|
|
||||||
|
/** 标记整体位置:靠负 margin 居中(不用 transform,把 transform 留给旋转) */
|
||||||
|
function markStyle(m : HrInjury) : string {
|
||||||
|
return 'left:' + m.x + '%;top:' + m.y + '%;'
|
||||||
|
}
|
||||||
|
|
||||||
|
function dotStyle(m : HrInjury) : string {
|
||||||
|
return 'background-color:' + bleedingColor(m.bleeding) + ';'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 伤口长轴条:长度按尺寸、角度按 rot。
|
||||||
|
* left:50% + 负 margin 做水平居中,避免和 rotate 抢 transform。
|
||||||
|
*/
|
||||||
|
function barStyle(m : HrInjury) : string {
|
||||||
|
const w = sizeBarWidth(m.size)
|
||||||
|
const half = Math.round(w / 2)
|
||||||
|
return 'width:' + w + 'rpx;margin-left:-' + half + 'rpx;left:50%;' +
|
||||||
|
'background-color:' + bleedingBarColor(m.bleeding) + ';' +
|
||||||
|
'transform:rotate(' + m.rot + 'deg);'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMark(id : string) : void {
|
||||||
|
emits('mark-tap', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 导出给外部复用的换算(页面里的旋转预览用同一套) ---------- */
|
||||||
|
const sideLabel = computed(() : string => {
|
||||||
|
return props.side == 'back' ? '背面' : '正面'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 背面识别线:躯干中轴 */
|
||||||
|
.bmp-spine {
|
||||||
|
position: absolute;
|
||||||
|
left: 94rpx;
|
||||||
|
top: 78rpx;
|
||||||
|
width: 4rpx;
|
||||||
|
height: 108rpx;
|
||||||
|
border-radius: 2rpx;
|
||||||
|
background-color: #E2E8F0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 部位热区:平时只用极浅虚线提示「这里可点」,避免糊住人体图 */
|
||||||
|
.bmp-hit {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
border: 2rpx dashed #EDF0F4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 已有标记的部位点亮,形成「伤情分布」 */
|
||||||
|
.bmp-hit-on {
|
||||||
|
border: 2rpx solid #BFDBFE;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标记容器:绝对定位 + 负 margin 居中 */
|
||||||
|
.bmp-mark {
|
||||||
|
position: absolute;
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
margin-left: -38rpx;
|
||||||
|
margin-top: -38rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 伤口长轴条:竖直居中于容器((76-24)/2 = 26rpx) */
|
||||||
|
.bmp-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 26rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 编号圆点:不参与旋转 */
|
||||||
|
.bmp-dot {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
border: 3rpx solid #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bmp-dot-t {
|
||||||
|
font-size: 23rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<view :class="cardClass" :style="cardStyle">
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用卡片容器
|
||||||
|
* variant:default(默认) / list(自带列表内边距) / tight(紧凑) / danger(预警红底) / flat(无边框)
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
variant : { type: String, default: 'default' },
|
||||||
|
/** 自定义内边距,传空取 variant 默认值 */
|
||||||
|
padding : { type: String, default: '' },
|
||||||
|
/** 自定义外边距(底部) */
|
||||||
|
marginBottom : { type: String, default: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const cardClass = computed(() : string => {
|
||||||
|
let c = 'hr-card'
|
||||||
|
if (props.variant == 'list') { c += ' hr-card-list' }
|
||||||
|
if (props.variant == 'danger') { c += ' hr-card-danger' }
|
||||||
|
if (props.variant == 'tight') { c += ' hr-card-tight' }
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
|
||||||
|
const cardStyle = computed(() : string => {
|
||||||
|
let s = ''
|
||||||
|
if (props.padding != '') { s += `padding:${props.padding};` }
|
||||||
|
if (props.marginBottom != '') { s += `margin-bottom:${props.marginBottom};` }
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<text :class="chipClass" :style="chipStyle" @click="onClick">{{ text }}</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标签 / 筛选片
|
||||||
|
* theme:default / red / amb / grn ;active 为选中态(主色实底)
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
text : { type: String, default: '' },
|
||||||
|
active : { type: Boolean, default: false },
|
||||||
|
/** default / red / amb / grn / pri */
|
||||||
|
theme : { type: String, default: 'default' },
|
||||||
|
/** 是否大号(作业页 40px 高) */
|
||||||
|
large : { type: Boolean, default: false },
|
||||||
|
/** 自定义背景 / 文字 / 边线(传空取 theme 值) */
|
||||||
|
bg : { type: String, default: '' },
|
||||||
|
color : { type: String, default: '' },
|
||||||
|
border : { type: String, default: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'click') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const chipClass = computed(() : string => {
|
||||||
|
let c = 'hr-chip'
|
||||||
|
if (props.large) { c += ' hr-chip-lg' }
|
||||||
|
if (props.active) { c += ' hr-chip-active' }
|
||||||
|
if (!props.active) {
|
||||||
|
if (props.theme == 'red') { c += ' hr-chip-red' }
|
||||||
|
if (props.theme == 'amb') { c += ' hr-chip-amb' }
|
||||||
|
if (props.theme == 'grn') { c += ' hr-chip-grn' }
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
|
||||||
|
const chipStyle = computed(() : string => {
|
||||||
|
let s = ''
|
||||||
|
if (props.bg != '') { s += `background-color:${props.bg};` }
|
||||||
|
if (props.color != '') { s += `color:${props.color};` }
|
||||||
|
if (props.border != '') { s += `border:1px solid ${props.border};` }
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
|
||||||
|
function onClick() : void {
|
||||||
|
emits('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-empty">
|
||||||
|
<view class="hr-empty-ic">
|
||||||
|
<x-icon :name="iconName" color="#9CA3AF" font-size="52rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="hr-empty-b">{{ title }}</text>
|
||||||
|
<text v-if="sub != ''" class="hr-empty-s">{{ sub }}</text>
|
||||||
|
<view v-if="action != ''" class="hr-empty-btn" @click="onAction">
|
||||||
|
<text class="hr-empty-btn-t">{{ action }}</text>
|
||||||
|
</view>
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { toRemixIcon } from '@/utils/icon.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 空状态 / 异常态
|
||||||
|
* icon 支持字符图标(设计稿沿用)或 RemixIcon 名,统一由 xIcon 渲染
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
icon : { type: String, default: 'file-list-3-line' },
|
||||||
|
title : { type: String, default: '暂无数据' },
|
||||||
|
sub : { type: String, default: '' },
|
||||||
|
/** 操作按钮文案,为空则不显示 */
|
||||||
|
action : { type: String, default: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'action') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const iconName = computed(() : string => {
|
||||||
|
return toRemixIcon(props.icon)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onAction() : void {
|
||||||
|
emits('action')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hr-empty {
|
||||||
|
padding: 88rpx 40rpx 88rpx 40rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-empty-ic {
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-empty-b {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-empty-s {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
text-align: center;
|
||||||
|
lines: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-empty-btn {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
padding-left: 36rpx;
|
||||||
|
padding-right: 36rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-empty-btn-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
列表项:内部统一由组件库 xCell 渲染
|
||||||
|
对外保持本项目既有的 <hr-lrow icon theme title sub value …/> 用法,
|
||||||
|
这样 30+ 处列表无需逐个改造,且全站列表外观/交互由 xCell 统一保证。
|
||||||
|
-->
|
||||||
|
<x-cell :card="false" :margin="emptyMargin" :padding="cellPadding" :min-height="minHeight"
|
||||||
|
:show-bottom-border="!isLast" :bottom-border-insert="true" bottom-border-color="#F3F4F6" :title="title"
|
||||||
|
title-color="#1F2937" title-size="28rpx" :icon="resolvedIcon" :link="false" @click="onClick">
|
||||||
|
<!-- 左侧图标:彩色圆角块 + RemixIcon -->
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<view class="hr-lrow-ic" :style="{ backgroundColor: tc.bg }">
|
||||||
|
<x-icon :name="resolvedIcon" :color="tc.color" font-size="32rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
标题走默认插槽自己渲染。
|
||||||
|
原因:库内 .title 写死 lines:2(且是 scoped 样式),全站列表希望标题只有一行、
|
||||||
|
超出省略号;用插槽把标题换成自己的元素,样式完全可控,不依赖 !important 覆盖。
|
||||||
|
-->
|
||||||
|
<template v-slot:default>
|
||||||
|
<text class="hr-lrow-t">{{ title }}</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 副标题 -->
|
||||||
|
<template v-slot:desc>
|
||||||
|
<text v-if="sub != ''" class="hr-lmain-s">{{ sub }}</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 右侧:自定义插槽 + 数值文本 -->
|
||||||
|
<template v-slot:right>
|
||||||
|
<slot name="right"></slot>
|
||||||
|
<text v-if="value != ''" class="hr-lval" :style="valueStyle">{{ value }}</text>
|
||||||
|
</template>
|
||||||
|
</x-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { toRemixIcon, themeColor, HrThemeColor } from '@/utils/icon.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表行
|
||||||
|
* theme:default / pri / red / amb / grn —— 控制左侧图标底色
|
||||||
|
* valueTheme:t3(默认) / pri / red / amb / grn / t1
|
||||||
|
* icon 支持字符图标(设计稿沿用)或 RemixIcon 名,内部自动识别
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
icon : { type: String, default: '' },
|
||||||
|
theme : { type: String, default: 'default' },
|
||||||
|
title : { type: String, default: '' },
|
||||||
|
sub : { type: String, default: '' },
|
||||||
|
value : { type: String, default: '' },
|
||||||
|
valueTheme : { type: String, default: 't3' },
|
||||||
|
/** 右侧文字字号(rpx) */
|
||||||
|
valueSize : { type: String, default: '26rpx' },
|
||||||
|
/** 值是否加粗 */
|
||||||
|
valueBold : { type: Boolean, default: false },
|
||||||
|
/** 是否是最后一行(去除分隔线) */
|
||||||
|
isLast : { type: Boolean, default: false },
|
||||||
|
/** 是否是第一行 */
|
||||||
|
isFirst : { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'click') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const resolvedIcon = computed(() : string => {
|
||||||
|
return toRemixIcon(props.icon)
|
||||||
|
})
|
||||||
|
|
||||||
|
const tc = computed(() : HrThemeColor => {
|
||||||
|
return themeColor(props.theme)
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyMargin = computed(() : string[] => {
|
||||||
|
return ['0', '0']
|
||||||
|
})
|
||||||
|
|
||||||
|
const cellPadding = computed(() : string[] => {
|
||||||
|
return props.isFirst ? ['20rpx', '0'] : ['24rpx', '0']
|
||||||
|
})
|
||||||
|
|
||||||
|
const minHeight = computed(() : string => {
|
||||||
|
return '112rpx'
|
||||||
|
})
|
||||||
|
|
||||||
|
const valueStyle = computed(() : string => {
|
||||||
|
let color = '#9CA3AF'
|
||||||
|
if (props.valueTheme == 'pri') { color = '#2563EB' }
|
||||||
|
if (props.valueTheme == 'red') { color = '#DC2626' }
|
||||||
|
if (props.valueTheme == 'amb') { color = '#D97706' }
|
||||||
|
if (props.valueTheme == 'grn') { color = '#059669' }
|
||||||
|
if (props.valueTheme == 't1') { color = '#1F2937' }
|
||||||
|
const weight = props.valueBold ? 'bold' : 'normal'
|
||||||
|
return `color:${color};font-size:${props.valueSize};font-weight:${weight};`
|
||||||
|
})
|
||||||
|
|
||||||
|
function onClick() : void {
|
||||||
|
emits('click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hr-lrow-ic {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列表行标题:单行显示,超出省略号 */
|
||||||
|
.hr-lrow-t {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
lines: 1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-page" :style="{ backgroundColor: pageBg }">
|
||||||
|
|
||||||
|
<!-- 无导航栏时:普通状态栏占位 -->
|
||||||
|
<hr-statusbar v-if="navTitle == '' && !hiddenStatusbar" :bg="statusBg"></hr-statusbar>
|
||||||
|
|
||||||
|
<!-- 导航栏:使用组件库 x-navbar -->
|
||||||
|
<x-navbar v-if="navTitle != ''" :title="navTitle" :show-nav-back="showBack" :height="48" :bg-color="navBg"
|
||||||
|
:title-color="navTitleColor" :is-place="true" :static-transparent="statusBg == 'transparent'">
|
||||||
|
<template v-slot:right>
|
||||||
|
<view class="hr-row hr-navright" @click="onRightClick">
|
||||||
|
<slot name="right"></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</x-navbar>
|
||||||
|
|
||||||
|
<!-- 滚动主体:外层 .hr-flow 提供「内容区」定位基准(不含导航栏/底栏) -->
|
||||||
|
<view v-if="!noScroll" class="hr-flow">
|
||||||
|
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<slot></slot>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
加载中覆盖层(组件库 xLoading)
|
||||||
|
用绝对定位铺满整个内容区:绝对定位元素不参与 flex 布局,
|
||||||
|
高度必然等于内容区,不受 scroll-view 压缩影响。
|
||||||
|
-->
|
||||||
|
<view v-if="xloading && loading" class="hr-loading-mask">
|
||||||
|
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true"
|
||||||
|
color="#2563EB" text-color="#6B7280"></x-loading>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="hr-scroll">
|
||||||
|
<slot></slot>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部固定操作条 -->
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
|
||||||
|
<!-- 底部 TabBar -->
|
||||||
|
<slot name="tabbar"></slot>
|
||||||
|
|
||||||
|
<!-- 覆盖层(底部选择器 / 加载遮罩等,不随内容滚动) -->
|
||||||
|
<slot name="overlay"></slot>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
import { hrSession } from '@/utils/role.uts'
|
||||||
|
import { mockRequest } from '@/utils/request.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面骨架
|
||||||
|
* 统一承载「状态栏占位 / 导航栏 / 可滚动主体 / 底部固定条 / 底部 TabBar」五段结构,
|
||||||
|
* 各业务页只需关心自己的内容,避免每页重复搭建骨架。
|
||||||
|
*
|
||||||
|
* 列表页只要传 :xloading="true",请求期间就自动用组件库 xLoading 展示「加载中」,无需在页面里写 loading。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/** 页面底色 */
|
||||||
|
pageBg : { type: String, default: '#F3F4F6' },
|
||||||
|
/** 状态栏占位背景色;传 transparent 表示内容顶到状态栏下方 */
|
||||||
|
statusBg : { type: String, default: '#FFFFFF' },
|
||||||
|
/** 导航栏标题,为空则不渲染导航栏 */
|
||||||
|
navTitle : { type: String, default: '' },
|
||||||
|
/** 导航栏背景色 */
|
||||||
|
navBg : { type: String, default: '#FFFFFF' },
|
||||||
|
/** 导航栏标题颜色 */
|
||||||
|
navTitleColor : { type: String, default: '#1F2937' },
|
||||||
|
/** 是否显示返回按钮 */
|
||||||
|
showBack : { type: Boolean, default: true },
|
||||||
|
/** 是否隐藏状态栏占位(内容自绘状态区时使用) */
|
||||||
|
hiddenStatusbar : { type: Boolean, default: false },
|
||||||
|
/** 关闭内部滚动(三段式作业页自行控制滚动区) */
|
||||||
|
noScroll : { type: Boolean, default: false },
|
||||||
|
/** 是否展示「加载中」覆盖层(请求期间用组件库 xLoading) */
|
||||||
|
xloading : { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'right-click') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** 数据请求中:请求期间显示 xLoading */
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.xloading) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mockRequest(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 返回失败时的兜底页(按角色回到各自的一级页) */
|
||||||
|
const backErrorPath = computed(() : string => {
|
||||||
|
return hrSession.role == 'keeper' ? '/pages/warehouse/scan' : '/pages/home/index'
|
||||||
|
})
|
||||||
|
|
||||||
|
function onRightClick() : void {
|
||||||
|
emits('right-click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hr-navright {
|
||||||
|
height: 96rpx;
|
||||||
|
padding-left: 16rpx;
|
||||||
|
padding-right: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<text v-if="text != ''" :class="pcClass" :style="pcStyle">{{ text }}</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优先级角标
|
||||||
|
* 0 → P0 紧急(红) / 1 → P1 优先(黄) / 2 → P2 常规(蓝)
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
level : { type: Number, default: 2 },
|
||||||
|
/** 自定义文字,空则取 P0/P1/P2 */
|
||||||
|
text : { type: String, default: '' },
|
||||||
|
/** 实底(默认)或浅底 */
|
||||||
|
outline : { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = computed(() : string => {
|
||||||
|
if (props.text != '') { return props.text }
|
||||||
|
return `P${props.level}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const pcClass = computed(() : string => {
|
||||||
|
return `hr-pc hr-pc${props.level}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const pcStyle = computed(() : string => {
|
||||||
|
let c = '#DC2626'
|
||||||
|
if (props.level == 1) { c = '#F59E0B' }
|
||||||
|
if (props.level == 2) { c = '#2563EB' }
|
||||||
|
if (!props.outline) { return '' }
|
||||||
|
return `background-color:${c}1A;color:${c};`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<view :class="barClass" :style="trackStyle">
|
||||||
|
<view class="hr-pbar-i" :style="barStyle"></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进度条
|
||||||
|
* 高度用 rpx 下发,保证随屏宽等比缩放。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/** 百分比 0 ~ 100 */
|
||||||
|
percent : { type: Number, default: 0 },
|
||||||
|
/** 高度(设计稿 px,内部 ×2 转 rpx) */
|
||||||
|
h : { type: Number, default: 4 },
|
||||||
|
/** 进度色 */
|
||||||
|
color : { type: String, default: '#2563EB' },
|
||||||
|
/** 深色底(用于蓝色 hero / 视频条上) */
|
||||||
|
onDark : { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const pct = computed(() : number => {
|
||||||
|
if (props.percent < 0) { return 0 }
|
||||||
|
if (props.percent > 100) { return 100 }
|
||||||
|
return props.percent
|
||||||
|
})
|
||||||
|
|
||||||
|
const barClass = computed(() : string => {
|
||||||
|
return props.onDark ? 'hr-pbar hr-pbar-dark' : 'hr-pbar'
|
||||||
|
})
|
||||||
|
|
||||||
|
const barColor = computed(() : string => {
|
||||||
|
return props.onDark ? '#FFFFFF' : props.color
|
||||||
|
})
|
||||||
|
|
||||||
|
const rpxH = computed(() : number => {
|
||||||
|
return props.h * 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const trackStyle = computed(() : string => {
|
||||||
|
return `height:${rpxH.value}rpx;border-radius:${rpxH.value / 2}rpx;`
|
||||||
|
})
|
||||||
|
|
||||||
|
const barStyle = computed(() : string => {
|
||||||
|
return `width:${pct.value}%;height:${rpxH.value}rpx;border-radius:${rpxH.value / 2}rpx;background-color:${barColor.value};`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hr-pbar-dark {
|
||||||
|
background-color: rgba(255, 255, 255, 0.22);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<view class="rl-wrap">
|
||||||
|
<view class="rl-head">
|
||||||
|
<text class="rl-t">伤员记录</text>
|
||||||
|
<view class="rl-c">
|
||||||
|
<text class="rl-c-t">{{ list.length }} 人</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<text class="rl-clear" @click="onClear">清空 ›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view class="rl-list" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<view v-for="(v, i) in list" :key="v.id + '_' + i" class="rl-row">
|
||||||
|
<view class="rl-ix">
|
||||||
|
<text class="rl-ix-t">{{ i + 1 }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<view class="hr-row">
|
||||||
|
<text class="rl-name">{{ v.name }}</text>
|
||||||
|
<hr-wb :level="v.level" small></hr-wb>
|
||||||
|
</view>
|
||||||
|
<text class="rl-sub">{{ v.gender }} · {{ v.age }} 岁 · {{ v.injury }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="rl-del" @click="onRemove(i)">
|
||||||
|
<x-icon name="close-line" color="#6B7280" font-size="13"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="list.length == 0" class="rl-empty">
|
||||||
|
<x-icon name="scan-line" color="#9CA3AF" font-size="26"></x-icon>
|
||||||
|
<text class="rl-empty-t">尚未识别到伤员</text>
|
||||||
|
<text class="rl-empty-s">将腕带二维码对准上方取景框,自动连续识别</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text v-if="list.length > 0" class="rl-more">— 继续扫腕带可追加 —</text>
|
||||||
|
<view style="height:6px"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { HrVictim } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 伤员记录滚动列表(批量处置 / 批量转运共用)
|
||||||
|
* 三段落布局里唯一的滚动区。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
list : { type: Array as PropType<HrVictim[]>, default: () : HrVictim[] => [] as HrVictim[] }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'remove', index : number) : void
|
||||||
|
(e : 'clear') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function onRemove(i : number) : void {
|
||||||
|
emits('remove', i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() : void {
|
||||||
|
emits('clear')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rl-wrap {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 18rpx 32rpx 14rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-c {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 2rpx 14rpx 2rpx 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-c-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-clear {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-list {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0rpx 32rpx 20rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #F3F4F6;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
padding: 18rpx 24rpx 18rpx 24rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-ix {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-ix-t {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-name {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-sub {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-del {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-empty {
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 60rpx;
|
||||||
|
padding-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-empty-t {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-empty-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rl-more {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
padding: 4rpx 0rpx 8rpx 0rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
<template>
|
||||||
|
<view class="cm">
|
||||||
|
<!-- 取景区:默认不启动相机,点击本区域才开启(避免进页面就占用摄像头) -->
|
||||||
|
<view class="cm-view" @click="openCamera">
|
||||||
|
<!-- 真实相机扫码(APP / H5 均可用),失败时保留下方深色取景底 -->
|
||||||
|
<!-- #ifdef APP || WEB -->
|
||||||
|
<x-mlkit-scannig-u v-if="cameraOn" :flash="false" :autoOpenCamera="true" :cameraWidth="1080"
|
||||||
|
:cameraeiHght="1080" style="width:100%;height:100%;position:absolute;left:0;top:0"
|
||||||
|
@scan="onScanResult"></x-mlkit-scannig-u>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<!-- 未开启:居中引导,提示点击整块取景区 -->
|
||||||
|
<view v-if="!cameraOn" class="cm-idle">
|
||||||
|
<x-icon name="camera-line" color="#60A5FA" font-size="30"></x-icon>
|
||||||
|
<text class="cm-idle-t">点击开启摄像头</text>
|
||||||
|
<text class="cm-idle-s">对准伤员腕带二维码,自动连续识别</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="cameraOn" class="cm-frame">
|
||||||
|
<view class="cm-cf cm-cf-tl"></view>
|
||||||
|
<view class="cm-cf cm-cf-tr"></view>
|
||||||
|
<view class="cm-cf cm-cf-bl"></view>
|
||||||
|
<view class="cm-cf cm-cf-br"></view>
|
||||||
|
<view class="cm-laser" :style="laserStyle"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="cm-cnt">
|
||||||
|
<text class="cm-cnt-t">已识别 <text class="cm-cnt-n">{{ count }}</text> 人</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="cm-acts">
|
||||||
|
<view :class="cameraOn ? 'cm-act cm-act-on' : 'cm-act'" @click="toggleCamera">
|
||||||
|
<x-icon :name="cameraOn ? 'scan-2-line' : 'play-circle-line'" :color="cameraOn ? '#6EE7B7' : '#FFFFFF'"
|
||||||
|
font-size="15"></x-icon>
|
||||||
|
<text :class="cameraOn ? 'cm-act-t cm-act-t-on' : 'cm-act-t'">
|
||||||
|
{{ cameraOn ? '连续扫码中' : '开启扫码' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="cm-act" @click="onManual">
|
||||||
|
<x-icon name="keyboard-line" color="#FFFFFF" font-size="15"></x-icon>
|
||||||
|
<text class="cm-act-t">手动输入编号</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连续扫码取景组件(批量处置 / 批量转运共用)
|
||||||
|
* 顶部相机为固定区,**默认不启动**:点击顶部取景区才开启摄像头实时识别腕带二维码,
|
||||||
|
* 识别结果通过 scan 事件抛给页面。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/** 已识别人数 */
|
||||||
|
count: { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'scan', code : string) : void
|
||||||
|
(e : 'manual') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const cameraOn = ref(false)
|
||||||
|
const lastCode = ref('')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 取景框扫描线
|
||||||
|
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
|
||||||
|
* 改为定时器逐帧推进 top:cm-frame 高 172rpx,16%~84% ⇒ 28~145rpx。
|
||||||
|
* 周期 2.2s(68ms × 32 帧),与设计稿 hrCamScanMove 一致。
|
||||||
|
*/
|
||||||
|
const laserTop = ref(28)
|
||||||
|
let laserT = 0.0
|
||||||
|
let laserTimer : number = 0
|
||||||
|
|
||||||
|
const laserStyle = computed(() : string => {
|
||||||
|
return 'top:' + laserTop.value.toString() + 'rpx;'
|
||||||
|
})
|
||||||
|
|
||||||
|
function startLaser() : void {
|
||||||
|
if (laserTimer != 0) { return }
|
||||||
|
laserTimer = setInterval(() => {
|
||||||
|
laserT = laserT + 0.03125 // 1/32:32 帧正好走完一个往返,端点(16%/84%)可精确命中
|
||||||
|
if (laserT > 1) { laserT = laserT - 1 }
|
||||||
|
const k = laserT < 0.5 ? laserT * 2 : (1 - laserT) * 2
|
||||||
|
laserTop.value = Math.round(28 + k * 117)
|
||||||
|
}, 68)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopLaser() : void {
|
||||||
|
if (laserTimer != 0) {
|
||||||
|
clearInterval(laserTimer)
|
||||||
|
laserTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScanResult(res : string[]) : void {
|
||||||
|
if (res.length == 0) { return }
|
||||||
|
const code = res[0]
|
||||||
|
// 同一条码 2 秒内不重复计入
|
||||||
|
if (code == lastCode.value) { return }
|
||||||
|
lastCode.value = code
|
||||||
|
emits('scan', code)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击顶部取景区:开启相机并启动扫描线(已在扫码则忽略) */
|
||||||
|
function openCamera() : void {
|
||||||
|
if (cameraOn.value) { return }
|
||||||
|
cameraOn.value = true
|
||||||
|
startLaser()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 底部按钮:开启 / 暂停连续扫码 */
|
||||||
|
function toggleCamera() : void {
|
||||||
|
cameraOn.value = !cameraOn.value
|
||||||
|
if (cameraOn.value) { startLaser() } else { stopLaser() }
|
||||||
|
}
|
||||||
|
|
||||||
|
function onManual() : void {
|
||||||
|
emits('manual')
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopLaser()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cm {
|
||||||
|
background-color: #0B1220;
|
||||||
|
padding: 16rpx 28rpx 18rpx 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-view {
|
||||||
|
height: 216rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-color: #10192A;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 未开启相机时的居中引导(整块取景区可点,引导用户点击开启) */
|
||||||
|
.cm-idle {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-idle-t {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #E5E7EB;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-idle-s {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
color: rgba(148, 163, 184, 0.92);
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cnt {
|
||||||
|
position: absolute;
|
||||||
|
left: 20rpx;
|
||||||
|
top: 20rpx;
|
||||||
|
background-color: rgba(17, 24, 39, 0.62);
|
||||||
|
border-radius: 14rpx;
|
||||||
|
padding: 6rpx 16rpx 6rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cnt-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cnt-n {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #60A5FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-frame {
|
||||||
|
width: 172rpx;
|
||||||
|
height: 172rpx;
|
||||||
|
border-radius: 32rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cf {
|
||||||
|
position: absolute;
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border: 6rpx solid #60A5FA;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cf-tl {
|
||||||
|
left: 0rpx;
|
||||||
|
top: 0rpx;
|
||||||
|
border-right-width: 0rpx;
|
||||||
|
border-bottom-width: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cf-tr {
|
||||||
|
right: 0rpx;
|
||||||
|
top: 0rpx;
|
||||||
|
border-left-width: 0rpx;
|
||||||
|
border-bottom-width: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cf-bl {
|
||||||
|
left: 0rpx;
|
||||||
|
bottom: 0rpx;
|
||||||
|
border-right-width: 0rpx;
|
||||||
|
border-top-width: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-cf-br {
|
||||||
|
right: 0rpx;
|
||||||
|
bottom: 0rpx;
|
||||||
|
border-left-width: 0rpx;
|
||||||
|
border-top-width: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 扫描线:⚠ App 端 uvue 不支持 @keyframes/animation,
|
||||||
|
上下位移已改为 script 内定时器推进 laserTop + laserStyle 下发 top。 */
|
||||||
|
.cm-laser {
|
||||||
|
position: absolute;
|
||||||
|
left: 14rpx;
|
||||||
|
right: 14rpx;
|
||||||
|
top: 28rpx;
|
||||||
|
height: 4rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
background-color: #93C5FD;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-acts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-act {
|
||||||
|
flex: 1;
|
||||||
|
height: 68rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.10);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-act-on {
|
||||||
|
background-color: rgba(16, 185, 129, 0.20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-act-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-act-t-on {
|
||||||
|
color: #6EE7B7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-sec" :style="wrapStyle">
|
||||||
|
<view v-if="showBar" class="hr-sec-bar"></view>
|
||||||
|
<text class="hr-sec-t">{{ title }}</text>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<text v-if="more != ''" class="hr-sec-more" @click="onMore">{{ more }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小节标题
|
||||||
|
* 设计稿中由 .sec-t::before 伪元素绘制蓝色竖条,uvue 不支持伪元素,故拆为实体 view。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
title : { type: String, default: '' },
|
||||||
|
/** 右侧箭头文字,如「全部 ›」 */
|
||||||
|
more : { type: String, default: '' },
|
||||||
|
/** 是否显示蓝色竖条 */
|
||||||
|
showBar : { type: Boolean, default: true },
|
||||||
|
/** 是否为首个小节(收紧上边距) */
|
||||||
|
first : { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e : 'more-click') : void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const wrapStyle = computed(() : string => {
|
||||||
|
return props.first ? 'margin-top:8rpx;margin-bottom:16rpx;' : 'margin-top:32rpx;margin-bottom:16rpx;'
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMore() : void {
|
||||||
|
emits('more-click')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<view :style="barStyle"></view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态栏占位
|
||||||
|
* 设计稿中为 44px 的「9:41 + 状态图标」区域;真机上由系统状态栏承担,
|
||||||
|
* 因此这里只做高度占位,避免真机出现双状态栏。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/** 背景色,默认白 */
|
||||||
|
bg : { type: String, default: '#FFFFFF' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusHeight = ref(44)
|
||||||
|
|
||||||
|
const barStyle = computed(() : string => {
|
||||||
|
return `height:${statusHeight.value}px;background-color:${props.bg};`
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const info = uni.getWindowInfo()
|
||||||
|
if (info.statusBarHeight > 0) {
|
||||||
|
statusHeight.value = info.statusBarHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
使用组件库 x-tabbar。
|
||||||
|
- out-index=-1:关闭中间凸起(设计稿为平底导航)
|
||||||
|
- position=relative:x-tabbar 默认 position:fixed 会脱离文档流,
|
||||||
|
导致页面内容被底栏遮住。改成 relative 后它会占位,页面无需再手动预留高度。
|
||||||
|
-->
|
||||||
|
<x-tabbar :list="tabs" :active-index="active" :out-index="-1" position="relative" :is-canvas-render="false"
|
||||||
|
bg-color="#FFFFFF" dark-bg-color="#FFFFFF" color="#9CA3AF" selected-color="#2563EB" font-size="26rpx"
|
||||||
|
icon-size="48rpx" border-color="#F3F4F6" :show-top-border="true" @click="onClick">
|
||||||
|
</x-tabbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { hrSession, currentTabs, currentTabPages } from '@/utils/role.uts'
|
||||||
|
import { TABBAR_ITEM_INFO } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 底部导航
|
||||||
|
* 双角色核心:救援人员端 3 Tab / 仓管人员端 5 Tab,切换角色后自动换装。
|
||||||
|
* 始终显式传 list / activeIndex,不依赖 tmx-ui 全局 TabBar 状态,避免多页面互相干扰。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
/** 当前选中的 Tab 下标 */
|
||||||
|
active : { type: Number, default: 0 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = computed(() : TABBAR_ITEM_INFO[] => {
|
||||||
|
// 依赖 hrSession.role,角色变化时自动刷新
|
||||||
|
const role = hrSession.role
|
||||||
|
return currentTabs()
|
||||||
|
})
|
||||||
|
|
||||||
|
function onClick(index : number) : void {
|
||||||
|
if (index == props.active) { return }
|
||||||
|
const pages = currentTabPages()
|
||||||
|
if (index < 0 || index >= pages.length) { return }
|
||||||
|
uni.reLaunch({ url: pages[index] })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 时间线 / 步骤条:内部统一由组件库 xSteps 渲染(vertical 模式) -->
|
||||||
|
<x-steps :model-value="activeIndex" vertical :icon-size="'40rpx'" :label-size="'27rpx'" :desc-size="'24rpx'"
|
||||||
|
color="#9CA3AF" active-color="#2563EB">
|
||||||
|
<x-steps-item v-for="(node, i) in nodes" :key="i" :label="node.title" :desc="descOf(node)"
|
||||||
|
:icon="iconOf(node)" :active-icon="activeIconOf(node)" :active-color="colorOf(node)"></x-steps-item>
|
||||||
|
</x-steps>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { PropType, computed } from 'vue'
|
||||||
|
import { HrTimelineNode } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 时间线
|
||||||
|
* 设计稿的时间线 = 纵向步骤条,这里直接交给 xSteps(vertical)渲染,
|
||||||
|
* 用每项的 icon / activeIcon / activeColor 表达 normal / ok / warn 三种语义色。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
nodes : {
|
||||||
|
type : Array as PropType<HrTimelineNode[]>,
|
||||||
|
default : () : HrTimelineNode[] => [] as HrTimelineNode[]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 全部节点都视为「已发生」 */
|
||||||
|
const activeIndex = computed(() : number => {
|
||||||
|
return props.nodes.length == 0 ? 0 : props.nodes.length - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
function descOf(node : HrTimelineNode) : string {
|
||||||
|
return node.sub == '' ? node.time : `${node.time} · ${node.sub}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconOf(node : HrTimelineNode) : string {
|
||||||
|
return 'checkbox-blank-circle-line'
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeIconOf(node : HrTimelineNode) : string {
|
||||||
|
if (node.theme == 'ok') { return 'checkbox-circle-fill' }
|
||||||
|
if (node.theme == 'warn') { return 'error-warning-fill' }
|
||||||
|
return 'checkbox-blank-circle-fill'
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorOf(node : HrTimelineNode) : string {
|
||||||
|
if (node.theme == 'ok') { return '#10B981' }
|
||||||
|
if (node.theme == 'warn') { return '#DC2626' }
|
||||||
|
return '#2563EB'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 提示条:内部统一由组件库 xAlert 渲染(thin 浅色模式) -->
|
||||||
|
<x-alert :status="status" :icon="resolvedIcon" :show-close="false" skin="thin" font-size="25.8rpx"
|
||||||
|
:round="alertRound" :padding="alertPadding" :margin="emptyMargin">
|
||||||
|
<text class="hr-tip-tx">{{ text }}</text>
|
||||||
|
<slot></slot>
|
||||||
|
</x-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { toRemixIcon } from '@/utils/icon.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提示条
|
||||||
|
* theme:info / warn / err / ok —— 映射到 xAlert 的 status
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
theme : { type: String, default: 'info' },
|
||||||
|
icon : { type: String, default: '' },
|
||||||
|
text : { type: String, default: '' }
|
||||||
|
})
|
||||||
|
|
||||||
|
const status = computed(() : string => {
|
||||||
|
if (props.theme == 'warn') { return 'warn' }
|
||||||
|
if (props.theme == 'err') { return 'error' }
|
||||||
|
if (props.theme == 'ok') { return 'success' }
|
||||||
|
return 'info'
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 未传图标时交给 xAlert 按 status 取默认图标 */
|
||||||
|
const resolvedIcon = computed(() : string => {
|
||||||
|
return props.icon == '' ? '' : toRemixIcon(props.icon)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* xAlert 的 round / margin / padding 都是「数组」prop,
|
||||||
|
* 传字符串会在内部 val.map 处抛 `val.map is not a function`,务必用数组。
|
||||||
|
*/
|
||||||
|
const alertRound = computed(() : string[] => {
|
||||||
|
return ['22rpx']
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 两元素约定 = [左右, 上下](内部按 CSS 上右下左 重排) */
|
||||||
|
const alertPadding = computed(() : string[] => {
|
||||||
|
return ['24rpx', '20rpx']
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyMargin = computed(() : string[] => {
|
||||||
|
return ['0', '0']
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hr-tip-tx {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<view :class="wbClass">
|
||||||
|
<view class="hr-wb-dot" :style="{ backgroundColor: dotColor }"></view>
|
||||||
|
<text :style="{ color: textColor }">{{ label }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 伤情腕带分级标签
|
||||||
|
* 设计规则:颜色 + 文字双重编码
|
||||||
|
* 1 危重(红) / 2 重伤(黄) / 3 轻伤(绿) / 4 死亡(黑)
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
level : { type: Number, default: 3 },
|
||||||
|
/** 自定义文字,空则按分级取默认文案 */
|
||||||
|
text : { type: String, default: '' },
|
||||||
|
/** 小号(作业列表内使用) */
|
||||||
|
small : { type: Boolean, default: false }
|
||||||
|
})
|
||||||
|
|
||||||
|
const label = computed(() : string => {
|
||||||
|
if (props.text != '') { return props.text }
|
||||||
|
if (props.level == 1) { return '危重' }
|
||||||
|
if (props.level == 2) { return '重伤' }
|
||||||
|
if (props.level == 3) { return '轻伤' }
|
||||||
|
return '死亡'
|
||||||
|
})
|
||||||
|
|
||||||
|
const dotColor = computed(() : string => {
|
||||||
|
if (props.level == 1) { return '#B91C1C' }
|
||||||
|
if (props.level == 2) { return '#92400E' }
|
||||||
|
if (props.level == 3) { return '#065F46' }
|
||||||
|
return '#111827'
|
||||||
|
})
|
||||||
|
|
||||||
|
const textColor = computed(() : string => {
|
||||||
|
return dotColor.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const wbClass = computed(() : string => {
|
||||||
|
let c = 'hr-wb'
|
||||||
|
if (props.small) { c += ' hr-wb-sm' }
|
||||||
|
if (props.level == 1) { c += ' hr-wb1' }
|
||||||
|
if (props.level == 2) { c += ' hr-wb2' }
|
||||||
|
if (props.level == 3) { c += ' hr-wb3' }
|
||||||
|
if (props.level == 4) { c += ' hr-wb4' }
|
||||||
|
return c
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-wc">
|
||||||
|
<view v-for="(it, i) in items" :key="i" class="hr-row">
|
||||||
|
<view v-if="i > 0" class="hr-wc-sep"></view>
|
||||||
|
<view class="hr-wc-cell">
|
||||||
|
<view class="hr-wc-dot" :style="{ backgroundColor: it.color }"></view>
|
||||||
|
<text class="hr-wc-num" :style="{ color: it.color }">{{ it.value }}</text>
|
||||||
|
<text class="hr-wc-lb">{{ it.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<text class="hr-wc-note">{{ note }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
/** 4 色分级计数条(常驻事件详情顶部) */
|
||||||
|
const props = defineProps({
|
||||||
|
red : { type: Number, default: 0 },
|
||||||
|
amber : { type: Number, default: 0 },
|
||||||
|
green : { type: Number, default: 0 },
|
||||||
|
black : { type: Number, default: 0 },
|
||||||
|
note : { type: String, default: '实时更新' }
|
||||||
|
})
|
||||||
|
|
||||||
|
type WcItem = { color : string, label : string, value : number }
|
||||||
|
|
||||||
|
const items = computed(() : WcItem[] => {
|
||||||
|
const list : WcItem[] = [
|
||||||
|
{ color: '#DC2626', label: '红', value: props.red } as WcItem,
|
||||||
|
{ color: '#F59E0B', label: '黄', value: props.amber } as WcItem,
|
||||||
|
{ color: '#10B981', label: '绿', value: props.green } as WcItem,
|
||||||
|
{ color: '#1F2937', label: '黑', value: props.black } as WcItem
|
||||||
|
]
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.hr-wc {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 22rpx 26rpx 22rpx 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-wc-sep {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-wc-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-wc-dot {
|
||||||
|
width: 22rpx;
|
||||||
|
height: 22rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-wc-num {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-wc-lb {
|
||||||
|
font-size: 21.2rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-left: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-wc-note {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
module.exports = {
|
||||||
|
"is-custom-runtime": false,
|
||||||
|
"UNI_TEST_CUSTOM_ENV": {},
|
||||||
|
"compile": true,
|
||||||
|
"h5": {
|
||||||
|
"options": {
|
||||||
|
"headless": true
|
||||||
|
},
|
||||||
|
"executablePath": ""
|
||||||
|
},
|
||||||
|
"mp-weixin": {
|
||||||
|
"port": 9520,
|
||||||
|
"account": "",
|
||||||
|
"args": "",
|
||||||
|
"cwd": "",
|
||||||
|
"launch": true,
|
||||||
|
"teardown": "disconnect",
|
||||||
|
"remote": false,
|
||||||
|
"executablePath": "D:\\app\\微信web开发者工具\\微信开发者工具.exe"
|
||||||
|
},
|
||||||
|
"app-plus": {
|
||||||
|
"android": {
|
||||||
|
"id": "",
|
||||||
|
"executablePath": ""
|
||||||
|
},
|
||||||
|
"version": "",
|
||||||
|
"ios": {
|
||||||
|
"id": "",
|
||||||
|
"executablePath": ""
|
||||||
|
},
|
||||||
|
"uni-app-x": {
|
||||||
|
"version": "D:\\app\\HBuilderX.4.27.2024091305-alpha\\HBuilderX\\plugins\\uniappx-launcher\\base\\version.txt",
|
||||||
|
"android": {
|
||||||
|
"id": "emulator-5554",
|
||||||
|
"executablePath": "D:\\app\\HBuilderX.4.27.2024091305-alpha\\HBuilderX\\plugins\\uniappx-launcher\\base\\android_base.apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*!
|
||||||
|
Theme: Default
|
||||||
|
Description: Original highlight.js style
|
||||||
|
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||||
|
Maintainer: @highlightjs/core-team
|
||||||
|
Website: https://highlightjs.org/
|
||||||
|
License: see project LICENSE
|
||||||
|
Touched: 2021
|
||||||
|
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:6rpx 10rpx}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
text-align: center;
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格隔行变色 */
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border: 2rpx solid #ddd;
|
||||||
|
padding: 8rpx 16rpx;
|
||||||
|
font-size:31.4rpx;
|
||||||
|
}
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
|
<link href="./css/qulliCore.css" type="text/css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#layout,
|
||||||
|
#layout:focus,
|
||||||
|
.ql-editor,
|
||||||
|
.ql-editor:focus {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-editor {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-size-medium {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="layout">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script src="./js/uni.webview.1.5.5.js"></script>
|
||||||
|
<script src="./js/marked.min.js"></script>
|
||||||
|
<script src="./js/quill.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// 3. 注册自定义格式
|
||||||
|
const Inline = Quill.import('blots/inline');
|
||||||
|
|
||||||
|
class FontSizeBlot extends Inline {
|
||||||
|
static create(value) {
|
||||||
|
let node = super.create();
|
||||||
|
node.style.fontSize = value;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
static formats(node) {
|
||||||
|
return node.style.fontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FontSizeBlot.blotName = 'size';
|
||||||
|
|
||||||
|
Quill.register(FontSizeBlot);
|
||||||
|
|
||||||
|
var quill = new Quill('#layout', {
|
||||||
|
modules: {
|
||||||
|
toolbar: false // 不使用默认主题的工具栏
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var testEditormdView;
|
||||||
|
|
||||||
|
function getBodyHeight() {
|
||||||
|
var targetDiv = document.body.querySelector("#layout")
|
||||||
|
var height = targetDiv.scrollHeight
|
||||||
|
// document.body.style.height = (height) + 'px'
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'offsetHeight',
|
||||||
|
data: height + 5,
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'offsetHeight',
|
||||||
|
data: height
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetDiv = document.querySelector("#layout")
|
||||||
|
// 创建一个观察者对象并传入回调函数
|
||||||
|
var observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
if (mutation.type === 'childList' || mutation.type === 'attributes') {
|
||||||
|
getBodyHeight()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 配置观察选项:
|
||||||
|
var config = {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择需要观察变动的节点
|
||||||
|
observer.observe(targetDiv, config);
|
||||||
|
|
||||||
|
function getValueContentFormt(nr, isHtml) {
|
||||||
|
var content = ''
|
||||||
|
|
||||||
|
if (typeof nr == 'string') {
|
||||||
|
var cto = JSON.parse(nr);
|
||||||
|
if (cto.render == 'android') {
|
||||||
|
content = decodeURIComponent(atob(cto.value))
|
||||||
|
} else {
|
||||||
|
content = decodeURIComponent(cto.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (nr.render == 'android') {
|
||||||
|
content = decodeURIComponent(atob(nr.value))
|
||||||
|
} else {
|
||||||
|
content = decodeURIComponent(nr.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHtml) {
|
||||||
|
try {
|
||||||
|
content = marked.parse(content)
|
||||||
|
} catch (e) {
|
||||||
|
alert(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdown(nr, isHtml) {
|
||||||
|
var length = this.quill.getLength();
|
||||||
|
|
||||||
|
// 删除所有现有内容并在位置0插入新内容
|
||||||
|
quill.deleteText(0, length);
|
||||||
|
quill.clipboard.dangerouslyPasteHTML(0, getValueContentFormt(nr, isHtml))
|
||||||
|
// quill.root.innerHTML = getValueContentFormt(nr, isHtml)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBodyStyle(stylenr, isHtml) {
|
||||||
|
targetDiv.style = stylenr;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtml() {
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'toValue',
|
||||||
|
data: quill.root.innerHTML,
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'toValue',
|
||||||
|
data: quill.root.innerHTML
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectedStyle(nr, isHtml) {
|
||||||
|
|
||||||
|
var data = JSON.parse(getValueContentFormt(nr, isHtml))
|
||||||
|
var key = data.key.toLowerCase()
|
||||||
|
var value = data.value;
|
||||||
|
|
||||||
|
var eobj = {
|
||||||
|
'b': 'bold', //true,false
|
||||||
|
'i': 'italic', //true,false
|
||||||
|
's': 'strike', //true,false
|
||||||
|
'u': 'underline', //true,false
|
||||||
|
'link': 'link',
|
||||||
|
'img': 'image',
|
||||||
|
'video': 'video',
|
||||||
|
'align': 'align', //left,right,center
|
||||||
|
/**
|
||||||
|
* ordered:有序列表(编号列表)
|
||||||
|
* bullet:无序列表(项目符号列表)
|
||||||
|
* unchecked:未选中的复选框列表项
|
||||||
|
* checked:选中的复选框列表项
|
||||||
|
*/
|
||||||
|
'list': 'list',
|
||||||
|
'blockquote': 'blockquote', //true,false
|
||||||
|
'indent': 'indent', //缩进值0-8
|
||||||
|
'color': 'color',
|
||||||
|
'background': 'background',
|
||||||
|
'size': 'size',
|
||||||
|
'script': 'script', //super,sub
|
||||||
|
'header': 'header', //1-6
|
||||||
|
}
|
||||||
|
var range = quill.getSelection();
|
||||||
|
if (range) {
|
||||||
|
var currentFormat = quill.getFormat(range.index, range.length);
|
||||||
|
var styleforamtvalue = value;
|
||||||
|
var foramtStyleName = eobj[key]
|
||||||
|
if (
|
||||||
|
(foramtStyleName == 'header' && styleforamtvalue == 0) ||
|
||||||
|
(foramtStyleName == 'size' && styleforamtvalue == '') ||
|
||||||
|
foramtStyleName == 'align' && (styleforamtvalue == '' || styleforamtvalue == 'left')
|
||||||
|
) {
|
||||||
|
styleforamtvalue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(foramtStyleName, styleforamtvalue)
|
||||||
|
quill.format(foramtStyleName, styleforamtvalue);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quill.on('selection-change', function() {
|
||||||
|
var range = quill.getSelection();
|
||||||
|
if (!range) return;
|
||||||
|
var format = quill.getFormat(range);
|
||||||
|
|
||||||
|
var fontStyleOpts = {
|
||||||
|
img: format['image'] ? format['image'] : '',
|
||||||
|
link: format['link'] ? format['link'] : '',
|
||||||
|
b: format['bold'] === true,
|
||||||
|
i: format['italic'] === true,
|
||||||
|
s: format['strike'] === true,
|
||||||
|
u: format['underline'] === true,
|
||||||
|
align: format['align'] ? format['align'] : 'left',
|
||||||
|
list: format['list'] ? format['list'] : '',
|
||||||
|
indent: format['indent'] ? format['indent'] : 0,
|
||||||
|
header: format['header'] ? format['header'] : 0,
|
||||||
|
size: format['size'] ? format['size'] : 0,
|
||||||
|
color: format['color'] ? format['color'] : '',
|
||||||
|
background: format['background'] ? format['background'] : '',
|
||||||
|
}
|
||||||
|
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'fontStyleOpts',
|
||||||
|
data: fontStyleOpts,
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'fontStyleOpts',
|
||||||
|
data: fontStyleOpts
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
});
|
||||||
|
// 在iframe内部执行以下代码
|
||||||
|
var frameElement = window.frameElement;
|
||||||
|
var iframeId = frameElement == null ? "" : frameElement.getAttribute('id');
|
||||||
|
// 等待初始化完毕
|
||||||
|
document.addEventListener('UniAppJSBridgeReady', () => {
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'onJSBridgeReady',
|
||||||
|
data: '',
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'onJSBridgeReady'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
|
||||||
|
var iframeId = new URLSearchParams(window.location.search).get('id')||Math.random().toString(16).substring(4)
|
||||||
|
|
||||||
|
// 等待初始化完毕
|
||||||
|
document.addEventListener('UniAppJSBridgeReady', function(){
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'onJSBridgeReady',
|
||||||
|
data: '',
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'onJSBridgeReady'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// window.addEventListener('message', function(event) {
|
||||||
|
|
||||||
|
// // 根据消息内容执行相应的操作或调用函数
|
||||||
|
// if (typeof event.data === 'object' && event.data.action === 'callFunctionInIframe') {
|
||||||
|
// var data = event.data.data;
|
||||||
|
// myFunctionInIframe(data);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
var qr = null
|
||||||
|
// 绘制二维码。
|
||||||
|
//https://github.com/soldair/node-qrcode?tab=readme-ov-file#createtext-options
|
||||||
|
function createQrcode(size, foreground, background, text, logo, logoSize, padding) {
|
||||||
|
// let width = window.frameElement.offsetWidth
|
||||||
|
// let height = window.frameElement.offsetHeight
|
||||||
|
var canvas = document.getElementById("qrcode")
|
||||||
|
// canvas.width=width
|
||||||
|
// canvas.height=height
|
||||||
|
// canvas.style.width=width+'px'
|
||||||
|
// canvas.style.height=height+'px'
|
||||||
|
canvas.style.display = "block"
|
||||||
|
let ctx = canvas.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
document.body.style.height = "100%"
|
||||||
|
size = size || 128;
|
||||||
|
padding = padding || 2
|
||||||
|
QRCode.toCanvas(canvas, text || 'xui', {
|
||||||
|
element: canvas,
|
||||||
|
width: size,
|
||||||
|
errorCorrectionLevel: 'H',
|
||||||
|
margin: padding,
|
||||||
|
color: {
|
||||||
|
dark: foreground || "black",
|
||||||
|
light: background || "white"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (logo) {
|
||||||
|
let ctx = canvas.getContext("2d");
|
||||||
|
let img = new Image();
|
||||||
|
logoSize = logoSize || 50
|
||||||
|
img.width = logoSize + "px"
|
||||||
|
img.height = logoSize + "px"
|
||||||
|
img.src = logo
|
||||||
|
img.onload = function() {
|
||||||
|
let x = (size - logoSize) / 2
|
||||||
|
let y = x;
|
||||||
|
ctx.drawImage(img, x, y, logoSize, logoSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
//获取二维码图片。
|
||||||
|
function QrcodeToDateUrl() {
|
||||||
|
if (qr) {
|
||||||
|
let imgdata = qr.toDataURL();
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'img',
|
||||||
|
url: imgdata
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'error',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var ect = null;
|
||||||
|
|
||||||
|
function getchart() {
|
||||||
|
if (ect) return ect;
|
||||||
|
let dom = document.getElementById('echart');
|
||||||
|
return echarts.init(dom);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chart_setSize() {
|
||||||
|
let width = window.innerWidth
|
||||||
|
let height = window.innerHeight
|
||||||
|
let dom = document.getElementById('echart');
|
||||||
|
let domhtml = document.getElement
|
||||||
|
dom.style.width = width + 'px'
|
||||||
|
dom.style.height = height + 'px'
|
||||||
|
document.getElementsByTagName('html')[0].style = 'overflow:hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码 JSON 字符串(包括深层解码)
|
||||||
|
function decodeJSON(jsonString) {
|
||||||
|
// 如果输入不是字符串,直接返回
|
||||||
|
if (typeof jsonString !== 'string') {
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = JSON.parse(jsonString,function(k,v){
|
||||||
|
// 处理常规函数定义,包括多行函数 "key": function(params) { ... }
|
||||||
|
var isFunctionStr =
|
||||||
|
// 常规函数
|
||||||
|
/^\s*function\s*\([^)]*\)\s*\{[\s\S]*\}\s*$/.test(v) ||
|
||||||
|
// 箭头函数 - 带花括号
|
||||||
|
/^\s*\([^)]*\)\s*=>\s*\{[\s\S]*\}\s*$/.test(v) ||
|
||||||
|
// 箭头函数 - 简写形式
|
||||||
|
/^\s*\([^)]*\)\s*=>\s*[^{\s][\s\S]*$/.test(v) ||
|
||||||
|
// 单参数箭头函数 - 无括号
|
||||||
|
/^\s*[a-zA-Z0-9_$]+\s*=>\s*[^{\s][\s\S]*$/.test(v) ||
|
||||||
|
// 单参数箭头函数 - 带花括号
|
||||||
|
/^\s*[a-zA-Z0-9_$]+\s*=>\s*\{[\s\S]*\}\s*$/.test(v);
|
||||||
|
|
||||||
|
|
||||||
|
if(typeof v === 'string' && isFunctionStr){
|
||||||
|
v = eval('('+v+')')
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function chart_setOption(optionStr) {
|
||||||
|
chart_setSize()
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
let chart = getchart();
|
||||||
|
if (!optionStr) return;
|
||||||
|
try {
|
||||||
|
|
||||||
|
let option = decodeJSON(optionStr)
|
||||||
|
|
||||||
|
chart.setOption(option)
|
||||||
|
} catch (error) {
|
||||||
|
alert(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chart_call(funName, optionStr, eventsid) {
|
||||||
|
|
||||||
|
chart_setSize()
|
||||||
|
let filterevents = ["click"]
|
||||||
|
// 基于准备好的dom,初始化echarts实例
|
||||||
|
let chart = getchart();
|
||||||
|
|
||||||
|
if (filterevents.includes(funName)) {
|
||||||
|
|
||||||
|
chart['on'](funName, function(params) {
|
||||||
|
delete params.event
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: funName,
|
||||||
|
eventId: eventsid,
|
||||||
|
data: JSON.stringify(params),
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: funName,
|
||||||
|
eventId: eventsid,
|
||||||
|
data: JSON.stringify(params)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
|
||||||
|
try {
|
||||||
|
let option = JSON.parse(optionStr)
|
||||||
|
chart[funName](option)
|
||||||
|
} catch (e) {
|
||||||
|
chart[funName]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
|
||||||
|
function markedHighlight(options) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
options = {
|
||||||
|
highlight: options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options || typeof options.highlight !== 'function') {
|
||||||
|
throw new Error('Must provide highlight function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.langPrefix !== 'string') {
|
||||||
|
options.langPrefix = 'language-';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.emptyLangClass !== 'string') {
|
||||||
|
options.emptyLangClass = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async: !!options.async,
|
||||||
|
walkTokens(token) {
|
||||||
|
if (token.type !== 'code') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLang(token.lang);
|
||||||
|
|
||||||
|
if (options.async) {
|
||||||
|
return Promise.resolve(options.highlight(token.text, lang, token.lang || '')).then(
|
||||||
|
updateToken(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = options.highlight(token.text, lang, token.lang || '');
|
||||||
|
if (code instanceof Promise) {
|
||||||
|
throw new Error(
|
||||||
|
'markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updateToken(token)(code);
|
||||||
|
},
|
||||||
|
useNewRenderer: true,
|
||||||
|
renderer: {
|
||||||
|
code(code, infoString, escaped) {
|
||||||
|
// istanbul ignore next
|
||||||
|
if (typeof code === 'object') {
|
||||||
|
escaped = code.escaped;
|
||||||
|
infoString = code.lang;
|
||||||
|
code = code.text;
|
||||||
|
}
|
||||||
|
const lang = getLang(infoString);
|
||||||
|
const classValue = lang ? options.langPrefix + escape(lang) : options.emptyLangClass;
|
||||||
|
const classAttr = classValue ?
|
||||||
|
` class="${classValue}"` :
|
||||||
|
'';
|
||||||
|
code = code.replace(/\n$/, '');
|
||||||
|
return `<pre><code${classAttr}>${escaped ? code : escape(code, true)}\n</code></pre>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLang(lang) {
|
||||||
|
return (lang || '').match(/\S*/)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateToken(token) {
|
||||||
|
return (code) => {
|
||||||
|
if (typeof code === 'string' && code !== token.text) {
|
||||||
|
token.escaped = true;
|
||||||
|
token.text = code;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from marked helpers
|
||||||
|
const escapeTest = /[&<>"']/;
|
||||||
|
const escapeReplace = new RegExp(escapeTest.source, 'g');
|
||||||
|
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
|
||||||
|
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
|
||||||
|
const escapeReplacements = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
};
|
||||||
|
const getEscapeReplacement = (ch) => escapeReplacements[ch];
|
||||||
|
|
||||||
|
function escape(html, encode) {
|
||||||
|
if (encode) {
|
||||||
|
if (escapeTest.test(html)) {
|
||||||
|
return html.replace(escapeReplace, getEscapeReplacement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (escapeTestNoEncode.test(html)) {
|
||||||
|
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('katex')) :
|
||||||
|
typeof define === 'function' && define.amd ? define(['katex'], factory) :
|
||||||
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.markedKatex = factory(global.katex));
|
||||||
|
})(this, (function (katex) { 'use strict';
|
||||||
|
|
||||||
|
const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/;
|
||||||
|
const inlineRuleNonStandard = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1/; // Non-standard, even if there are no spaces before and after $ or $$, try to parse
|
||||||
|
|
||||||
|
const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
|
||||||
|
|
||||||
|
function index(options = {}) {
|
||||||
|
return {
|
||||||
|
extensions: [
|
||||||
|
inlineKatex(options, createRenderer(options, false)),
|
||||||
|
blockKatex(options, createRenderer(options, true)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRenderer(options, newlineAfter) {
|
||||||
|
return (token) => katex.renderToString(token.text, { ...options, displayMode: token.displayMode }) + (newlineAfter ? '\n' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function inlineKatex(options, renderer) {
|
||||||
|
const nonStandard = options && options.nonStandard;
|
||||||
|
const ruleReg = nonStandard ? inlineRuleNonStandard : inlineRule;
|
||||||
|
return {
|
||||||
|
name: 'inlineKatex',
|
||||||
|
level: 'inline',
|
||||||
|
start(src) {
|
||||||
|
let index;
|
||||||
|
let indexSrc = src;
|
||||||
|
|
||||||
|
while (indexSrc) {
|
||||||
|
index = indexSrc.indexOf('$');
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const f = nonStandard ? index > -1 : index === 0 || indexSrc.charAt(index - 1) === ' ';
|
||||||
|
if (f) {
|
||||||
|
const possibleKatex = indexSrc.substring(index);
|
||||||
|
|
||||||
|
if (possibleKatex.match(ruleReg)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexSrc = indexSrc.substring(index + 1).replace(/^\$+/, '');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const match = src.match(ruleReg);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'inlineKatex',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[2].trim(),
|
||||||
|
displayMode: match[1].length === 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockKatex(options, renderer) {
|
||||||
|
return {
|
||||||
|
name: 'blockKatex',
|
||||||
|
level: 'block',
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
const match = src.match(blockRule);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
type: 'blockKatex',
|
||||||
|
raw: match[0],
|
||||||
|
text: match[2].trim(),
|
||||||
|
displayMode: match[1].length === 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
|
||||||
|
}));
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
|
||||||
|
function extendedTables({
|
||||||
|
interruptPatterns = [],
|
||||||
|
skipEmptyRows = true
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
extensions: [{
|
||||||
|
name: 'spanTable',
|
||||||
|
level: 'block', // Is this a block-level or inline-level tokenizer?
|
||||||
|
start(src) {
|
||||||
|
return src.match(/\n *([^\n ].*\|.*)\n/m)?.index;
|
||||||
|
}, // Hint to Marked.js to stop and check for a match
|
||||||
|
tokenizer(src, tokens) {
|
||||||
|
// const regex = this.tokenizer.rules.block.table;
|
||||||
|
let regexString = '^ *([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' // Header
|
||||||
|
+
|
||||||
|
' {0,3}(?:\\| *)?(:?-+(?: *(?:100|[1-9][0-9]?%) *-+)?:? *(?:\\| *:?-+(?: *(?:100|[1-9][0-9]?%) *-+)?:? *)*)(?:\\| *)?' // Align
|
||||||
|
+
|
||||||
|
'(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells
|
||||||
|
+
|
||||||
|
'(?:\\n+|$)| {0,3}#{1,6}(?:\\s|$)| {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
|
||||||
|
'(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
|
||||||
|
'<\\/?(?:address|article|aside|base|basefont|blockquote|body' +
|
||||||
|
'|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt' +
|
||||||
|
'|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]' +
|
||||||
|
'|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem' +
|
||||||
|
'|meta|nav|noframes|ol|optgroup|option|p|param|section|source' +
|
||||||
|
'|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)' +
|
||||||
|
'(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)endRegex).*(?:\\n|$))*)\\n*|$)'; // Cells
|
||||||
|
|
||||||
|
regexString = regexString.replace('endRegex', interruptPatterns.map(str =>
|
||||||
|
`|(?:${str})`).join(''));
|
||||||
|
const widthRegex = / *(?:100|[1-9][0-9]?%) */g;
|
||||||
|
const regex = new RegExp(regexString);
|
||||||
|
const cap = regex.exec(src);
|
||||||
|
|
||||||
|
if (cap) {
|
||||||
|
const item = {
|
||||||
|
type: 'spanTable',
|
||||||
|
header: cap[1].replace(/\n$/, '').split('\n'),
|
||||||
|
align: cap[2].replace(widthRegex, '').replace(/^ *|\| *$/g, '').split(
|
||||||
|
/ *\| */),
|
||||||
|
rows: cap[3]?.trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : [],
|
||||||
|
width: cap[2].replace(/:/g, '').replace(/-+| /g, '').split('|')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get first header row to determine how many columns
|
||||||
|
item.header[0] = splitCells(item.header[0]);
|
||||||
|
|
||||||
|
const colCount = item.header[0].reduce((length, header) => {
|
||||||
|
return length + header.colspan;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
if (colCount === item.align.length) {
|
||||||
|
item.raw = cap[0];
|
||||||
|
|
||||||
|
let i, j, k, row;
|
||||||
|
|
||||||
|
// Get alignment row (:---:)
|
||||||
|
let l = item.align.length;
|
||||||
|
|
||||||
|
for (i = 0; i < l; i++) {
|
||||||
|
if (/^ *-+: *$/.test(item.align[i])) {
|
||||||
|
item.align[i] = 'right';
|
||||||
|
} else if (/^ *:-+: *$/.test(item.align[i])) {
|
||||||
|
item.align[i] = 'center';
|
||||||
|
} else if (/^ *:-+ *$/.test(item.align[i])) {
|
||||||
|
item.align[i] = 'left';
|
||||||
|
} else {
|
||||||
|
item.align[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get any remaining header rows
|
||||||
|
l = item.header.length;
|
||||||
|
for (i = 1; i < l; i++) {
|
||||||
|
item.header[i] = splitCells(item.header[i], colCount, item.header[i -
|
||||||
|
1], skipEmptyRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get main table cells
|
||||||
|
l = item.rows.length;
|
||||||
|
for (i = 0; i < l; i++) {
|
||||||
|
item.rows[i] = splitCells(item.rows[i], colCount, item.rows[i - 1],
|
||||||
|
skipEmptyRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// header child tokens
|
||||||
|
l = item.header.length;
|
||||||
|
for (j = 0; j < l; j++) {
|
||||||
|
row = item.header[j];
|
||||||
|
for (k = 0; k < row.length; k++) {
|
||||||
|
row[k].tokens = [];
|
||||||
|
this.lexer.inline(row[k].text, row[k].tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cell child tokens
|
||||||
|
l = item.rows.length;
|
||||||
|
for (j = 0; j < l; j++) {
|
||||||
|
row = item.rows[j];
|
||||||
|
for (k = 0; k < row.length; k++) {
|
||||||
|
row[k].tokens = [];
|
||||||
|
this.lexer.inline(row[k].text, row[k].tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderer(token) {
|
||||||
|
|
||||||
|
let i, j, row, cell, col, text;
|
||||||
|
let output = '<table>';
|
||||||
|
output += '<thead>';
|
||||||
|
for (i = 0; i < token.header.length; i++) {
|
||||||
|
row = token.header[i];
|
||||||
|
let col = 0;
|
||||||
|
output += '<tr>';
|
||||||
|
for (j = 0; j < row.length; j++) {
|
||||||
|
cell = row[j];
|
||||||
|
text = this.parser.parseInline(cell.tokens);
|
||||||
|
output += getTableCell(text, cell, 'th', token.align[col], token.width[
|
||||||
|
col]);
|
||||||
|
col += cell.colspan;
|
||||||
|
}
|
||||||
|
output += '</tr>';
|
||||||
|
}
|
||||||
|
output += '</thead>';
|
||||||
|
if (token.rows.length) {
|
||||||
|
output += '<tbody>';
|
||||||
|
for (i = 0; i < token.rows.length; i++) {
|
||||||
|
row = token.rows[i];
|
||||||
|
col = 0;
|
||||||
|
if (!row[0].emptyRow) {
|
||||||
|
output += '<tr>';
|
||||||
|
for (j = 0; j < row.length; j++) {
|
||||||
|
cell = row[j];
|
||||||
|
text = this.parser.parseInline(cell.tokens);
|
||||||
|
output += getTableCell(text, cell, 'td', token.align[col], token
|
||||||
|
.width[col]);
|
||||||
|
col += cell.colspan;
|
||||||
|
}
|
||||||
|
output += '</tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output += '</tbody>';
|
||||||
|
}
|
||||||
|
output += '</table>';
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTableCell = (text, cell, type, align, width) => {
|
||||||
|
if (!cell.rowspan) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const tag = `<${type}` +
|
||||||
|
`${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}` +
|
||||||
|
`${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}` +
|
||||||
|
`${align ? ` align=${align}` : ''}` +
|
||||||
|
`${width ? ` width=${width}` : ''}>`;
|
||||||
|
return `${tag + text}</${type}>\n`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitCells = (tableRow, count, prevRow = [], skipEmptyRows) => {
|
||||||
|
const cells = [...tableRow.trim().matchAll(/(?:[^|\\]|\\.?)+(?:\|+|$)/g)].map((x) => x[0]);
|
||||||
|
|
||||||
|
// Remove first/last cell in a row if whitespace only and no leading/trailing pipe
|
||||||
|
if (!cells[0]?.trim()) {
|
||||||
|
cells.shift();
|
||||||
|
}
|
||||||
|
if (!cells[cells.length - 1]?.trim()) {
|
||||||
|
cells.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
let numCols = 0;
|
||||||
|
let i, j, trimmedCell, prevCell, prevCols;
|
||||||
|
|
||||||
|
for (i = 0; i < cells.length; i++) {
|
||||||
|
trimmedCell = cells[i].split(/\|+$/)[0];
|
||||||
|
cells[i] = {
|
||||||
|
rowspan: 1,
|
||||||
|
colspan: Math.max(cells[i].length - trimmedCell.length, 1),
|
||||||
|
text: trimmedCell.trim().replace(/\\\|/g, '|')
|
||||||
|
// display escaped pipes as normal character
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle Rowspan
|
||||||
|
if (trimmedCell.slice(-1) === '^' && prevRow.length) {
|
||||||
|
// Find matching cell in previous row
|
||||||
|
prevCols = 0;
|
||||||
|
for (j = 0; j < prevRow.length; j++) {
|
||||||
|
prevCell = prevRow[j];
|
||||||
|
if ((prevCols === numCols) && (prevCell.colspan === cells[i].colspan)) {
|
||||||
|
// merge into matching cell in previous row (the "target")
|
||||||
|
cells[i].rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
||||||
|
cells[i].rowSpanTarget.text += ` ${cells[i].text.slice(0, -1)}`;
|
||||||
|
cells[i].rowSpanTarget.rowspan += 1;
|
||||||
|
cells[i].rowspan = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
prevCols += prevCell.colspan;
|
||||||
|
if (prevCols > numCols) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numCols += cells[i].colspan;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all cells have been merged, flag as an empty row
|
||||||
|
if (cells.length > 0 && skipEmptyRows && cells.length === cells.filter((cell) => {
|
||||||
|
return cell.rowspan === 0;
|
||||||
|
}).length) {
|
||||||
|
cells[0].emptyRow = true;
|
||||||
|
for (i = 0; i < cells.length; i++) {
|
||||||
|
cells[i].rowSpanTarget.rowspan -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force main cell rows to match header column count
|
||||||
|
if (numCols > count) {
|
||||||
|
cells.splice(count);
|
||||||
|
} else {
|
||||||
|
while (numCols < count) {
|
||||||
|
cells.push({
|
||||||
|
rowspan: 1,
|
||||||
|
colspan: 1,
|
||||||
|
text: ''
|
||||||
|
});
|
||||||
|
numCols += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cells;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<title>本地网页</title>
|
||||||
|
</head>
|
||||||
|
<style>
|
||||||
|
body,html{
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width:100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<body>
|
||||||
|
<!-- html组件使用 -->
|
||||||
|
<div id="html" style="overflow: auto;"></div>
|
||||||
|
<!-- 给echart图表使用 -->
|
||||||
|
<div id="echart" ></div>
|
||||||
|
<script type="text/javascript" src="./js/uni.webview.1.5.5.js"></script>
|
||||||
|
<script type="text/javascript" src="./js/echarts.min.js"></script>
|
||||||
|
<script type="text/javascript" src="./js/handler.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||||
|
<link href="./css/default.min.css" type="text/css" rel="stylesheet" />
|
||||||
|
<link href="./css/katex.min.css" type="text/css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
background: transparent;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
/* padding: 0;
|
||||||
|
margin: 0; */
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h1,
|
||||||
|
h3,
|
||||||
|
h2,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="layout">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./js/uni.webview.1.5.5.js"></script>
|
||||||
|
<script src="./js/highlight.min.js"></script>
|
||||||
|
<script src="./js/marked-highlight.js"></script>
|
||||||
|
<script src="./js/marked-tables.js"></script>
|
||||||
|
<script src="./js/katex.js"></script>
|
||||||
|
<script src="./js/marked.min.js"></script>
|
||||||
|
<script src="./js/marked-katex-extension.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var md;
|
||||||
|
|
||||||
|
function getBodyHeight() {
|
||||||
|
var targetDiv = document.body.querySelector("#layout")
|
||||||
|
let height = targetDiv.scrollHeight
|
||||||
|
document.body.style.height = (height) + 'px'
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'offsetHeight',
|
||||||
|
data: height + 5,
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'offsetHeight',
|
||||||
|
data: height
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetDiv = document.querySelector("#layout")
|
||||||
|
// 创建一个观察者对象并传入回调函数
|
||||||
|
var observer = new MutationObserver(function(mutations) {
|
||||||
|
mutations.forEach(function(mutation) {
|
||||||
|
if (mutation.type === 'childList' || mutation.type === 'attributes') {
|
||||||
|
getBodyHeight()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 配置观察选项:
|
||||||
|
var config = {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择需要观察变动的节点
|
||||||
|
observer.observe(targetDiv, config);
|
||||||
|
|
||||||
|
|
||||||
|
// 监听 div 的点击事件
|
||||||
|
targetDiv.addEventListener('click', function(event) {
|
||||||
|
let attr = {
|
||||||
|
text: "",
|
||||||
|
attr: "",
|
||||||
|
tag: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查点击的目标是否是 <a> 标签
|
||||||
|
if (event.target.tagName === 'A') {
|
||||||
|
// 阻止默认行为(如跳转)
|
||||||
|
event.preventDefault();
|
||||||
|
attr.text = event.target.textContent
|
||||||
|
attr.attr = event.target.href
|
||||||
|
attr.tag = "a"
|
||||||
|
|
||||||
|
} else if (event.target.tagName === 'IMG') {
|
||||||
|
attr.attr = event.target.src
|
||||||
|
attr.tag = "img"
|
||||||
|
}
|
||||||
|
if (attr.attr != '') {
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'click',
|
||||||
|
data: attr,
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'click',
|
||||||
|
data: attr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function processMarkdown(markdown) {
|
||||||
|
// 1. 分割成行
|
||||||
|
const lines = markdown.split('\n');
|
||||||
|
let inCodeBlock = false;
|
||||||
|
let codeBlockIndentation = 0;
|
||||||
|
let processedLines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
const trimmedLine = line.trimLeft();
|
||||||
|
|
||||||
|
// 检测代码块开始
|
||||||
|
if (trimmedLine.startsWith('```')) {
|
||||||
|
if (!inCodeBlock) {
|
||||||
|
// 进入代码块
|
||||||
|
inCodeBlock = true;
|
||||||
|
// 记录代码块的缩进级别
|
||||||
|
codeBlockIndentation = line.length - trimmedLine.length;
|
||||||
|
// 移除开始标记的缩进
|
||||||
|
processedLines.push(trimmedLine);
|
||||||
|
} else {
|
||||||
|
// 退出代码块
|
||||||
|
inCodeBlock = false;
|
||||||
|
// 移除结束标记的缩进
|
||||||
|
processedLines.push(trimmedLine);
|
||||||
|
}
|
||||||
|
} else if (inCodeBlock) {
|
||||||
|
// 代码块内的内容保持原有缩进
|
||||||
|
processedLines.push(line);
|
||||||
|
} else {
|
||||||
|
// 非代码块内容保持不变
|
||||||
|
processedLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function markdown(nr, isHtml) {
|
||||||
|
let content = ''
|
||||||
|
|
||||||
|
if (typeof nr == 'string') {
|
||||||
|
let cto = JSON.parse(nr);
|
||||||
|
if (cto.render == 'android') {
|
||||||
|
content = decodeURIComponent(atob(cto.value))
|
||||||
|
} else {
|
||||||
|
content = decodeURIComponent(cto.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (nr.render == 'android') {
|
||||||
|
content = decodeURIComponent(atob(nr.value))
|
||||||
|
} else {
|
||||||
|
content = decodeURIComponent(nr.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHtml) {
|
||||||
|
document.querySelector("#layout").innerHTML = content
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('layout').innerHTML = md.parse(processMarkdown(content));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBodyStyle(stylenr, isHtml) {
|
||||||
|
targetDiv.style = stylenr;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHtml() {
|
||||||
|
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'toValue',
|
||||||
|
data: targetDiv.innerhtml,
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'toValue',
|
||||||
|
data: targetDiv.innerhtml
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// 在iframe内部执行以下代码
|
||||||
|
var frameElement = window.frameElement;
|
||||||
|
var iframeId = frameElement == null ? "" : frameElement.getAttribute('id');
|
||||||
|
|
||||||
|
|
||||||
|
// 等待初始化完毕
|
||||||
|
document.addEventListener('UniAppJSBridgeReady', () => {
|
||||||
|
|
||||||
|
md = new marked.Marked(
|
||||||
|
markedHighlight({
|
||||||
|
emptyLangClass: 'hljs',
|
||||||
|
langPrefix: 'hljs language-',
|
||||||
|
highlight(code, lang, info) {
|
||||||
|
// const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||||
|
// return hljs.highlight(code, {
|
||||||
|
// language
|
||||||
|
// }).value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
md.use(extendedTables());
|
||||||
|
md.use(markedKatex({
|
||||||
|
throwOnError: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
md.setOptions({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.parent.postMessage({
|
||||||
|
action: 'onJSBridgeReady',
|
||||||
|
data: '',
|
||||||
|
iframeId: iframeId
|
||||||
|
}, '*');
|
||||||
|
uni.postMessage({
|
||||||
|
data: {
|
||||||
|
action: 'onJSBridgeReady'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<script>
|
||||||
|
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||||
|
CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||||
|
</script>
|
||||||
|
<title></title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/main"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
"is-custom-runtime": false,
|
||||||
|
"compile": true,
|
||||||
|
watchPathIgnorePatterns: ['/node_modules/', '/dist/', '/.git/'],
|
||||||
|
moduleFileExtensions: ['js'],
|
||||||
|
rootDir: __dirname,
|
||||||
|
testMatch: ["<rootDir>/pages/**/*test.[jt]s?(x)","<rootDir>/pages/*test.[jt]s?(x)"],
|
||||||
|
testPathIgnorePatterns: [
|
||||||
|
'/node_modules/',
|
||||||
|
'/testbao/',
|
||||||
|
'/tmicon/',
|
||||||
|
'/tmicon/',
|
||||||
|
'/myUtools/',
|
||||||
|
'/myUtools/',
|
||||||
|
'/uni_modules/',
|
||||||
|
'/unpackage/',
|
||||||
|
'/static/',
|
||||||
|
'/hybrid/',
|
||||||
|
'/jtest/',
|
||||||
|
'/docsBuildCli/',
|
||||||
|
'/docweb/',
|
||||||
|
'/logo资源/',
|
||||||
|
'/tmy-ui/',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 114 KiB |
|
After Width: | Height: | Size: 158 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
// #ifndef MP
|
||||||
|
import { showRunLogo, hideRunLogo ,XRUN_START_LOGO_OPTS} from "@/uni_modules/x-runstart-logo"
|
||||||
|
import { showModal,X_MODAL_TYPE } from "@/uni_modules/x-modal-s"
|
||||||
|
|
||||||
|
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
|
||||||
|
// #ifndef MP
|
||||||
|
function setAgree() {
|
||||||
|
uni.setStorageSync('xTmui4.0Xieyi', 'true')
|
||||||
|
}
|
||||||
|
function getAgree() : boolean {
|
||||||
|
let tonyixie = uni.getStorageSync('xTmui4.0Xieyi')
|
||||||
|
|
||||||
|
return tonyixie == null || tonyixie == ''
|
||||||
|
}
|
||||||
|
export function showAgree() {
|
||||||
|
const needAgree = getAgree()
|
||||||
|
if (!needAgree) return;
|
||||||
|
showRunLogo({
|
||||||
|
bgColor: '#006deb',
|
||||||
|
logo: 'logo.png',
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
|
logoPadding: 20,
|
||||||
|
round: 14,
|
||||||
|
logoBgColor: '#ffffff',
|
||||||
|
rightText: 'tmui4.0x / 2025 by tmzdy',
|
||||||
|
fontSize: 11,
|
||||||
|
fontColor: '#ffffff'
|
||||||
|
} as XRUN_START_LOGO_OPTS)
|
||||||
|
showModal({
|
||||||
|
title: "隐私政策",
|
||||||
|
content: "<div><span>我同意</span><a style='color:rgb(5, 121, 255);' href='https://xui.tmui.design'>《TMUIX4.0使用协议》</a><span>我确认我使用的是正版tmuix4.0组件及附带插件软件,并承担相应法律风险,同意请点我同意,不同意请点不同意终止访问.</span></div>",
|
||||||
|
cancelText: '不同意',
|
||||||
|
confirmText: "我同意",
|
||||||
|
height:120,
|
||||||
|
confirmIcon:"EB80",
|
||||||
|
cancelIcon:"ECA0",
|
||||||
|
clickMaskClose: false,
|
||||||
|
clickLink(url:string){
|
||||||
|
console.log(url)
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title:"阅读协议",
|
||||||
|
content:`这个是协议,请阅读`,
|
||||||
|
showCancel:false,
|
||||||
|
confirmText:"已阅读"
|
||||||
|
})
|
||||||
|
},
|
||||||
|
confirm() {
|
||||||
|
setAgree()
|
||||||
|
setTimeout(function () {
|
||||||
|
hideRunLogo()
|
||||||
|
}, 1000);
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
// #ifdef APP
|
||||||
|
uni.exit()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
} as X_MODAL_TYPE)
|
||||||
|
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
type demoStoreType = {
|
||||||
|
homeMenuOpened:boolean
|
||||||
|
}
|
||||||
|
export const demoStore = reactive({
|
||||||
|
homeMenuOpened:false
|
||||||
|
} as demoStoreType)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import App from './App'
|
||||||
|
import {xui} from "@/uni_modules/tmx-ui/index.uts"
|
||||||
|
import {Tmui4xOptions} from "@/uni_modules/tmx-ui/interface.uts"
|
||||||
|
import { I18nOptions } from "@/uni_modules/x-vuei18n-s/interface.uts"
|
||||||
|
|
||||||
|
import en from "@/uni_modules/tmx-ui/localLanuage/en.json"
|
||||||
|
import zhHans from "@/uni_modules/tmx-ui/localLanuage/zh-Hans.json"
|
||||||
|
import zhHant from "@/uni_modules/tmx-ui/localLanuage/zh-Hant.json"
|
||||||
|
import ko from "@/uni_modules/tmx-ui/localLanuage/ko.json"
|
||||||
|
import ja from "@/uni_modules/tmx-ui/localLanuage/ja.json"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件语言包(与自定义语言包合并,共用一个实例)
|
||||||
|
*/
|
||||||
|
const messages : UTSJSONObject = {
|
||||||
|
"en":{
|
||||||
|
...en,
|
||||||
|
"hellow":"Hi~"
|
||||||
|
},
|
||||||
|
"zh-Hans":{
|
||||||
|
...zhHans,
|
||||||
|
"hellow":"哈喽"
|
||||||
|
},
|
||||||
|
"zh-Hant":zhHant,
|
||||||
|
"ko":ko,
|
||||||
|
"ja":ja
|
||||||
|
}
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 医院救援平台全局配置
|
||||||
|
* - designSize 375:与《移动端 UI 设计稿 v2》基准画布一致
|
||||||
|
* - color #2563EB:医疗蓝主色
|
||||||
|
* - dark light:医疗场景锁定亮色,避免跟随系统暗黑造成还原偏差
|
||||||
|
*/
|
||||||
|
const hrConfig : UTSJSONObject = {
|
||||||
|
designSize: 375,
|
||||||
|
unit: 'px',
|
||||||
|
color: '#2563EB',
|
||||||
|
dark: 'light',
|
||||||
|
inputRadius: '11',
|
||||||
|
buttonRadius: '12',
|
||||||
|
cardRound: '13',
|
||||||
|
tagRadius: '6',
|
||||||
|
placeholderStyle: 'color:#9CA3AF'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
app.use(xui, {
|
||||||
|
i18nOptions: { locale: "zh-Hans", messages } as I18nOptions,
|
||||||
|
config: hrConfig
|
||||||
|
} as Tmui4xOptions)
|
||||||
|
return {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"name": "医院救援管理平台",
|
||||||
|
"appid": "__UNI__B73859A",
|
||||||
|
"description": "医院智慧后勤管理平台 · 移动端(救援人员端 / 仓管人员端)",
|
||||||
|
"versionName": "2.0.0",
|
||||||
|
"versionCode": "200",
|
||||||
|
"uni-app-x": {},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
"quickapp": {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
"mp-weixin": {
|
||||||
|
"appid": "wxb30698c4b9f69030",
|
||||||
|
"darkmode": true,
|
||||||
|
"defaultAppTheme": "dark",
|
||||||
|
"permission": {
|
||||||
|
"scope.camera": {
|
||||||
|
"desc": "用于拍照及扫码识别"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"es6": true,
|
||||||
|
"minified": true
|
||||||
|
},
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-alipay": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-baidu": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-toutiao": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"uniStatistics": {
|
||||||
|
"enable": false
|
||||||
|
},
|
||||||
|
"vueVersion": "3",
|
||||||
|
"app": {
|
||||||
|
"defaultAppTheme": "light",
|
||||||
|
"distribute": {
|
||||||
|
"modules": {
|
||||||
|
"uni-createInnerAudioContext": {}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"android": {
|
||||||
|
"hdpi": "unpackage/res/icons/72x72.png",
|
||||||
|
"xhdpi": "unpackage/res/icons/96x96.png",
|
||||||
|
"xxhdpi": "unpackage/res/icons/144x144.png",
|
||||||
|
"xxxhdpi": "unpackage/res/icons/192x192.png"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"appstore": "unpackage/res/icons/1024x1024.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"title": "医院救援管理平台",
|
||||||
|
"rpxCalcMaxDeviceWidth": 480,
|
||||||
|
"rpxCalcBaseDeviceWidth": 480,
|
||||||
|
"rpxCalcIncludeWidth": 750,
|
||||||
|
"router": {
|
||||||
|
"mode": "hash",
|
||||||
|
"base": "./"
|
||||||
|
},
|
||||||
|
"devServer": {
|
||||||
|
"port": 3721
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app-android": {
|
||||||
|
"distribute": {
|
||||||
|
"modules": {
|
||||||
|
"uni-createInnerAudioContext": {}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"hdpi": "",
|
||||||
|
"xhdpi": "",
|
||||||
|
"xxhdpi": "",
|
||||||
|
"xxxhdpi": ""
|
||||||
|
},
|
||||||
|
"splashScreens": {
|
||||||
|
"default": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app-ios": {
|
||||||
|
"distribute": {
|
||||||
|
"modules": {
|
||||||
|
"uni-createInnerAudioContext": {}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"appstore": ""
|
||||||
|
},
|
||||||
|
"splashScreens": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app-harmony": {
|
||||||
|
"distribute": {
|
||||||
|
"bundleName": "UNI.APP.B73859A",
|
||||||
|
"icons": {
|
||||||
|
"foreground": "logo资源/logo/logo-rect.png",
|
||||||
|
"background": "logo资源/logo/logo-rect.png"
|
||||||
|
},
|
||||||
|
"splashScreens": {
|
||||||
|
"startWindowIcon": "logo资源/logo/logo-rect.png"
|
||||||
|
},
|
||||||
|
"signingConfigs": {
|
||||||
|
"default": {
|
||||||
|
"certpath": "/Volumes/app/myProjects/tmui4xPravite/logo资源/鸿蒙证书/鸿蒙调试证书/调试证书.cer",
|
||||||
|
"keyAlias": "tmui4xTestHaHa",
|
||||||
|
"keyPassword": "0000001AAE6DEADCC1BD7D1544384BE5806C3858AACC25643D9A0ADCE77E7FE5D28DA4764A4EBDE65CD3",
|
||||||
|
"profile": "/Volumes/app/myProjects/tmui4xPravite/logo资源/鸿蒙证书/鸿蒙调试证书/调试证书描述Debug.p7b",
|
||||||
|
"signAlg": "SHA256withECDSA",
|
||||||
|
"storeFile": "/Volumes/app/myProjects/tmui4xPravite/logo资源/鸿蒙证书/鸿蒙调试证书/调试keystore.p12",
|
||||||
|
"storePassword": "0000001AD3619DBB1D23483C207D64709AA30A897411EBC5C5C63FA88A89F2B1053DC049F051BAB1CD8A"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"certpath": "/Volumes/app/myProjects/tmui4xPravite/logo资源/鸿蒙证书/tmui4xDebug.cer",
|
||||||
|
"keyAlias": "tmui4xDebug",
|
||||||
|
"keyPassword": "0000001C407505F426690725620F8B5F257FA36D43963607BF4DE48A36962C503A468B14FB553ADA962A5EBB",
|
||||||
|
"profile": "/Volumes/app/myProjects/tmui4xPravite/logo资源/鸿蒙证书/tmui4xDebugRelease.p7b",
|
||||||
|
"signAlg": "SHA256withECDSA",
|
||||||
|
"storeFile": "/Volumes/app/myProjects/tmui4xPravite/logo资源/鸿蒙证书/tmui4xKeyStore.p12",
|
||||||
|
"storePassword": "0000001C388F5AEF60B4A6713552A68C80FEFE801042A072E40FEF6B23EB74D31772502E96E5DBE47C63E36D"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,549 @@
|
|||||||
|
/**
|
||||||
|
* 医院救援管理平台 · 移动端 Mock 数据
|
||||||
|
* ------------------------------------------------------------
|
||||||
|
* 所有页面数据均从本文件读取,后续对接接口时只需替换本文件的取数实现,
|
||||||
|
* 页面层无需改动(保持低耦合)。
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
HrUser, HrTodoTask, HrActivity, HrToolBox,
|
||||||
|
HrCatalog, HrGroup, HrSubject, HrMaterial,
|
||||||
|
HrExamRecord, HrCert, HrExamPlan, HrQuestion, HrOption, HrPracticeMeta,
|
||||||
|
HrInjuryMark, HrVital, HrTimelineNode, HrVictim,
|
||||||
|
HrEventMember, HrEventItem, HrEventDetail,
|
||||||
|
HrScanFeedback, HrScanRecord, HrStockItem, HrGoods, HrDevice, HrDeviceCheck,
|
||||||
|
HrMsg, HrMsgGroup, HrContact, HrContactGroup, HrOfflineTask,
|
||||||
|
HrSettingSwitch, HrSettingRow, HrDesignSection, HrSwatch, HrTokenRow
|
||||||
|
} from './types.uts'
|
||||||
|
|
||||||
|
/* ============================ 用户 ============================ */
|
||||||
|
export const rescuerUser : HrUser = {
|
||||||
|
name: '张明远', avatar: '张', title: '主治医师', dept: '急诊科', jobNo: '1247',
|
||||||
|
role: 'rescuer', roleLabel: '救援人员 · 急救中心',
|
||||||
|
tags: ['ACLS 认证', 'BLS 认证', '创伤急救', '培训等级 B'],
|
||||||
|
hours: 42.5, courseCount: 12, certCount: 3
|
||||||
|
} as HrUser
|
||||||
|
|
||||||
|
export const keeperUser : HrUser = {
|
||||||
|
name: '陈志强', avatar: '陈', title: '物资管理员', dept: '后勤保障科', jobNo: '2083',
|
||||||
|
role: 'keeper', roleLabel: '仓管人员 · 后勤保障科',
|
||||||
|
tags: ['仓储管理', '设备维保', '危化品资质', '培训等级 A'],
|
||||||
|
hours: 26.0, courseCount: 8, certCount: 2
|
||||||
|
} as HrUser
|
||||||
|
|
||||||
|
/* ============================ 学员首页 ============================ */
|
||||||
|
export const todoTasks : HrTodoTask[] = [
|
||||||
|
{ icon: '⚑', iconTheme: 'amb', title: '成人 CPR 课题 · 还剩 2 份资料', sub: '09-30 截止 · 已学 68%', action: '继续 ›' } as HrTodoTask,
|
||||||
|
{ icon: '✓', iconTheme: 'red', title: '年度急救复训 · 报名确认', sub: '09-30 截止 · 未开始', action: '去完成 ›' } as HrTodoTask
|
||||||
|
]
|
||||||
|
|
||||||
|
export const activities : HrActivity[] = [
|
||||||
|
{ icon: '▦', iconTheme: 'pri', title: '完成「创伤评估」资料学习', sub: '今天 09:20 · 学习 38 分钟' } as HrActivity,
|
||||||
|
{ icon: '✗', iconTheme: 'red', title: '错题 +2(已复习 1)', sub: '昨天 21:12 · 心肺复苏' } as HrActivity,
|
||||||
|
{ icon: '▤', iconTheme: 'grn', title: '获得「AED 操作」证书', sub: '2 天前 · 有效期至 2028-09' } as HrActivity
|
||||||
|
]
|
||||||
|
|
||||||
|
export const toolBox : HrToolBox[] = [
|
||||||
|
{ icon: 'book-2-line', name: '课程库', iconBg: '#EFF6FF', iconColor: '#2563EB' } as HrToolBox,
|
||||||
|
{ icon: 'file-edit-line', name: '我的考试', iconBg: '#FEF3C7', iconColor: '#B45309' } as HrToolBox,
|
||||||
|
{ icon: 'award-line', name: '我的证书', iconBg: '#ECFDF5', iconColor: '#059669' } as HrToolBox,
|
||||||
|
{ icon: 'file-warning-line', name: '错题本', iconBg: '#FEF2F2', iconColor: '#DC2626' } as HrToolBox
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 课程库 ============================ */
|
||||||
|
export const catalogList : HrCatalog[] = [
|
||||||
|
{ id: 'c1', name: '急救基础', count: 11, icon: '+', iconBg: '#FEF2F2', iconColor: '#DC2626' } as HrCatalog,
|
||||||
|
{ id: 'c2', name: '创伤急救', count: 9, icon: '⚡', iconBg: '#FEF3C7', iconColor: '#B45309' } as HrCatalog,
|
||||||
|
{ id: 'c3', name: 'ACLS / BLS', count: 8, icon: '✓', iconBg: '#ECFDF5', iconColor: '#059669' } as HrCatalog,
|
||||||
|
{ id: 'c4', name: '检伤分类', count: 6, icon: '♡', iconBg: '#EFF6FF', iconColor: '#2563EB' } as HrCatalog,
|
||||||
|
{ id: 'c5', name: '设备操作', count: 5, icon: '⚙', iconBg: '#F5F3FF', iconColor: '#7C3AED' } as HrCatalog,
|
||||||
|
{ id: 'c6', name: '心理干预', count: 4, icon: '◈', iconBg: '#FFFBEB', iconColor: '#92400E' } as HrCatalog,
|
||||||
|
{ id: 'c7', name: '中毒急救', count: 3, icon: '☣', iconBg: '#FEF2F2', iconColor: '#B91C1C' } as HrCatalog,
|
||||||
|
{ id: 'c8', name: '应急指挥', count: 3, icon: '⚠', iconBg: '#ECFEFF', iconColor: '#0E7490' } as HrCatalog
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 每个一级分类下的二级分组(三级课题为唯一可点项) */
|
||||||
|
export const courseGroups : HrGroup[] = [
|
||||||
|
{
|
||||||
|
name: '心肺复苏', count: 4,
|
||||||
|
items: [
|
||||||
|
{ no: '01', name: '成人 CPR(高级生命支持)', meta: '资料 6 份 · 题库 50 · 必修' } as HrSubject,
|
||||||
|
{ no: '02', name: '儿童 CPR', meta: '资料 4 份 · 题库 32 · 必修' } as HrSubject,
|
||||||
|
{ no: '03', name: '婴儿 CPR', meta: '资料 4 份 · 题库 28 · 必修' } as HrSubject,
|
||||||
|
{ no: '04', name: '溺水者心肺复苏', meta: '资料 3 份 · 题库 18 · 选修' } as HrSubject
|
||||||
|
]
|
||||||
|
} as HrGroup,
|
||||||
|
{
|
||||||
|
name: 'AED 操作', count: 3,
|
||||||
|
items: [
|
||||||
|
{ no: '01', name: 'AED 设备认知与自检', meta: '资料 3 份 · 题库 20 · 必修' } as HrSubject,
|
||||||
|
{ no: '02', name: 'AED 操作流程与除颤时机', meta: '资料 4 份 · 题库 26 · 必修' } as HrSubject,
|
||||||
|
{ no: '03', name: '特殊场景 AED(潮湿 / 儿童 / 起搏器)', meta: '资料 2 份 · 题库 15 · 选修' } as HrSubject
|
||||||
|
]
|
||||||
|
} as HrGroup,
|
||||||
|
{
|
||||||
|
name: '气道管理', count: 4,
|
||||||
|
items: [
|
||||||
|
{ no: '01', name: '气道开放与评估', meta: '资料 3 份 · 题库 22 · 必修' } as HrSubject,
|
||||||
|
{ no: '02', name: '球囊面罩通气', meta: '资料 3 份 · 题库 18 · 必修' } as HrSubject,
|
||||||
|
{ no: '03', name: '气管插管术', meta: '资料 5 份 · 题库 30 · 认证' } as HrSubject,
|
||||||
|
{ no: '04', name: '喉罩与声门上气道', meta: '资料 2 份 · 题库 16 · 选修' } as HrSubject
|
||||||
|
]
|
||||||
|
} as HrGroup
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 课题详情对应的资料列表 */
|
||||||
|
export const subjectMaterials : HrMaterial[] = [
|
||||||
|
{ id: 'm1', kind: 'video', badge: '视频', badgeColor: '#7C3AED', icon: '▶', title: '高质量胸外按压操作示范', meta: '视频 · 时长 08:24 · 09-20 更新', summary: '由急诊科教学组录制,演示成人胸外按压的手位、深度、频率与胸廓回弹要点,并对比 3 处常见错误动作。', from: '成人 CPR(高级生命支持) · 急救基础 / 心肺复苏', progress: 62 } as HrMaterial,
|
||||||
|
{ id: 'm2', kind: 'doc', badge: 'PDF', badgeColor: '#DC2626', icon: 'PDF', title: '成人 CPR 操作流程(2025 版)', meta: '文档 · PDF · 2.4 MB · 12 页', summary: '依据《2025 心肺复苏与心血管急救指南》整理的操作流程速查手册,含成人基础生命支持流程图、除颤时机判定表与 3 个常见错误对照。', from: '成人 CPR(高级生命支持) · 急救基础 / 心肺复苏', progress: 0 } as HrMaterial,
|
||||||
|
{ id: 'm3', kind: 'doc', badge: 'DOC', badgeColor: '#2563EB', icon: 'DOC', title: '高质量胸外按压要点', meta: '文档 · Word · 860 KB · 6 页', summary: '按压深度 5~6cm、频率 100~120 次/分,每次按压后必须保证胸廓完全回弹,中断控制在 10 秒以内。', from: '成人 CPR(高级生命支持) · 急救基础 / 心肺复苏', progress: 100 } as HrMaterial,
|
||||||
|
{ id: 'm4', kind: 'doc', badge: 'PPT', badgeColor: '#D97706', icon: 'PPT', title: '心肺复苏教学课件', meta: '文档 · PPT · 5.6 MB · 38 页', summary: '急救基础课程配套课件,覆盖 BLS 全流程、团队分工与考核要点。', from: '成人 CPR(高级生命支持) · 急救基础 / 心肺复苏', progress: 0 } as HrMaterial,
|
||||||
|
{ id: 'm5', kind: 'doc', badge: 'XLS', badgeColor: '#059669', icon: 'XLS', title: 'CPR 考核评分表', meta: '文档 · Excel · 124 KB · 2 个工作表', summary: '实操考核使用评分表,包含操作步骤分值、扣分项与总评标准。', from: '成人 CPR(高级生命支持) · 急救基础 / 心肺复苏', progress: 0 } as HrMaterial,
|
||||||
|
{ id: 'm6', kind: 'video', badge: '视频', badgeColor: '#7C3AED', icon: '▶', title: '双人配合通气节律演示', meta: '视频 · 时长 06:12 · 09-12 更新', summary: '双人配合心肺复苏中通气节律与按压衔接的演示视频。', from: '成人 CPR(高级生命支持) · 急救基础 / 心肺复苏', progress: 0 } as HrMaterial
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 练习 / 考试 / 证书 ============================ */
|
||||||
|
export const practiceMeta : HrPracticeMeta = {
|
||||||
|
subject: '成人 CPR(高级生命支持)',
|
||||||
|
total: 50,
|
||||||
|
minutes: 30,
|
||||||
|
goal: '80%',
|
||||||
|
lastTime: '09-21 21:08',
|
||||||
|
lastDone: 18,
|
||||||
|
lastRate: 72
|
||||||
|
} as HrPracticeMeta
|
||||||
|
|
||||||
|
export const quizQuestions : HrQuestion[] = [
|
||||||
|
{
|
||||||
|
id: 'q1', stem: '成人心肺复苏时,胸外按压的深度应达到多少?', answer: 1,
|
||||||
|
options: [
|
||||||
|
{ key: 'A', text: '3 ~ 4 cm' } as HrOption,
|
||||||
|
{ key: 'B', text: '5 ~ 6 cm' } as HrOption,
|
||||||
|
{ key: 'C', text: '6 ~ 8 cm' } as HrOption,
|
||||||
|
{ key: 'D', text: '按压至胸廓完全塌陷即可' } as HrOption
|
||||||
|
],
|
||||||
|
analysis: '成人胸外按压深度为 5~6 cm,过浅无法产生有效心排血量,过深易造成肋骨骨折等损伤。',
|
||||||
|
from: '成人 CPR'
|
||||||
|
} as HrQuestion,
|
||||||
|
{
|
||||||
|
id: 'q2', stem: '成人胸外按压的频率应保持在每分钟多少次?', answer: 1,
|
||||||
|
options: [
|
||||||
|
{ key: 'A', text: '60 ~ 80 次' } as HrOption,
|
||||||
|
{ key: 'B', text: '100 ~ 120 次' } as HrOption,
|
||||||
|
{ key: 'C', text: '120 ~ 140 次' } as HrOption,
|
||||||
|
{ key: 'D', text: '越快越好' } as HrOption
|
||||||
|
],
|
||||||
|
analysis: '指南推荐成人胸外按压频率为 100~120 次/分,过快会降低按压深度与回弹质量。',
|
||||||
|
from: '成人 CPR'
|
||||||
|
} as HrQuestion,
|
||||||
|
{
|
||||||
|
id: 'q3', stem: '球囊面罩通气时,成人推荐的通气频率是?', answer: 0,
|
||||||
|
options: [
|
||||||
|
{ key: 'A', text: '每 6 秒 1 次(10 次/分)' } as HrOption,
|
||||||
|
{ key: 'B', text: '每 3 秒 1 次(20 次/分)' } as HrOption,
|
||||||
|
{ key: 'C', text: '每 10 秒 1 次(6 次/分)' } as HrOption,
|
||||||
|
{ key: 'D', text: '每 6 秒 1 次(6 次/分)' } as HrOption
|
||||||
|
],
|
||||||
|
analysis: '建立高级气道后,成人通气频率为每 6 秒 1 次,即 10 次/分,且不再与按压交替中断。',
|
||||||
|
from: '气道管理'
|
||||||
|
} as HrQuestion,
|
||||||
|
{
|
||||||
|
id: 'q4', stem: '发现有人倒地无反应、无呼吸,首先应当?', answer: 2,
|
||||||
|
options: [
|
||||||
|
{ key: 'A', text: '立即开始胸外按压' } as HrOption,
|
||||||
|
{ key: 'B', text: '先寻找 AED' } as HrOption,
|
||||||
|
{ key: 'C', text: '判断环境安全并呼救、启动应急系统' } as HrOption,
|
||||||
|
{ key: 'D', text: '掐人中并等待家属到来' } as HrOption
|
||||||
|
],
|
||||||
|
analysis: '施救前必须先确认现场环境安全,随后判断反应与呼吸,呼救并启动应急反应系统,再开始 CPR。',
|
||||||
|
from: '成人 CPR'
|
||||||
|
} as HrQuestion,
|
||||||
|
{
|
||||||
|
id: 'q5', stem: 'AED 到达后,正确做法是?', answer: 1,
|
||||||
|
options: [
|
||||||
|
{ key: 'A', text: '先停止按压擦干胸部再贴电极片' } as HrOption,
|
||||||
|
{ key: 'B', text: '尽量不中断按压,开机贴片后按提示操作' } as HrOption,
|
||||||
|
{ key: 'C', text: '等专业医护到场再使用' } as HrOption,
|
||||||
|
{ key: 'D', text: '除颤后不需要继续按压' } as HrOption
|
||||||
|
],
|
||||||
|
analysis: 'AED 到达应尽快使用,尽量缩短按压中断;电击后立即恢复胸外按压。',
|
||||||
|
from: 'AED 操作'
|
||||||
|
} as HrQuestion
|
||||||
|
]
|
||||||
|
|
||||||
|
export const examPlans : HrExamPlan[] = [
|
||||||
|
{ name: 'BLS 基础生命支持 · 年度复训', priority: 0, countdown: '距开考 02 天 04 小时 12 分', time: '2026-09-24 14:00', place: '临床技能中心 3F' } as HrExamPlan
|
||||||
|
]
|
||||||
|
|
||||||
|
export const examRecords : HrExamRecord[] = [
|
||||||
|
{ name: '心肺复苏理论测验', date: '09-18', score: 92, passed: true } as HrExamRecord,
|
||||||
|
{ name: 'AED 操作规范考核', date: '08-30', score: 88, passed: true } as HrExamRecord,
|
||||||
|
{ name: '创伤急救理论测验', date: '08-12', score: 71, passed: false } as HrExamRecord
|
||||||
|
]
|
||||||
|
|
||||||
|
export const certList : HrCert[] = [
|
||||||
|
{ name: 'AED 操作证书', gotAt: '2025-09', expireAt: '2028-09', status: '有效', statusTheme: 'grn' } as HrCert,
|
||||||
|
{ name: '成人心肺复苏(CPR)证书', gotAt: '2025-06', expireAt: '2028-06', status: '有效', statusTheme: 'grn' } as HrCert,
|
||||||
|
{ name: 'BLS 基础生命支持证书', gotAt: '2024-10', expireAt: '2026-10-15', status: '即将到期', statusTheme: 'amb' } as HrCert
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 错题本 / 学习中心共用 */
|
||||||
|
export const wrongQuestions : HrQuestion[] = [
|
||||||
|
{
|
||||||
|
id: 'w1', stem: '成人心肺复苏时,胸外按压的深度应达到多少?', answer: 1,
|
||||||
|
options: [ { key: 'A', text: '3 ~ 4 cm' } as HrOption, { key: 'B', text: '5 ~ 6 cm' } as HrOption ],
|
||||||
|
analysis: '成人胸外按压深度为 5~6 cm。', from: '成人 CPR'
|
||||||
|
} as HrQuestion,
|
||||||
|
{
|
||||||
|
id: 'w2', stem: '成人胸外按压的频率应保持在每分钟多少次?', answer: 1,
|
||||||
|
options: [ { key: 'A', text: '60 ~ 80 次' } as HrOption, { key: 'B', text: '100 ~ 120 次' } as HrOption ],
|
||||||
|
analysis: '推荐频率为 100~120 次/分。', from: '成人 CPR'
|
||||||
|
} as HrQuestion,
|
||||||
|
{
|
||||||
|
id: 'w3', stem: '球囊面罩通气时,成人推荐的通气频率是?', answer: 0,
|
||||||
|
options: [ { key: 'A', text: '每 6 秒 1 次(10 次/分)' } as HrOption, { key: 'D', text: '每 6 秒 1 次' } as HrOption ],
|
||||||
|
analysis: '建立高级气道后每 6 秒 1 次,即 10 次/分。', from: '气道管理'
|
||||||
|
} as HrQuestion
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 伤员 ============================ */
|
||||||
|
export const victimList : HrVictim[] = [
|
||||||
|
{ id: 'v1', name: '王建国', gender: '男', age: 52, code: 'TRIAGE-20260922-0001', level: 1, injury: '开放性颅脑损伤、左股骨骨折 · 意识模糊,GCS 9 分', status: '已转运 · 协和医院急诊', statusTheme: 'pri', createdAt: '14:21 建档' } as HrVictim,
|
||||||
|
{ id: 'v2', name: '李华', gender: '男', age: 35, code: 'TRIAGE-20260922-0003', level: 2, injury: '头部开放性外伤、右前臂疑似骨折、左小腿挤压伤', status: '转运中 · 平车 → 创伤外科', statusTheme: 'amb', createdAt: '14:23 建档' } as HrVictim,
|
||||||
|
{ id: 'v3', name: '陈小梅', gender: '女', age: 28, code: 'TRIAGE-20260922-0006', level: 3, injury: '右膝软组织挫伤 · 可自行行走,生命体征平稳', status: '待转运 · 轻症观察区', statusTheme: 'grn', createdAt: '14:31 建档' } as HrVictim,
|
||||||
|
{ id: 'v4', name: '赵国强', gender: '男', age: 41, code: 'TRIAGE-20260922-0008', level: 2, injury: '胸部挤压伤、多发肋骨骨折(4-7 肋) · 呼吸 26 次/分', status: '未转运 · 现场待命', statusTheme: 'gray', createdAt: '14:38 建档' } as HrVictim,
|
||||||
|
{ id: 'v5', name: '吴长胜', gender: '男', age: 61, code: 'TRIAGE-20260922-0012', level: 4, injury: '重度颅脑损伤 · 无呼吸无脉搏,经确认已无生命体征', status: '已确认 · 待移交', statusTheme: 'gray', createdAt: '14:45 建档' } as HrVictim
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 检伤分级(一行四级) */
|
||||||
|
export const gradeList : string[] = ['轻伤', '重伤', '危重', '死亡']
|
||||||
|
|
||||||
|
/** 2D 人体图鉴标记 */
|
||||||
|
export const injuryMarks : HrInjuryMark[] = [
|
||||||
|
{ no: '1', title: '头部 · 开放性外伤', sub: '顶枕部裂伤约 4cm · 出血中', theme: 'red' } as HrInjuryMark,
|
||||||
|
{ no: '2', title: '右前臂 · 疑似骨折', sub: '肿胀畸形 · 触痛明显', theme: 'amb' } as HrInjuryMark,
|
||||||
|
{ no: '3', title: '左小腿 · 挤压伤', sub: '皮肤瘀斑 · 局部压痛', theme: 'amb' } as HrInjuryMark
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 生命体征 */
|
||||||
|
export const vitalSigns : HrVital[] = [
|
||||||
|
{ value: '37.8', unit: 'T ℃', abnormal: false } as HrVital,
|
||||||
|
{ value: '102', unit: 'P 次/分', abnormal: true } as HrVital,
|
||||||
|
{ value: '22', unit: 'R 次/分', abnormal: false } as HrVital,
|
||||||
|
{ value: '95/60', unit: 'BP mmHg', abnormal: true } as HrVital,
|
||||||
|
{ value: '88', unit: 'SpO₂ %', abnormal: true } as HrVital
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 检伤评估下拉 */
|
||||||
|
export const triagePickList : string[] = ['行走能力', '呼吸', '循环', '意识 AVPU']
|
||||||
|
export const triagePickValues : string[] = ['不可走', '受限', '出血', '声音']
|
||||||
|
export const triagePickOptions : string[][] = [
|
||||||
|
['可走', '不可走'],
|
||||||
|
['正常', '受限', '无呼吸'],
|
||||||
|
['循环正常', '出血', '脉搏消失'],
|
||||||
|
['清醒', '声音', '疼痛', '无反应']
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 伤员详情:流转时间线 */
|
||||||
|
export const victimTimeline : HrTimelineNode[] = [
|
||||||
|
{ time: '14:23', title: '现场检伤建档', sub: '张明 于坍塌现场完成建档,标记 3 处伤情,判定「重伤」', theme: 'normal' } as HrTimelineNode,
|
||||||
|
{ time: '14:28', title: '分级确认', sub: '现场检伤组 3 组复核,维持「重伤」· 操作人 张明', theme: 'ok' } as HrTimelineNode,
|
||||||
|
{ time: '14:35', title: '转运交接', sub: '平车送达 协和医院 创伤外科,转出方 张明 / 接收方 刘伟 电子签字', theme: 'normal' } as HrTimelineNode,
|
||||||
|
{ time: '15:10', title: '院内处置', sub: '创伤外科接诊,建立静脉通路 + 持续心电监护,清创缝合待执行', theme: 'warn' } as HrTimelineNode
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 处置记录时间线 */
|
||||||
|
export const treatRecords : HrTimelineNode[] = [
|
||||||
|
{ time: '15:25', title: '分级调整:重伤 → 危重', sub: 'BP 88/56 mmHg · 判断失血性休克进展', theme: 'warn' } as HrTimelineNode,
|
||||||
|
{ time: '15:10', title: '建立静脉通路', sub: '左上肢 · 16G · 生理盐水 500 ml 快滴', theme: 'normal' } as HrTimelineNode,
|
||||||
|
{ time: '15:02', title: '止血包扎', sub: '右前臂加压包扎 + 额部敷料', theme: 'ok' } as HrTimelineNode,
|
||||||
|
{ time: '14:55', title: '检伤分级', sub: '判定「重伤」· START 协议', theme: 'normal' } as HrTimelineNode
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 转运 ============================ */
|
||||||
|
export const hospitalList : string[] = ['协和医院 · 急诊创伤中心', '市第一人民医院 · 急诊', '同仁医院 · 急诊']
|
||||||
|
export const hospitalMeta : string[] = ['距现场 6.2 km · 约 14 分钟', '距现场 8.9 km · 约 21 分钟', '距现场 4.1 km · 但急诊已满']
|
||||||
|
export const hospitalBed : string[] = ['床位 2', '床位 5', '满员 ✕']
|
||||||
|
export const hospitalAvail : boolean[] = [true, true, false]
|
||||||
|
|
||||||
|
/* ============================ 离线队列 ============================ */
|
||||||
|
export const offlineTasks : HrOfflineTask[] = [
|
||||||
|
{ icon: '⇅', theme: 'amb', title: '检伤建档 · 李华(含 3 个附件)', sub: '15:32 暂存 · 等待上传', status: '待传', statusColor: '#D97706' } as HrOfflineTask,
|
||||||
|
{ icon: '⇅', theme: 'amb', title: '检伤建档 · 赵国强', sub: '15:28 暂存 · 等待上传', status: '待传', statusColor: '#D97706' } as HrOfflineTask,
|
||||||
|
{ icon: '✓', theme: 'grn', title: '处置记录 · 李华止血包扎', sub: '15:02 提交成功', status: '已同步', statusColor: '#059669' } as HrOfflineTask
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 事件 ============================ */
|
||||||
|
export const eventList : HrEventItem[] = [
|
||||||
|
{
|
||||||
|
id: 'e1', name: '京港澳高速 K120 多车追尾', kind: '大型交通事故', place: '京港澳高速 K120', distance: '距您 3.2 km',
|
||||||
|
priority: 0, dotColor: '#DC2626', stage: 'ongoing', urgent: false,
|
||||||
|
members: [
|
||||||
|
{ name: '张', avatar: '张', theme: 'blue' } as HrEventMember,
|
||||||
|
{ name: '李', avatar: '李', theme: 'gn' } as HrEventMember,
|
||||||
|
{ name: '王', avatar: '王', theme: 'ag' } as HrEventMember
|
||||||
|
]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e2', name: '阳光养老院 · 老人跌倒', kind: '一般救援', place: '阳光养老院 3 号楼', distance: '距您 1.1 km',
|
||||||
|
priority: 2, dotColor: '#10B981', stage: 'ongoing', urgent: false,
|
||||||
|
members: [ { name: '陈', avatar: '陈', theme: 'gn' } as HrEventMember ]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e3', name: '城东工业园区 · 化学品泄漏', kind: '危化品事故', place: '城东工业园 B 区', distance: '距您 9.6 km',
|
||||||
|
priority: 0, dotColor: '#DC2626', stage: 'pending', urgent: true, members: [] as HrEventMember[]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e4', name: '城南小学 · 学生集体腹痛', kind: '群体性事件', place: '城南小学食堂', distance: '距您 4.3 km',
|
||||||
|
priority: 1, dotColor: '#F59E0B', stage: 'pending', urgent: true, members: [] as HrEventMember[]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e5', name: '幸福小区 · 老人心梗', kind: '一般救援', place: '幸福小区 6 号楼', distance: '距您 2.4 km',
|
||||||
|
priority: 1, dotColor: '#F59E0B', stage: 'history', urgent: false,
|
||||||
|
members: [ { name: '张', avatar: '张', theme: 'blue' } as HrEventMember ]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e6', name: '滨江路 · 电动车相撞', kind: '交通事故', place: '滨江路与建设街路口', distance: '已完成',
|
||||||
|
priority: 2, dotColor: '#9CA3AF', stage: 'history', urgent: false,
|
||||||
|
members: [ { name: '李', avatar: '李', theme: 'gn' } as HrEventMember ]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e7', name: '第一中学 · 学生运动损伤', kind: '一般救援', place: '第一中学操场', distance: '已完成',
|
||||||
|
priority: 2, dotColor: '#9CA3AF', stage: 'history', urgent: false,
|
||||||
|
members: [ { name: '陈', avatar: '陈', theme: 'gy' } as HrEventMember ]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e8', name: '老城街区 · 煤气泄漏排查', kind: '危化品事故', place: '老城街区 12 巷', distance: '已完成',
|
||||||
|
priority: 1, dotColor: '#9CA3AF', stage: 'history', urgent: false,
|
||||||
|
members: [ { name: '王', avatar: '王', theme: 'ag' } as HrEventMember ]
|
||||||
|
} as HrEventItem,
|
||||||
|
{
|
||||||
|
id: 'e9', name: '火车站 · 旅客突发晕厥', kind: '一般救援', place: '火车站候车大厅', distance: '已完成',
|
||||||
|
priority: 2, dotColor: '#9CA3AF', stage: 'history', urgent: false,
|
||||||
|
members: [ { name: '赵', avatar: '赵', theme: 'blue' } as HrEventMember ]
|
||||||
|
} as HrEventItem
|
||||||
|
]
|
||||||
|
|
||||||
|
export const eventDetail : HrEventDetail = {
|
||||||
|
id: 'e1',
|
||||||
|
name: '京港澳高速 K120 多车追尾',
|
||||||
|
sub: '大型交通事故 · 京港澳高速 K120 · 距您 3.2 km',
|
||||||
|
teamKey: '小组类型',
|
||||||
|
teamValue: '现场搜救组 · 组长 张明远',
|
||||||
|
tags: ['P0 紧急', '已抵达现场'],
|
||||||
|
victimTotal: 8, transferred: 5, pending: 3,
|
||||||
|
redCount: 3, amberCount: 2, greenCount: 1, blackCount: 2,
|
||||||
|
members: [
|
||||||
|
{ name: '张', avatar: '张', theme: 'blue' } as HrEventMember,
|
||||||
|
{ name: '李', avatar: '李', theme: 'gn' } as HrEventMember,
|
||||||
|
{ name: '王', avatar: '王', theme: 'ag' } as HrEventMember,
|
||||||
|
{ name: '陈', avatar: '陈', theme: 'gy' } as HrEventMember,
|
||||||
|
{ name: '赵', avatar: '赵', theme: 'blue' } as HrEventMember
|
||||||
|
],
|
||||||
|
timeline: [
|
||||||
|
{ time: '15:20', title: '已完成首轮检伤', sub: '8 人全部分级 · 红 3 / 黄 2 / 绿 1 / 黑 2', theme: 'normal' } as HrTimelineNode,
|
||||||
|
{ time: '14:50', title: '抵达现场 · 建立临时指挥点', sub: '现场 A 区 · 已设置警戒', theme: 'ok' } as HrTimelineNode,
|
||||||
|
{ time: '14:35', title: '接收派单并出发', sub: '响应耗时 3 分钟', theme: 'normal' } as HrTimelineNode,
|
||||||
|
{ time: '14:32', title: '后台调度派单', sub: 'P0 紧急 · 已推送全屏告警', theme: 'normal' } as HrTimelineNode
|
||||||
|
]
|
||||||
|
} as HrEventDetail
|
||||||
|
|
||||||
|
/* ============================ 扫码作业(批量处置 / 转运) ============================ */
|
||||||
|
export const scanVictims : HrVictim[] = [
|
||||||
|
{ id: 'v1', name: '王建国', gender: '男', age: 52, code: 'TRIAGE-20260922-0001', level: 1, injury: '开放性颅脑损伤', status: '', statusTheme: '', createdAt: '' } as HrVictim,
|
||||||
|
{ id: 'v2', name: '李华', gender: '男', age: 35, code: 'TRIAGE-20260922-0003', level: 2, injury: '头部外伤 / 右前臂骨折', status: '', statusTheme: '', createdAt: '' } as HrVictim,
|
||||||
|
{ id: 'v3', name: '陈小梅', gender: '女', age: 28, code: 'TRIAGE-20260922-0006', level: 3, injury: '右膝软组织挫伤', status: '', statusTheme: '', createdAt: '' } as HrVictim,
|
||||||
|
{ id: 'v4', name: '赵国强', gender: '男', age: 41, code: 'TRIAGE-20260922-0008', level: 2, injury: '胸部挤压伤 / 多发肋骨骨折', status: '', statusTheme: '', createdAt: '' } as HrVictim,
|
||||||
|
{ id: 'v5', name: '吴长胜', gender: '男', age: 61, code: 'TRIAGE-20260922-0012', level: 4, injury: '重度颅脑损伤', status: '', statusTheme: '', createdAt: '' } as HrVictim
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 批量处置 · 处置类型 */
|
||||||
|
export const treatTypes : string[] = ['止血包扎', '固定制动', '静脉通路', '清创消毒', '给氧']
|
||||||
|
|
||||||
|
/* ============================ 仓管 · 扫码 ============================ */
|
||||||
|
export const scanFeedbacks : HrScanFeedback[] = [
|
||||||
|
{ theme: 'ok', icon: '✓', title: '一次性医用口罩 · 入库成功', sub: '规格 50 只/盒 · 现存 320 盒 · 张明远' } as HrScanFeedback,
|
||||||
|
{ theme: 'warn', icon: '!', title: 'N95 防护口罩 · 库存不足', sub: '当前仅 8 盒,低于预警线 20 盒,请及时补货' } as HrScanFeedback,
|
||||||
|
{ theme: 'err', icon: '✕', title: '条码未登记 · 无法识别', sub: '该编码不在物资台账中,请先建档或联系管理员' } as HrScanFeedback
|
||||||
|
]
|
||||||
|
|
||||||
|
export const scanRecords : HrScanRecord[] = [
|
||||||
|
{ name: '一次性医用口罩', time: '15:41', delta: '+20 盒', status: '已入库', theme: 'grn', icon: '✓' } as HrScanRecord,
|
||||||
|
{ name: '医用无菌纱布', time: '15:40', delta: '+12 包', status: '已入库', theme: 'grn', icon: '✓' } as HrScanRecord,
|
||||||
|
{ name: 'N95 防护口罩', time: '15:39', delta: '库存不足未入库', status: '待处理', theme: 'amb', icon: '!' } as HrScanRecord
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 仓管 · 库存 ============================ */
|
||||||
|
export const stockWarn : HrStockItem[] = [
|
||||||
|
{ name: 'N95 防护口罩', spec: '现存 8 盒 · 预警线 20 盒', value: '严重不足 ›', valueTheme: 'red', low: true, icon: '!' } as HrStockItem,
|
||||||
|
{ name: '肾上腺素注射液', spec: '现存 12 支 · 预警线 30 支', value: '严重不足 ›', valueTheme: 'red', low: true, icon: '!' } as HrStockItem,
|
||||||
|
{ name: '一次性输液器', spec: '现存 46 套 · 预警线 50 套', value: '接近预警 ›', valueTheme: 'amb', low: false, icon: '!' } as HrStockItem
|
||||||
|
]
|
||||||
|
|
||||||
|
export const stockAll : HrStockItem[] = [
|
||||||
|
{ name: '一次性医用口罩', spec: '规格 50 只/盒 · 09-20 入库', value: '320', valueTheme: 't1', low: false, icon: '▦' } as HrStockItem,
|
||||||
|
{ name: '医用无菌纱布', spec: '规格 8×10cm · 09-18 入库', value: '186', valueTheme: 't1', low: false, icon: '▦' } as HrStockItem,
|
||||||
|
{ name: '一次性输液器', spec: '规格 1.2m · 09-15 入库', value: '46', valueTheme: 'amb', low: true, icon: '▦' } as HrStockItem,
|
||||||
|
{ name: '肾上腺素注射液', spec: '规格 1mg/1ml · 09-12 入库', value: '12', valueTheme: 'red', low: true, icon: '▦' } as HrStockItem
|
||||||
|
]
|
||||||
|
|
||||||
|
export const goodsDetail : HrGoods = {
|
||||||
|
id: 'g1', name: 'N95 防护口罩', code: 'MZ-0042', spec: '规格 20 只/盒',
|
||||||
|
stock: 8, unit: '盒', warnLine: 20, statusLabel: '库存告急',
|
||||||
|
out30: 72, dailyUse: '日均消耗 2.4 盒', lastIn: '09-12 · 供应商 XX 医疗', lastInQty: '+40 盒',
|
||||||
|
trend: [38, 52, 44, 68, 56, 84, 96]
|
||||||
|
} as HrGoods
|
||||||
|
|
||||||
|
/* ============================ 仓管 · 设备 ============================ */
|
||||||
|
export const deviceList : HrDevice[] = [
|
||||||
|
{ id: 'd1', name: '便携式除颤仪(AED)', code: 'SB-0018', meta: '编码 SB-0018 · 电池 82% · 03-15 维护', status: '可借', theme: 'grn', icon: 'flashlight-line', iconBg: '#EFF6FF', iconColor: '#2563EB', checkList: [] as HrDeviceCheck[], maintain: [] as HrTimelineNode[] } as HrDevice,
|
||||||
|
{ id: 'd2', name: '折叠式担架', code: 'SB-0031', meta: '编码 SB-0031 · 承重 150kg · 06-20 维护', status: '借用中', theme: 'amb', icon: 'file-list-3-line', iconBg: '#FEF3C7', iconColor: '#B45309', checkList: [] as HrDeviceCheck[], maintain: [] as HrTimelineNode[] } as HrDevice,
|
||||||
|
{ id: 'd3', name: '便携式氧气瓶(2L)', code: 'SB-0055', meta: '编码 SB-0055 · 压力正常 · 09-01 检查', status: '可借', theme: 'grn', icon: 'bubble-chart-line', iconBg: '#ECFDF5', iconColor: '#059669', checkList: [] as HrDeviceCheck[], maintain: [] as HrTimelineNode[] } as HrDevice,
|
||||||
|
{ id: 'd4', name: '多参数监护仪', code: 'SB-0063', meta: '编码 SB-0063 · 屏幕故障 · 待维修', status: '维修中', theme: 'red', icon: 'monitor-line', iconBg: '#FEF2F2', iconColor: '#DC2626', checkList: [] as HrDeviceCheck[], maintain: [] as HrTimelineNode[] } as HrDevice
|
||||||
|
]
|
||||||
|
|
||||||
|
export const deviceDetail : HrDevice = {
|
||||||
|
id: 'd1', name: '便携式除颤仪(AED)', code: 'SB-0018', meta: '编码 SB-0018 · 型号 HeartSave PAD',
|
||||||
|
status: '可借用', theme: 'pri', icon: '⚡', iconBg: '#EFF6FF', iconColor: '#2563EB',
|
||||||
|
checkList: [
|
||||||
|
{ title: '电池电量 ≥ 50%', sub: '当前 82% · 满足', checked: true } as HrDeviceCheck,
|
||||||
|
{ title: '电极片在有效期内', sub: '有效期至 2027-03', checked: true } as HrDeviceCheck,
|
||||||
|
{ title: '设备外观无破损', sub: '外壳 / 按键 / 屏幕正常', checked: true } as HrDeviceCheck,
|
||||||
|
{ title: '自检指示正常', sub: '开机绿灯闪烁 · 待确认', checked: false } as HrDeviceCheck,
|
||||||
|
{ title: '附件齐全(剪刀 / 剃刀 / 手套)', sub: '待确认', checked: false } as HrDeviceCheck
|
||||||
|
],
|
||||||
|
maintain: [
|
||||||
|
{ time: '03-15', title: '季度维护完成', sub: '更换电极片 · 电池校准 · 设备科 陈工', theme: 'ok' } as HrTimelineNode,
|
||||||
|
{ time: '2025-12-08', title: '电池更换', sub: '更换原厂电池组', theme: 'normal' } as HrTimelineNode
|
||||||
|
]
|
||||||
|
} as HrDevice
|
||||||
|
|
||||||
|
/* ============================ 消息中心 ============================ */
|
||||||
|
export const keeperMsgGroups : HrMsgGroup[] = [
|
||||||
|
{
|
||||||
|
title: '库存预警', count: 4, danger: true,
|
||||||
|
items: [
|
||||||
|
{ icon: '!', theme: 'red', title: 'N95 防护口罩库存告急', sub: '现存 8 盒,低于预警线 · 10 分钟前', action: '', unread: 1 } as HrMsg,
|
||||||
|
{ icon: '!', theme: 'red', title: '肾上腺素注射液库存告急', sub: '现存 12 支,低于预警线 · 1 小时前', action: '', unread: 1 } as HrMsg
|
||||||
|
]
|
||||||
|
} as HrMsgGroup,
|
||||||
|
{
|
||||||
|
title: '借用审批', count: 3, danger: false,
|
||||||
|
items: [
|
||||||
|
{ icon: '⚑', theme: 'amb', title: '张明远 申请借用「便携式除颤仪」', sub: '用途:院前急救 · 2 小时前', action: '去审批 ›', unread: 0 } as HrMsg,
|
||||||
|
{ icon: '✓', theme: 'grn', title: '李承志 借用「折叠担架」已通过', sub: '预计 09-24 归还 · 昨天', action: '', unread: 0 } as HrMsg
|
||||||
|
]
|
||||||
|
} as HrMsgGroup,
|
||||||
|
{
|
||||||
|
title: '系统通知', count: 0, danger: false,
|
||||||
|
items: [
|
||||||
|
{ icon: '⚙', theme: 'gray', title: '9 月物资盘点任务已下发', sub: '09-30 前完成 · 3 天前', action: '', unread: 0 } as HrMsg
|
||||||
|
]
|
||||||
|
} as HrMsgGroup
|
||||||
|
]
|
||||||
|
|
||||||
|
export const rescuerMsgGroups : HrMsgGroup[] = [
|
||||||
|
{
|
||||||
|
title: '派单通知', count: 2, danger: true,
|
||||||
|
items: [
|
||||||
|
{ icon: '⚠', theme: 'red', title: 'P0 派单 · 城东工业园区化学品泄漏', sub: '15:38 · 距您 9.6 km · 待接收', action: '去接收 ›', unread: 0 } as HrMsg,
|
||||||
|
{ icon: '⚠', theme: 'amb', title: '事件已归档 · 幸福小区老人心梗', sub: '09-21 08:12 · 系统自动归档', action: '', unread: 0 } as HrMsg
|
||||||
|
]
|
||||||
|
} as HrMsgGroup,
|
||||||
|
{
|
||||||
|
title: '系统消息', count: 3, danger: false,
|
||||||
|
items: [
|
||||||
|
{ icon: '▤', theme: 'amb', title: 'BLS 证书即将到期提醒', sub: '2026-10-15 到期 · 请完成复训', action: '', unread: 0 } as HrMsg,
|
||||||
|
{ icon: '▦', theme: 'pri', title: '新课题已上线:溺水者心肺复苏', sub: '昨天 · 急救基础 / 心肺复苏', action: '', unread: 0 } as HrMsg,
|
||||||
|
{ icon: '⚙', theme: 'gray', title: '系统将于本周日凌晨维护', sub: '09-19 · 预计 2 小时', action: '', unread: 0 } as HrMsg
|
||||||
|
]
|
||||||
|
} as HrMsgGroup
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 通讯录 ============================ */
|
||||||
|
export const contactGroups : HrContactGroup[] = [
|
||||||
|
{
|
||||||
|
title: '指挥调度',
|
||||||
|
items: [
|
||||||
|
{ name: '刘建国 · 调度室主任', duty: '', phone: '值班室 8801 · 手机 138****2201', avatar: '刘', theme: 'blue', useAvatar: true, icon: '' } as HrContact,
|
||||||
|
{ name: '陈静 · 现场指挥', duty: '', phone: '值班室 8802 · 手机 139****3387', avatar: '陈', theme: 'blue', useAvatar: true, icon: '' } as HrContact
|
||||||
|
]
|
||||||
|
} as HrContactGroup,
|
||||||
|
{
|
||||||
|
title: '协作医院',
|
||||||
|
items: [
|
||||||
|
{ name: '协和医院 · 急诊创伤中心', duty: '', phone: '急诊台 010-6688 0001', avatar: '', theme: 'pri', useAvatar: false, icon: '▤' } as HrContact,
|
||||||
|
{ name: '市第一人民医院 · 急诊', duty: '', phone: '急诊台 010-6688 0022', avatar: '', theme: 'pri', useAvatar: false, icon: '▤' } as HrContact
|
||||||
|
]
|
||||||
|
} as HrContactGroup,
|
||||||
|
{
|
||||||
|
title: '本队同事',
|
||||||
|
items: [
|
||||||
|
{ name: '李承志 · 主治医师', duty: '', phone: '急诊科 · 工号 1248', avatar: '李', theme: 'gn', useAvatar: true, icon: '' } as HrContact,
|
||||||
|
{ name: '王志远 · 主管护师', duty: '', phone: '急诊科 · 工号 1252', avatar: '王', theme: 'ag', useAvatar: true, icon: '' } as HrContact
|
||||||
|
]
|
||||||
|
} as HrContactGroup
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 设置 ============================ */
|
||||||
|
export const settingSwitches : HrSettingSwitch[] = [
|
||||||
|
{ title: 'P0 派单强提醒', sub: '全屏告警 + 强制震动', on: true } as HrSettingSwitch,
|
||||||
|
{ title: '任务推送', sub: '跟随调度后台的派单通知', on: true } as HrSettingSwitch,
|
||||||
|
{ title: '学习提醒', sub: '课程与考试到期提醒', on: true } as HrSettingSwitch
|
||||||
|
]
|
||||||
|
|
||||||
|
export const offlineSettingRows : HrSettingRow[] = [
|
||||||
|
{ title: '离线队列', sub: '3 条待上传', value: '›' } as HrSettingRow,
|
||||||
|
{ title: '已下载学习资料', sub: '占用 8.6 MB', value: '清理 ›' } as HrSettingRow
|
||||||
|
]
|
||||||
|
|
||||||
|
export const commonSettingRows : HrSettingRow[] = [
|
||||||
|
{ title: '清理缓存', sub: '当前 24.3 MB', value: '清理 ›' } as HrSettingRow,
|
||||||
|
{ title: '关于我们', sub: '版本 v2.0.0', value: '›' } as HrSettingRow,
|
||||||
|
{ title: '隐私政策', sub: '', value: '›' } as HrSettingRow
|
||||||
|
]
|
||||||
|
|
||||||
|
/* ============================ 设计系统 ============================ */
|
||||||
|
export const designSections : HrDesignSection[] = [
|
||||||
|
{
|
||||||
|
title: '主色 / 语义色',
|
||||||
|
swatches: [
|
||||||
|
{ label: '主色', color: '#2563EB', hex: '#2563EB' } as HrSwatch,
|
||||||
|
{ label: '主色深', color: '#1D4ED8', hex: '#1D4ED8' } as HrSwatch,
|
||||||
|
{ label: '浅底', color: '#EFF6FF', hex: '#EFF6FF' } as HrSwatch,
|
||||||
|
{ label: '页面底', color: '#F3F4F6', hex: '#F3F4F6' } as HrSwatch
|
||||||
|
],
|
||||||
|
rows: [] as HrTokenRow[]
|
||||||
|
} as HrDesignSection,
|
||||||
|
{
|
||||||
|
title: '四色腕带分级(颜色 + 文字双重编码)',
|
||||||
|
swatches: [
|
||||||
|
{ label: 'I 级 红', color: '#DC2626', hex: '#DC2626' } as HrSwatch,
|
||||||
|
{ label: 'II 级 黄', color: '#F59E0B', hex: '#F59E0B' } as HrSwatch,
|
||||||
|
{ label: 'III 级 绿', color: '#10B981', hex: '#10B981' } as HrSwatch,
|
||||||
|
{ label: 'IV 级 黑', color: '#1F2937', hex: '#1F2937' } as HrSwatch
|
||||||
|
],
|
||||||
|
rows: [] as HrTokenRow[]
|
||||||
|
} as HrDesignSection,
|
||||||
|
{
|
||||||
|
title: '字号梯度',
|
||||||
|
swatches: [] as HrSwatch[],
|
||||||
|
rows: [
|
||||||
|
{ key: '标题 / 32', value: '救援事件' } as HrTokenRow,
|
||||||
|
{ key: '小标题 / 20', value: '伤员查询' } as HrTokenRow,
|
||||||
|
{ key: '正文 / 14', value: '创伤性失血' } as HrTokenRow,
|
||||||
|
{ key: '辅助 / 12', value: '14:55 建档' } as HrTokenRow,
|
||||||
|
{ key: '角标 / 10', value: 'I 级 · 红' } as HrTokenRow
|
||||||
|
]
|
||||||
|
} as HrDesignSection,
|
||||||
|
{
|
||||||
|
title: '圆角 / 间距 / 触控',
|
||||||
|
swatches: [] as HrSwatch[],
|
||||||
|
rows: [
|
||||||
|
{ key: '圆角', value: '8 / 12 / 16 / 24' } as HrTokenRow,
|
||||||
|
{ key: '间距', value: '8 点网格' } as HrTokenRow,
|
||||||
|
{ key: '主按钮高度', value: '≥ 56pt' } as HrTokenRow,
|
||||||
|
{ key: '其他可点高度', value: '≥ 44pt' } as HrTokenRow,
|
||||||
|
{ key: '正文对比度', value: '≥ 4.5:1(WCAG AA)' } as HrTokenRow
|
||||||
|
]
|
||||||
|
} as HrDesignSection
|
||||||
|
]
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
/**
|
||||||
|
* 医院救援管理平台 · 移动端 数据模型定义
|
||||||
|
* ------------------------------------------------------------
|
||||||
|
* 说明:本文件只放类型定义,不放数据。
|
||||||
|
* 后续对接接口时,仅需保证后端返回结构与此处类型一致(或在此处做一层适配)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 角色:救护人员端 / 仓管人员端 */
|
||||||
|
export type HrRole = 'rescuer' | 'keeper'
|
||||||
|
|
||||||
|
/** 伤情腕带分级:1 危重(红) / 2 重伤(黄) / 3 轻伤(绿) / 4 死亡(黑) */
|
||||||
|
export type HrLevel = 1 | 2 | 3 | 4
|
||||||
|
|
||||||
|
/** 优先级 */
|
||||||
|
export type HrPriority = 0 | 1 | 2
|
||||||
|
|
||||||
|
/* ============================ 用户 ============================ */
|
||||||
|
export type HrUser = {
|
||||||
|
/** 姓名 */
|
||||||
|
name : string
|
||||||
|
/** 头像文字 */
|
||||||
|
avatar : string
|
||||||
|
/** 职称 */
|
||||||
|
title : string
|
||||||
|
/** 科室 */
|
||||||
|
dept : string
|
||||||
|
/** 工号(角色由后台按工号识别) */
|
||||||
|
jobNo : string
|
||||||
|
/** 角色 */
|
||||||
|
role : HrRole
|
||||||
|
/** 角色中文名 */
|
||||||
|
roleLabel : string
|
||||||
|
/** 能力标签 */
|
||||||
|
tags : string[]
|
||||||
|
/** 累计学时 */
|
||||||
|
hours : number
|
||||||
|
/** 完成课程数 */
|
||||||
|
courseCount : number
|
||||||
|
/** 有效证书数 */
|
||||||
|
certCount : number
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 培训学习 ============================ */
|
||||||
|
export type HrTodoTask = {
|
||||||
|
icon : string
|
||||||
|
iconTheme : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
action : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrActivity = {
|
||||||
|
icon : string
|
||||||
|
iconTheme : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrToolBox = {
|
||||||
|
icon : string
|
||||||
|
name : string
|
||||||
|
iconBg : string
|
||||||
|
iconColor : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 课程库一级分类 */
|
||||||
|
export type HrCatalog = {
|
||||||
|
id : string
|
||||||
|
name : string
|
||||||
|
count : number
|
||||||
|
icon : string
|
||||||
|
iconBg : string
|
||||||
|
iconColor : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 课程库二级分组(仅作分组标题,不可点) */
|
||||||
|
export type HrGroup = {
|
||||||
|
name : string
|
||||||
|
count : number
|
||||||
|
items : HrSubject[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 课程库三级课题(唯一可点) */
|
||||||
|
export type HrSubject = {
|
||||||
|
no : string
|
||||||
|
name : string
|
||||||
|
meta : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 学习资料(视频 / 各类文档共用) */
|
||||||
|
export type HrMaterial = {
|
||||||
|
id : string
|
||||||
|
/** video | doc */
|
||||||
|
kind : string
|
||||||
|
/** PDF / DOC / PPT / XLS / 视频 */
|
||||||
|
badge : string
|
||||||
|
badgeColor : string
|
||||||
|
icon : string
|
||||||
|
title : string
|
||||||
|
meta : string
|
||||||
|
/** 资料简介 */
|
||||||
|
summary : string
|
||||||
|
/** 关联课题 */
|
||||||
|
from : string
|
||||||
|
/** 学习/观看进度 0~100 */
|
||||||
|
progress : number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 考试记录 */
|
||||||
|
export type HrExamRecord = {
|
||||||
|
name : string
|
||||||
|
date : string
|
||||||
|
score : number
|
||||||
|
passed : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 证书 */
|
||||||
|
export type HrCert = {
|
||||||
|
name : string
|
||||||
|
gotAt : string
|
||||||
|
expireAt : string
|
||||||
|
status : string
|
||||||
|
statusTheme : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 待考安排 */
|
||||||
|
export type HrExamPlan = {
|
||||||
|
name : string
|
||||||
|
priority : HrPriority
|
||||||
|
countdown : string
|
||||||
|
time : string
|
||||||
|
place : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 顺序练习信息 */
|
||||||
|
export type HrPracticeMeta = {
|
||||||
|
subject : string
|
||||||
|
total : number
|
||||||
|
minutes : number
|
||||||
|
goal : string
|
||||||
|
lastTime : string
|
||||||
|
lastDone : number
|
||||||
|
lastRate : number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 题目选项 */
|
||||||
|
export type HrOption = {
|
||||||
|
key : string
|
||||||
|
text : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 题目 */
|
||||||
|
export type HrQuestion = {
|
||||||
|
id : string
|
||||||
|
stem : string
|
||||||
|
options : HrOption[]
|
||||||
|
/** 正确选项下标 */
|
||||||
|
answer : number
|
||||||
|
analysis : string
|
||||||
|
/** 所属课题 */
|
||||||
|
from : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 检伤 / 伤员 ============================ */
|
||||||
|
export type HrInjuryMark = {
|
||||||
|
no : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
/** 标记点色:red / amb */
|
||||||
|
theme : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrVital = {
|
||||||
|
/** 数值,如 37.8 / 95/60 */
|
||||||
|
value : string
|
||||||
|
/** 指标名 + 单位,如 T ℃ */
|
||||||
|
unit : string
|
||||||
|
/** 是否异常(异常高亮为红) */
|
||||||
|
abnormal : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrTimelineNode = {
|
||||||
|
time : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
/** normal | ok | warn */
|
||||||
|
theme : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrVictim = {
|
||||||
|
id : string
|
||||||
|
name : string
|
||||||
|
gender : string
|
||||||
|
age : number
|
||||||
|
code : string
|
||||||
|
level : HrLevel
|
||||||
|
/** 伤情摘要 */
|
||||||
|
injury : string
|
||||||
|
/** 转运/处置状态文案 */
|
||||||
|
status : string
|
||||||
|
statusTheme : string
|
||||||
|
createdAt : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 应急事件 ============================ */
|
||||||
|
export type HrEventMember = {
|
||||||
|
name : string
|
||||||
|
avatar : string
|
||||||
|
theme : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrEventItem = {
|
||||||
|
id : string
|
||||||
|
name : string
|
||||||
|
kind : string
|
||||||
|
place : string
|
||||||
|
distance : string
|
||||||
|
priority : HrPriority
|
||||||
|
dotColor : string
|
||||||
|
/**进行中 / 待接收 / 历史 */
|
||||||
|
stage : string
|
||||||
|
urgent : boolean
|
||||||
|
members : HrEventMember[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrEventDetail = {
|
||||||
|
id : string
|
||||||
|
name : string
|
||||||
|
sub : string
|
||||||
|
teamKey : string
|
||||||
|
teamValue : string
|
||||||
|
tags : string[]
|
||||||
|
victimTotal : number
|
||||||
|
transferred : number
|
||||||
|
pending : number
|
||||||
|
redCount : number
|
||||||
|
amberCount : number
|
||||||
|
greenCount : number
|
||||||
|
blackCount : number
|
||||||
|
members : HrEventMember[]
|
||||||
|
timeline : HrTimelineNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 仓管 ============================ */
|
||||||
|
export type HrScanFeedback = {
|
||||||
|
/** ok | warn | err */
|
||||||
|
theme : string
|
||||||
|
icon : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrScanRecord = {
|
||||||
|
name : string
|
||||||
|
time : string
|
||||||
|
delta : string
|
||||||
|
status : string
|
||||||
|
theme : string
|
||||||
|
icon : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrStockItem = {
|
||||||
|
name : string
|
||||||
|
spec : string
|
||||||
|
value : string
|
||||||
|
valueTheme : string
|
||||||
|
low : boolean
|
||||||
|
icon : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrGoods = {
|
||||||
|
id : string
|
||||||
|
name : string
|
||||||
|
code : string
|
||||||
|
spec : string
|
||||||
|
stock : number
|
||||||
|
unit : string
|
||||||
|
warnLine : number
|
||||||
|
statusLabel : string
|
||||||
|
out30 : number
|
||||||
|
dailyUse : string
|
||||||
|
lastIn : string
|
||||||
|
lastInQty : string
|
||||||
|
/** 趋势柱高百分比 */
|
||||||
|
trend : number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrDevice = {
|
||||||
|
id : string
|
||||||
|
name : string
|
||||||
|
code : string
|
||||||
|
meta : string
|
||||||
|
status : string
|
||||||
|
theme : string
|
||||||
|
icon : string
|
||||||
|
iconBg : string
|
||||||
|
iconColor : string
|
||||||
|
checkList : HrDeviceCheck[]
|
||||||
|
maintain : HrTimelineNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrDeviceCheck = {
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
checked : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 消息 / 通讯录 ============================ */
|
||||||
|
export type HrMsg = {
|
||||||
|
icon : string
|
||||||
|
theme : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
action : string
|
||||||
|
unread : number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrMsgGroup = {
|
||||||
|
title : string
|
||||||
|
count : number
|
||||||
|
danger : boolean
|
||||||
|
items : HrMsg[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrContact = {
|
||||||
|
name : string
|
||||||
|
duty : string
|
||||||
|
phone : string
|
||||||
|
avatar : string
|
||||||
|
theme : string
|
||||||
|
/** true 用图标,false 用文字头像 */
|
||||||
|
useAvatar : boolean
|
||||||
|
icon : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrContactGroup = {
|
||||||
|
title : string
|
||||||
|
items : HrContact[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 离线队列 ============================ */
|
||||||
|
export type HrOfflineTask = {
|
||||||
|
icon : string
|
||||||
|
theme : string
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
status : string
|
||||||
|
statusColor : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 设置 ============================ */
|
||||||
|
export type HrSettingSwitch = {
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
on : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrSettingRow = {
|
||||||
|
title : string
|
||||||
|
sub : string
|
||||||
|
value : string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================ 设计系统 ============================ */
|
||||||
|
export type HrSwatch = {
|
||||||
|
label : string
|
||||||
|
color : string
|
||||||
|
hex : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrTokenRow = {
|
||||||
|
key : string
|
||||||
|
value : string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HrDesignSection = {
|
||||||
|
title : string
|
||||||
|
swatches : HrSwatch[]
|
||||||
|
rows : HrTokenRow[]
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
{
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "登录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/home/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "首页"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/event/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "事件"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/event/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "事件详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "我的"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/mine/settings",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "设置"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course/library",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "课程库"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course/subject",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "课题详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course/media",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "资料详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course/practice",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "顺序练习"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course/quiz",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "答题"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/course/answer-card",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "答题卡"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/study/exams",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "我的考试"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/study/certificates",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "我的证书"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/study/wrong-book",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "错题本"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/study/center",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "学习中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/victims",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "伤员查询"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/create",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "检伤建档"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "伤员详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/batch-treat",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "批量处置"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/batch-transfer",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "批量转运"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/transfer",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "转运登记"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/treat-records",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "处置记录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/triage/offline-sync",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "离线同步"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/scan",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "扫码出入库"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/stock",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "库存查询"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/goods-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "物资详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/devices",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "设备借用"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/warehouse/device-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "设备详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/index",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "消息"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/list",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "消息中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/contacts",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true,
|
||||||
|
"navigationBarTitleText": "通讯录"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "@navTxtStyle",
|
||||||
|
"navigationBarBackgroundColor": "@navBgColor",
|
||||||
|
"backgroundColorContent": "@backgroundColor",
|
||||||
|
"backgroundColor": "@backgroundColor",
|
||||||
|
"navigationBarTitleText": "医院救援管理平台",
|
||||||
|
"pageOrientation": "portrait"
|
||||||
|
},
|
||||||
|
"uniIdRouter": {},
|
||||||
|
"condition": {
|
||||||
|
"current": 0,
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "登录",
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"query": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="答题卡" @right-click="submit">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="ac-nav-submit">交卷</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 图例 ============ -->
|
||||||
|
<hr-card>
|
||||||
|
<view class="hr-row hr-wrap">
|
||||||
|
<view class="ac-lg">
|
||||||
|
<view class="ac-lg-s" style="background-color:#EFF6FF;border:1px solid #BFDBFE"></view>
|
||||||
|
<text class="ac-lg-t">已答 {{ doneCount }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="ac-lg">
|
||||||
|
<view class="ac-lg-s" style="background-color:#FFFFFF;border:1px solid #E5E7EB"></view>
|
||||||
|
<text class="ac-lg-t">未答 {{ total - doneCount }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="ac-lg">
|
||||||
|
<view class="ac-lg-s" style="background-color:#FEF3C7;border:1px solid #FDE68A"></view>
|
||||||
|
<text class="ac-lg-t">标记 {{ markCount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 题号九宫格 ============ -->
|
||||||
|
<hr-card style="margin-top:10px">
|
||||||
|
<view class="ac-grid">
|
||||||
|
<view v-for="(q, i) in questions" :key="i" :class="cellClass(i)" :style="cellStyle" @click="jump(i)">
|
||||||
|
<text :class="cellTextClass(i)">{{ i + 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:10px" :theme="answeredAll ? 'ok' : 'warn'"
|
||||||
|
:icon="answeredAll ? '✓' : '⚑'"
|
||||||
|
:text="answeredAll ? '全部题目已作答,可以交卷了。' : ('仍有 ' + (total - doneCount) + ' 题未作答,交卷后将无法返回修改。')"></hr-tip>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-ghost ac-foot-prev" @click="resume">
|
||||||
|
<text class="ac-foot-t">继续答题</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-btn hr-btn-primary" style="flex:1" @click="submit">
|
||||||
|
<text class="ac-foot-t2">交卷并查看成绩</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { quizQuestions } from '@/mock/index.uts'
|
||||||
|
import { quizState, answeredCount, markedCount } from '@/utils/quiz.uts'
|
||||||
|
import { navBack, relaunchTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/** s18 答题卡 */
|
||||||
|
const questions = quizQuestions
|
||||||
|
const total = questions.length
|
||||||
|
const refreshKey = ref(0)
|
||||||
|
const cellSize = ref(92)
|
||||||
|
|
||||||
|
const doneCount = computed(() : number => {
|
||||||
|
const k = refreshKey.value
|
||||||
|
return answeredCount()
|
||||||
|
})
|
||||||
|
|
||||||
|
const markCount = computed(() : number => {
|
||||||
|
const k = refreshKey.value
|
||||||
|
return markedCount()
|
||||||
|
})
|
||||||
|
|
||||||
|
const answeredAll = computed(() : boolean => {
|
||||||
|
return doneCount.value == total
|
||||||
|
})
|
||||||
|
|
||||||
|
const cellStyle = computed(() : string => {
|
||||||
|
return `width:${cellSize.value}rpx;height:${cellSize.value}rpx;`
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
// 6 列均分:内容宽 750rpx - 页面左右 64 - 卡片左右 52 - 5 个 16rpx 间隙 80
|
||||||
|
cellSize.value = Math.floor((750 - 64 - 52 - 80) / 6)
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageShow(() => {
|
||||||
|
refreshKey.value = refreshKey.value + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
function cellClass(i : number) : string {
|
||||||
|
const k = refreshKey.value
|
||||||
|
if (quizState.current == i) { return 'ac-cell ac-cell-cur' }
|
||||||
|
if (quizState.marked[i]) { return 'ac-cell ac-cell-mark' }
|
||||||
|
if (quizState.answers[i] >= 0) { return 'ac-cell ac-cell-done' }
|
||||||
|
return 'ac-cell'
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellTextClass(i : number) : string {
|
||||||
|
const k = refreshKey.value
|
||||||
|
if (quizState.current == i) { return 'ac-cell-t ac-cell-t-cur' }
|
||||||
|
return 'ac-cell-t'
|
||||||
|
}
|
||||||
|
|
||||||
|
function jump(i : number) : void {
|
||||||
|
quizState.current = i
|
||||||
|
navBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resume() : void {
|
||||||
|
navBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '交卷确认',
|
||||||
|
content: `已作答 ${doneCount.value} / ${total} 题。交卷后无法修改。`,
|
||||||
|
cancelText: '继续答题',
|
||||||
|
confirmText: '交卷',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
quizState.submitted = true
|
||||||
|
uni.showToast({ title: '已交卷 · 得分 92', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
relaunchTo('/pages/study/exams')
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ac-nav-submit {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-lg {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 28rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-lg-s {
|
||||||
|
width: 24rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-lg-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-cell {
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-cell-done {
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
border: 2rpx solid #BFDBFE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-cell-mark {
|
||||||
|
background-color: #FEF3C7;
|
||||||
|
border: 2rpx solid #FDE68A;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-cell-cur {
|
||||||
|
background-color: #2563EB;
|
||||||
|
border: 2rpx solid #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-cell-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-cell-t-cur {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-foot-prev {
|
||||||
|
flex: 0 0 220rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-foot-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ac-foot-t2 {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#FFFFFF" status-bg="#FFFFFF" nav-title="课程库" :no-scroll="true" @right-click="goCert">
|
||||||
|
|
||||||
|
<!-- ============ 搜索行(xSearch) ============ -->
|
||||||
|
<view class="cl-search">
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<x-search v-model="keyword" placeholder="搜索课题,如 成人 CPR / AED" round="20rpx" height="72rpx"
|
||||||
|
input-bg-color="#F3F4F6" icon-color="#9CA3AF" font-color="#1F2937"
|
||||||
|
:show-cancel="false"></x-search>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 一级侧栏 + 三级内容 ============ -->
|
||||||
|
<view class="cl-split">
|
||||||
|
|
||||||
|
<scroll-view class="cl-side" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<view v-for="(c, i) in catalogs" :key="c.id"
|
||||||
|
:class="activeCatalog == i ? 'cl-side-item cl-side-item-on' : 'cl-side-item'" @click="onCatalog(i)">
|
||||||
|
<view class="cl-side-ic" :style="{ backgroundColor: c.iconBg }">
|
||||||
|
<text class="cl-side-ic-t" :style="{ color: c.iconColor }">{{ c.icon }}</text>
|
||||||
|
</view>
|
||||||
|
<text :class="activeCatalog == i ? 'cl-side-b cl-side-b-on' : 'cl-side-b'">{{ c.name }}</text>
|
||||||
|
<text class="cl-side-s">{{ c.count }} 课题</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<scroll-view class="cl-main" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<!-- 搜索过滤结果 -->
|
||||||
|
<view v-if="keyword != ''">
|
||||||
|
<view v-for="(r, i) in searchResult" :key="i" class="cl-l3" @click="openSubject(r)">
|
||||||
|
<view class="cl-l3-no">
|
||||||
|
<text class="cl-l3-no-t">{{ r.no }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="cl-l3-b">{{ r.name }}</text>
|
||||||
|
<text class="cl-l3-s">{{ r.meta }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="cl-l3-go">›</text>
|
||||||
|
</view>
|
||||||
|
<hr-empty v-if="searchResult.length == 0" icon="⌕" title="没有匹配的课题"
|
||||||
|
sub="换个关键词试试,例如「CPR」「AED」「气道」"></hr-empty>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 二级组标题 + 三级条目 -->
|
||||||
|
<view v-else-if="activeCatalog == 0">
|
||||||
|
<view v-for="(g, gi) in groups" :key="gi">
|
||||||
|
<view class="cl-group">
|
||||||
|
<view class="cl-group-bar"></view>
|
||||||
|
<text class="cl-group-b">{{ g.name }}</text>
|
||||||
|
<view class="cl-group-cnt">
|
||||||
|
<text class="cl-group-cnt-t">{{ g.count }} 课题</text>
|
||||||
|
</view>
|
||||||
|
<view class="cl-group-line"></view>
|
||||||
|
</view>
|
||||||
|
<view v-for="(it, ii) in g.items" :key="ii" class="cl-l3" @click="openSubject(it)">
|
||||||
|
<view class="cl-l3-no">
|
||||||
|
<text class="cl-l3-no-t">{{ it.no }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="cl-l3-b">{{ it.name }}</text>
|
||||||
|
<text class="cl-l3-s">{{ it.meta }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="cl-l3-go">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="margin:6px 13px 14px 13px">
|
||||||
|
<hr-tip theme="info" icon="ℹ"
|
||||||
|
:text="'共 ' + catalogs.length + ' 个一级分类 · 49 个课题。其余分类的课题内容正在建设,敬请期待。'"></hr-tip>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-empty v-else icon="▦" :title="catalogName + ' · 内容建设中'" sub="该分类的课题与题库正在录入,可先学习「急救基础」"></hr-empty>
|
||||||
|
|
||||||
|
<view style="height:24px"></view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { catalogList, courseGroups } from '@/mock/index.uts'
|
||||||
|
import { HrCatalog, HrGroup, HrSubject } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s10 课程库(三级导航)
|
||||||
|
* 一级侧栏分类 + 二级组标题(不可点,仅作分组)+ 三级课题条目(唯一可点)。
|
||||||
|
*/
|
||||||
|
const catalogs = ref<HrCatalog[]>(catalogList)
|
||||||
|
const groups = ref<HrGroup[]>(courseGroups)
|
||||||
|
const activeCatalog = ref(0)
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
const catalogName = computed(() : string => {
|
||||||
|
return catalogs.value[activeCatalog.value].name
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 全量课题(用于搜索) */
|
||||||
|
const allSubjects = computed(() : HrSubject[] => {
|
||||||
|
const list : HrSubject[] = []
|
||||||
|
for (let gi = 0; gi < groups.value.length; gi++) {
|
||||||
|
const g = groups.value[gi]
|
||||||
|
for (let ii = 0; ii < g.items.length; ii++) {
|
||||||
|
list.push(g.items[ii])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
const searchResult = computed(() : HrSubject[] => {
|
||||||
|
if (keyword.value == '') { return [] as HrSubject[] }
|
||||||
|
const kw = keyword.value.toLowerCase()
|
||||||
|
const list : HrSubject[] = []
|
||||||
|
for (let i = 0; i < allSubjects.value.length; i++) {
|
||||||
|
const s = allSubjects.value[i]
|
||||||
|
if (s.name.toLowerCase().indexOf(kw) > -1) { list.push(s) }
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
function onCatalog(i : number) : void {
|
||||||
|
activeCatalog.value = i
|
||||||
|
keyword.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSubject(s : HrSubject) : void {
|
||||||
|
navTo('/pages/course/subject?name=' + s.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function goCert() : void {
|
||||||
|
navTo('/pages/study/certificates')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScanSearch() : void {
|
||||||
|
uni.showToast({ title: '扫码搜索课题', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cl-search {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-bottom: 2rpx solid #F3F4F6;
|
||||||
|
padding: 16rpx 28rpx 16rpx 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-search-scan {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-split {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 左侧一级分类 ---------- */
|
||||||
|
.cl-side {
|
||||||
|
width: 184rpx;
|
||||||
|
background-color: #FAFBFC;
|
||||||
|
border-right: 2rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-item {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 22rpx 12rpx 22rpx 12rpx;
|
||||||
|
border-left: 6rpx solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-item-on {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-left: 6rpx solid #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-ic {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-ic-t {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-b {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-b-on {
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-side-s {
|
||||||
|
font-size: 20.2rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 右侧内容 ---------- */
|
||||||
|
.cl-main {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-hint {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 26rpx 8rpx 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-hint-i {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-hint-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding: 26rpx 26rpx 16rpx 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-group-bar {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
background-color: #2563EB;
|
||||||
|
margin-right: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-group-b {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-group-cnt {
|
||||||
|
margin-left: 14rpx;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 2rpx 12rpx 2rpx 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-group-cnt-t {
|
||||||
|
font-size: 21.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-group-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 2rpx;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
margin-left: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-l3 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0rpx 26rpx 16rpx 26rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #F3F4F6;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
padding: 22rpx 24rpx 22rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-l3-no {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-l3-no-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-l3-b {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-l3-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cl-l3-go {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,490 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" :nav-title="material.title" @right-click="onStar">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon :name="starred ? 'star-fill' : 'star-line'" :color="starred ? '#F59E0B' : '#6B7280'"
|
||||||
|
font-size="19"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body" style="padding-bottom:22px">
|
||||||
|
|
||||||
|
<!-- ============ 视频资料 ============ -->
|
||||||
|
<view v-if="isVideo">
|
||||||
|
<view class="vd" :style="{ height: videoH + 'rpx' }">
|
||||||
|
<!-- 播放/暂停 -->
|
||||||
|
<view class="vd-play" @click="togglePlay">
|
||||||
|
<x-icon :name="playing ? 'pause-fill' : 'play-fill'" :color="playing ? '#FFFFFF' : '#1D4ED8'"
|
||||||
|
font-size="20"></x-icon>
|
||||||
|
</view>
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<view class="vd-bar">
|
||||||
|
<view class="vd-track">
|
||||||
|
<view class="vd-track-i" :style="{ width: played + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
<view class="vd-row">
|
||||||
|
<text class="vd-time">{{ curTime }} / {{ totalTime }}</text>
|
||||||
|
<view class="vd-acts">
|
||||||
|
<view @click="togglePlay">
|
||||||
|
<x-icon :name="playing ? 'pause-line' : 'play-line'" color="#FFFFFF"
|
||||||
|
font-size="16"></x-icon>
|
||||||
|
</view>
|
||||||
|
<view style="margin-left:14px" @click="onFullscreen">
|
||||||
|
<x-icon name="fullscreen-line" color="#FFFFFF" font-size="16"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-card style="margin-top:10px">
|
||||||
|
<view class="hr-row-top">
|
||||||
|
<text class="md-title">{{ material.title }}</text>
|
||||||
|
<view class="md-badge" :style="{ backgroundColor: material.badgeColor }">
|
||||||
|
<text class="md-badge-t">{{ material.badge }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="md-meta">{{ material.meta }}</text>
|
||||||
|
<text class="md-from">来自 {{ material.from }}</text>
|
||||||
|
<view class="hr-row" style="margin-top:12px">
|
||||||
|
<view style="flex:1">
|
||||||
|
<hr-progress :percent="played"></hr-progress>
|
||||||
|
</view>
|
||||||
|
<text class="md-seen">已看 {{ played }}%</text>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 文档资料 ============ -->
|
||||||
|
<view v-else>
|
||||||
|
<view class="dv">
|
||||||
|
<view class="dv-sheet">
|
||||||
|
<view class="dv-t"></view>
|
||||||
|
<view class="dv-l dv-l-s"></view>
|
||||||
|
<view class="dv-l"></view>
|
||||||
|
<view class="dv-l dv-l-m"></view>
|
||||||
|
<view class="dv-img"></view>
|
||||||
|
<view class="dv-l dv-l-s"></view>
|
||||||
|
<view class="dv-l dv-l-m"></view>
|
||||||
|
<text class="dv-pg">{{ page }} / 12</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="dv-pager">
|
||||||
|
<view class="dv-pager-b" @click="prevPage">
|
||||||
|
<x-icon name="arrow-left-s-line" color="#6B7280" font-size="16"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="dv-pager-t">第 {{ page }} 页 / 共 12 页</text>
|
||||||
|
<view class="dv-pager-b" @click="nextPage">
|
||||||
|
<x-icon name="arrow-right-s-line" color="#6B7280" font-size="16"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-card style="margin-top:10px">
|
||||||
|
<view class="hr-row-top">
|
||||||
|
<text class="md-title">{{ material.title }}</text>
|
||||||
|
<view class="md-badge" :style="{ backgroundColor: material.badgeColor }">
|
||||||
|
<text class="md-badge-t">{{ material.badge }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="md-meta">{{ material.meta }}</text>
|
||||||
|
<text class="md-from">来自 {{ material.from }}</text>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 资料简介 ============ -->
|
||||||
|
<hr-sec title="资料简介" :first="false"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<text class="md-desc">{{ material.summary }}</text>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 学习笔记(仅视频) ============ -->
|
||||||
|
<view v-if="isVideo">
|
||||||
|
<hr-sec title="学习笔记" more="编辑 ›" @more-click="onEditNote"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<text class="md-desc">{{ note }}</text>
|
||||||
|
<text class="md-note-time">最后编辑:昨天 21:32</text>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 离线下载 ============ -->
|
||||||
|
<hr-sec title="离线下载" :more="downloaded ? '⇅ 管理 ›' : '⇅ 下载 ›'" @more-click="onDownload"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow :icon="downloaded ? '✓' : '⇅'" :theme="downloaded ? 'grn' : 'pri'"
|
||||||
|
:title="downloaded ? '已下载到本机' : '尚未下载'" :sub="downloaded ? '2.4 MB · 无网络时仍可查阅' : '点击下载后可离线查阅'"
|
||||||
|
:value="downloaded ? '已存' : '下载'" :value-theme="downloaded ? 'grn' : 'pri'" :value-bold="true"
|
||||||
|
:is-last="true" @click="onDownload"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<view style="height:8px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-ghost md-fav" @click="onStar">
|
||||||
|
<x-icon :name="starred ? 'star-fill' : 'star-line'" :color="starred ? '#F59E0B' : '#1F2937'"
|
||||||
|
font-size="17"></x-icon>
|
||||||
|
<text class="md-fav-t">{{ starred ? '已收藏' : '收藏' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-btn hr-btn-primary md-main" @click="onMainAction">
|
||||||
|
<text class="md-main-t">{{ isVideo ? (downloaded ? '已下载离线观看' : '下载离线观看') : '在线查看' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { subjectMaterials } from '@/mock/index.uts'
|
||||||
|
import { HrMaterial } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s02 / s41 资料详情(视频与文档共用同一页)
|
||||||
|
* 标题即资料名称;不再有章节目录与章节学习。
|
||||||
|
*/
|
||||||
|
const material = ref<HrMaterial>(subjectMaterials[1])
|
||||||
|
const starred = ref(false)
|
||||||
|
const downloaded = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const playing = ref(false)
|
||||||
|
const played = ref(62)
|
||||||
|
const videoH = ref(386)
|
||||||
|
const totalSeconds = 504
|
||||||
|
let timer : number = 0
|
||||||
|
|
||||||
|
const note = '按压深度 5~6 cm,频率 100~120 次/分;每次按压后必须让胸廓完全回弹,尽量减少中断(<10 秒)……'
|
||||||
|
|
||||||
|
// ⚠ UTS 不提升函数声明(App 端编译报 UTSC error18: 找不到名称),
|
||||||
|
// 被调用的函数一律写在调用点之前
|
||||||
|
function formatTime(sec : number) : string {
|
||||||
|
const m = Math.floor(sec / 60)
|
||||||
|
const s = sec % 60
|
||||||
|
const ss = s < 10 ? '0' + s.toString() : s.toString()
|
||||||
|
return `${m}:${ss}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() : void {
|
||||||
|
if (timer != 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVideo = computed(() : boolean => {
|
||||||
|
return material.value.kind == 'video'
|
||||||
|
})
|
||||||
|
|
||||||
|
const curTime = computed(() : string => {
|
||||||
|
const sec = Math.floor(totalSeconds * played.value / 100)
|
||||||
|
return formatTime(sec)
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalTime = computed(() : string => {
|
||||||
|
return formatTime(totalSeconds)
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((opt) => {
|
||||||
|
const raw = opt['id']
|
||||||
|
if (raw != null) {
|
||||||
|
const id = raw as string
|
||||||
|
for (let i = 0; i < subjectMaterials.length; i++) {
|
||||||
|
if (subjectMaterials[i].id == id) {
|
||||||
|
material.value = subjectMaterials[i]
|
||||||
|
played.value = subjectMaterials[i].progress > 0 ? subjectMaterials[i].progress : 0
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 视频区保持 16:9:内容宽 = 750rpx - 页面左右各 32rpx
|
||||||
|
videoH.value = Math.round((750 - 64) * 9 / 16)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
stopTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
function togglePlay() : void {
|
||||||
|
playing.value = !playing.value
|
||||||
|
if (playing.value) {
|
||||||
|
stopTimer()
|
||||||
|
timer = setInterval(() => {
|
||||||
|
if (played.value >= 100) {
|
||||||
|
played.value = 100
|
||||||
|
playing.value = false
|
||||||
|
stopTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
played.value = played.value + 1
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevPage() : void {
|
||||||
|
if (page.value <= 1) {
|
||||||
|
uni.showToast({ title: '已是第一页', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.value = page.value - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextPage() : void {
|
||||||
|
if (page.value >= 12) {
|
||||||
|
uni.showToast({ title: '已是最后一页', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page.value = page.value + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStar() : void {
|
||||||
|
starred.value = !starred.value
|
||||||
|
uni.showToast({ title: starred.value ? '已加入收藏' : '已取消收藏', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDownload() : void {
|
||||||
|
if (downloaded.value) {
|
||||||
|
uni.showToast({ title: '已下载到本机', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '下载中' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
downloaded.value = true
|
||||||
|
uni.showToast({ title: '下载完成 · 可离线查阅', icon: 'success' })
|
||||||
|
}, 900)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMainAction() : void {
|
||||||
|
if (isVideo.value) {
|
||||||
|
onDownload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '已用系统阅读器打开', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditNote() : void {
|
||||||
|
uni.showToast({ title: '编辑学习笔记', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFullscreen() : void {
|
||||||
|
uni.showToast({ title: '已进入全屏播放', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ---------- 视频 ---------- */
|
||||||
|
.vd {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #111827;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-play {
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
border-radius: 52rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.92);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0rpx;
|
||||||
|
right: 0rpx;
|
||||||
|
bottom: 0rpx;
|
||||||
|
padding: 0rpx 24rpx 20rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-track {
|
||||||
|
height: 6rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-track-i {
|
||||||
|
height: 6rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-time {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vd-acts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 文档预览 ---------- */
|
||||||
|
.dv {
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 24rpx 24rpx 24rpx 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-sheet {
|
||||||
|
width: 80%;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 30rpx 26rpx 32rpx 26rpx;
|
||||||
|
box-shadow: 0 6rpx 24rpx rgba(15, 23, 42, 0.16);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-t {
|
||||||
|
height: 18rpx;
|
||||||
|
width: 58%;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: #CBD5E1;
|
||||||
|
margin-bottom: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-l {
|
||||||
|
height: 12rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-l-s {
|
||||||
|
width: 88%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-l-m {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-img {
|
||||||
|
height: 100rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background-color: #F1F5F9;
|
||||||
|
margin: 6rpx 0rpx 20rpx 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-pg {
|
||||||
|
font-size: 20.2rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-pager {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-pager-b {
|
||||||
|
width: 48rpx;
|
||||||
|
height: 48rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-pager-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 详情 ---------- */
|
||||||
|
.md-title {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-badge {
|
||||||
|
border-radius: 14rpx;
|
||||||
|
padding: 6rpx 18rpx 6rpx 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-badge-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-meta {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-from {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-seen {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-desc {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-note-time {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-fav {
|
||||||
|
flex: 0 0 220rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-fav-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-main-t {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="顺序练习" @right-click="onMore">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ hero ============ -->
|
||||||
|
<view class="prac-hero">
|
||||||
|
<view class="prac-c1"></view>
|
||||||
|
<view class="prac-ic">
|
||||||
|
<x-icon name="play-fill" color="#FFFFFF" font-size="27"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="prac-t">{{ meta.subject }}</text>
|
||||||
|
<text class="prac-s">顺序练习 · 按章节顺序出题</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 练习信息 ============ -->
|
||||||
|
<hr-card variant="list" style="margin-top:10px">
|
||||||
|
<hr-lrow icon="▦" theme="pri" title="题目数量" sub="覆盖本课题全部章节">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="prac-val">{{ meta.total }} 题</text>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
<hr-lrow icon="◔" theme="amb" title="预计用时" sub="按每题 36 秒估算">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="prac-val">约 {{ meta.minutes }} 分钟</text>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
<hr-lrow icon="✓" theme="grn" title="目标正确率" sub="达标后计入课题完成度" :is-last="true">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="prac-val prac-val-grn">{{ meta.goal }}</text>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:10px" theme="warn" icon="⚑"
|
||||||
|
:text="'顺序练习不限制次数,答错的题目会自动进入错题本,可随时回来复习。'"></hr-tip>
|
||||||
|
|
||||||
|
<!-- ============ 上次练习 ============ -->
|
||||||
|
<hr-sec title="上次练习" :first="false"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<view class="hr-row-between">
|
||||||
|
<text class="prac-last">{{ meta.lastTime }} · 已练 {{ meta.lastDone }} 题</text>
|
||||||
|
<text class="prac-rate">正确率 {{ meta.lastRate }}%</text>
|
||||||
|
</view>
|
||||||
|
<view style="margin-top:9px">
|
||||||
|
<hr-progress :percent="lastPct"></hr-progress>
|
||||||
|
</view>
|
||||||
|
<text class="prac-note">上次练到第 {{ meta.lastDone }} 题,可继续或重新开始。</text>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-primary hr-btn-lg" style="flex:1" @click="onStart">
|
||||||
|
<text class="prac-btn-t">开始练习</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { practiceMeta } from '@/mock/index.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/** s36 顺序练习 */
|
||||||
|
const meta = practiceMeta
|
||||||
|
|
||||||
|
const lastPct = computed(() : number => {
|
||||||
|
return Math.round(meta.lastDone * 100 / meta.total)
|
||||||
|
})
|
||||||
|
|
||||||
|
function onStart() : void {
|
||||||
|
navTo('/pages/course/quiz?idx=0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMore() : void {
|
||||||
|
uni.showToast({ title: '练习记录 / 重置进度', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prac-hero {
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1D4ED8);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 44rpx 32rpx 44rpx 32rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -80rpx;
|
||||||
|
bottom: -104rpx;
|
||||||
|
width: 240rpx;
|
||||||
|
height: 240rpx;
|
||||||
|
border-radius: 120rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-ic {
|
||||||
|
width: 128rpx;
|
||||||
|
height: 128rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-s {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.86);
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-val {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-val-grn {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-last {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-rate {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-note {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prac-btn-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" :nav-title="'顺序练习 · 第 ' + (cur + 1) + ' 题'"
|
||||||
|
@right-click="onMarkCurrent">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon :name="isMarked ? 'bookmark-fill' : 'bookmark-line'" :color="isMarked ? '#F59E0B' : '#6B7280'"
|
||||||
|
font-size="19"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 进度 ============ -->
|
||||||
|
<view class="hr-row">
|
||||||
|
<view style="flex:1">
|
||||||
|
<hr-progress :percent="pct"></hr-progress>
|
||||||
|
</view>
|
||||||
|
<text class="qz-prog">{{ cur + 1 }} / {{ total }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 题卡 ============ -->
|
||||||
|
<hr-card style="margin-top:10px;padding:16px 14px 16px 14px">
|
||||||
|
<text class="qz-stem">{{ q.stem }}</text>
|
||||||
|
|
||||||
|
<view class="qz-opts">
|
||||||
|
<view v-for="(o, i) in q.options" :key="i" :class="optClass(i)" @click="choose(i)">
|
||||||
|
<view :class="optKeyClass(i)">
|
||||||
|
<text :class="optKeyTextClass(i)">{{ o.key }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="qz-opt-t">{{ o.text }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="qz-acts">
|
||||||
|
<view class="hr-btn hr-btn-ghost qz-act" @click="onMarkCurrent">
|
||||||
|
<x-icon :name="isMarked ? 'bookmark-fill' : 'bookmark-line'"
|
||||||
|
:color="isMarked ? '#F59E0B' : '#1F2937'" font-size="16"></x-icon>
|
||||||
|
<text class="qz-act-t">{{ isMarked ? '已标记' : '标记本题' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-btn hr-btn-ghost qz-act qz-act-last" @click="openCard">
|
||||||
|
<x-icon name="layout-grid-line" color="#1F2937" font-size="16"></x-icon>
|
||||||
|
<text class="qz-act-t">答题卡</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:10px" theme="info" icon="ℹ"
|
||||||
|
text="答错的题目会自动进入错题本,可在课题功能页统一复习。"></hr-tip>
|
||||||
|
|
||||||
|
<!-- 作答反馈 -->
|
||||||
|
<view v-if="showAnswer" class="qz-feedback" :class="isRight ? 'qz-feedback-ok' : 'qz-feedback-err'">
|
||||||
|
<view class="qz-fb-ic" :class="isRight ? 'qz-fb-ic-ok' : 'qz-fb-ic-err'">
|
||||||
|
<text class="qz-fb-ic-t">{{ isRight ? '✓' : '✕' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="qz-fb-b">{{ isRight ? '回答正确' : '回答错误,已加入错题本' }}</text>
|
||||||
|
<text class="qz-fb-s">正确答案:{{ rightAnswer }}</text>
|
||||||
|
<text class="qz-fb-s">{{ q.analysis }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-ghost qz-foot-prev" @click="prev">
|
||||||
|
<text class="qz-foot-t">上一题</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-btn hr-btn-primary" style="flex:1" @click="next">
|
||||||
|
<text class="qz-foot-t2">{{ isLast ? '交卷并查看成绩' : '下一题' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { quizQuestions } from '@/mock/index.uts'
|
||||||
|
import { quizState, setAnswer, toggleMark, answeredCount } from '@/utils/quiz.uts'
|
||||||
|
import { relaunchTo, navTo } from '@/utils/nav.uts'
|
||||||
|
import { HrQuestion } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/** s03 答题界面 */
|
||||||
|
const total = quizQuestions.length
|
||||||
|
const cur = ref(0)
|
||||||
|
const refreshKey = ref(0)
|
||||||
|
|
||||||
|
const q = computed(() : HrQuestion => {
|
||||||
|
return quizQuestions[cur.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
const selected = computed(() : number => {
|
||||||
|
const k = refreshKey.value
|
||||||
|
return quizState.answers[cur.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
const isMarked = computed(() : boolean => {
|
||||||
|
const k = refreshKey.value
|
||||||
|
return quizState.marked[cur.value]
|
||||||
|
})
|
||||||
|
|
||||||
|
const isLast = computed(() : boolean => {
|
||||||
|
return cur.value == total - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const pct = computed(() : number => {
|
||||||
|
return Math.round((cur.value + 1) * 100 / total)
|
||||||
|
})
|
||||||
|
|
||||||
|
const showAnswer = computed(() : boolean => {
|
||||||
|
const k = refreshKey.value
|
||||||
|
return quizState.answers[cur.value] >= 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const isRight = computed(() : boolean => {
|
||||||
|
const k = refreshKey.value
|
||||||
|
return quizState.answers[cur.value] == q.value.answer
|
||||||
|
})
|
||||||
|
|
||||||
|
const rightAnswer = computed(() : string => {
|
||||||
|
const o = q.value.options[q.value.answer]
|
||||||
|
return `${o.key}. ${o.text}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// uni-app x 页面显示生命周期:从答题卡返回时同步当前题号与作答状态
|
||||||
|
onPageShow(() => {
|
||||||
|
cur.value = quizState.current
|
||||||
|
refreshKey.value = refreshKey.value + 1
|
||||||
|
})
|
||||||
|
|
||||||
|
function optClass(i : number) : string {
|
||||||
|
if (selected.value == i) {
|
||||||
|
if (i == q.value.answer) { return 'qz-opt qz-opt-ok' }
|
||||||
|
return 'qz-opt qz-opt-sel'
|
||||||
|
}
|
||||||
|
if (showAnswer.value && i == q.value.answer) { return 'qz-opt qz-opt-ok' }
|
||||||
|
return 'qz-opt'
|
||||||
|
}
|
||||||
|
|
||||||
|
function optKeyClass(i : number) : string {
|
||||||
|
if (selected.value == i) {
|
||||||
|
if (i == q.value.answer) { return 'qz-ok qz-ok-ok' }
|
||||||
|
return 'qz-ok qz-ok-sel'
|
||||||
|
}
|
||||||
|
if (showAnswer.value && i == q.value.answer) { return 'qz-ok qz-ok-ok' }
|
||||||
|
return 'qz-ok'
|
||||||
|
}
|
||||||
|
|
||||||
|
function optKeyTextClass(i : number) : string {
|
||||||
|
if (selected.value == i || (showAnswer.value && i == q.value.answer)) {
|
||||||
|
return 'qz-ok-t qz-ok-t-on'
|
||||||
|
}
|
||||||
|
return 'qz-ok-t'
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(i : number) : void {
|
||||||
|
if (selected.value >= 0) {
|
||||||
|
uni.showToast({ title: '本题已作答', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setAnswer(cur.value, i)
|
||||||
|
refreshKey.value = refreshKey.value + 1
|
||||||
|
if (i != q.value.answer) {
|
||||||
|
// 答错自动进错题本(mock 阶段仅提示)
|
||||||
|
uni.showToast({ title: '已加入错题本', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMarkCurrent() : void {
|
||||||
|
toggleMark(cur.value)
|
||||||
|
refreshKey.value = refreshKey.value + 1
|
||||||
|
uni.showToast({ title: isMarked.value ? '已标记本题' : '已取消标记', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function prev() : void {
|
||||||
|
if (cur.value == 0) {
|
||||||
|
uni.showToast({ title: '已是第一题', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cur.value = cur.value - 1
|
||||||
|
quizState.current = cur.value
|
||||||
|
refreshKey.value = refreshKey.value + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠ submit 被 next() 调用,必须先定义(UTS 不提升函数声明)
|
||||||
|
function submit() : void {
|
||||||
|
const done = answeredCount()
|
||||||
|
uni.showModal({
|
||||||
|
title: '交卷确认',
|
||||||
|
content: `已作答 ${done} / ${total} 题${done < total ? ',仍有题目未作答' : ''}。交卷后无法修改。`,
|
||||||
|
cancelText: '继续答题',
|
||||||
|
confirmText: '交卷',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
quizState.submitted = true
|
||||||
|
uni.showToast({ title: '已交卷 · 得分 92', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
relaunchTo('/pages/study/exams')
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() : void {
|
||||||
|
if (isLast.value) {
|
||||||
|
submit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cur.value = cur.value + 1
|
||||||
|
quizState.current = cur.value
|
||||||
|
refreshKey.value = refreshKey.value + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCard() : void {
|
||||||
|
quizState.current = cur.value
|
||||||
|
navTo('/pages/course/answer-card')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qz-prog {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-stem {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-opts {
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-opt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 112rpx;
|
||||||
|
padding: 20rpx 26rpx 20rpx 26rpx;
|
||||||
|
border: 3rpx solid #E5E7EB;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-opt-sel {
|
||||||
|
border: 3rpx solid #2563EB;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-opt-ok {
|
||||||
|
border: 3rpx solid #10B981;
|
||||||
|
background-color: #ECFDF5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-ok {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
border: 3rpx solid #E5E7EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-ok-sel {
|
||||||
|
background-color: #2563EB;
|
||||||
|
border: 3rpx solid #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-ok-ok {
|
||||||
|
background-color: #10B981;
|
||||||
|
border: 3rpx solid #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-ok-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-ok-t-on {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-opt-t {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-acts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-act {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-act-last {
|
||||||
|
margin-right: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-act-t {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-feedback {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 22rpx 24rpx 22rpx 24rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-feedback-ok {
|
||||||
|
background-color: #ECFDF5;
|
||||||
|
border: 2rpx solid #A7F3D0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-feedback-err {
|
||||||
|
background-color: #FEF2F2;
|
||||||
|
border: 2rpx solid #FECACA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-fb-ic {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-fb-ic-ok {
|
||||||
|
background-color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-fb-ic-err {
|
||||||
|
background-color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-fb-ic-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-fb-b {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-fb-s {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-foot-prev {
|
||||||
|
flex: 0 0 220rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-foot-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qz-foot-t2 {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="课题详情" :xloading="true" @right-click="onMore">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- ============ 课题 hero ============ -->
|
||||||
|
<view class="subj-bar">
|
||||||
|
<view class="subj-c1"></view>
|
||||||
|
<view class="subj-info">
|
||||||
|
<view class="subj-nav" @click="prevSubject">
|
||||||
|
<x-icon name="arrow-left-s-line" color="#FFFFFF" font-size="18"></x-icon>
|
||||||
|
</view>
|
||||||
|
<view class="subj-mid">
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="subj-name">{{ subject }}</text>
|
||||||
|
<text class="subj-sub">急救基础 · 心肺复苏 · 资料 6 份 · 题库 50</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="subj-nav" @click="nextSubject">
|
||||||
|
<x-icon name="arrow-right-s-line" color="#FFFFFF" font-size="18"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 学习进度 ============ -->
|
||||||
|
<hr-card>
|
||||||
|
<view class="hr-row-between" style="margin-bottom:9px">
|
||||||
|
<text class="sj-lb">学习进度</text>
|
||||||
|
<text class="sj-pct">{{ progress }}%</text>
|
||||||
|
</view>
|
||||||
|
<hr-progress :percent="progress"></hr-progress>
|
||||||
|
<view class="hr-row" style="margin-top:11px">
|
||||||
|
<text class="sj-meta">已学资料 <text class="sj-meta-v">4/6</text></text>
|
||||||
|
<text class="sj-meta sj-meta-gap">已练题 <text class="sj-meta-v">32/50</text></text>
|
||||||
|
<text class="sj-meta sj-meta-gap">正确率 <text class="sj-meta-ok">{{ rate }}%</text></text>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 功能入口 ============ -->
|
||||||
|
<hr-sec title="功能入口"></hr-sec>
|
||||||
|
<view class="sj-entries">
|
||||||
|
<view v-for="(e, i) in entries" :key="i" class="sj-entry"
|
||||||
|
:class="i == entries.length - 1 ? 'sj-entry-last' : ''" @click="onEntry(i)">
|
||||||
|
<view class="sj-entry-ic" :style="{ backgroundColor: e.bg }">
|
||||||
|
<x-icon :name="entryIcon(e.icon)" :color="e.color" font-size="40rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="sj-entry-b">{{ e.name }}</text>
|
||||||
|
<text class="sj-entry-s">{{ e.sub }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 文档资料 ============ -->
|
||||||
|
<hr-sec title="文档资料" more="共 6 份 ›" @more-click="onMore"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<view v-for="(m, i) in materials" :key="m.id" class="doc-row"
|
||||||
|
:class="i == materials.length - 1 ? 'doc-row-last' : ''" @click="openMaterial(m)">
|
||||||
|
<view class="doc-ic" :style="{ backgroundColor: m.badgeColor }">
|
||||||
|
<x-icon v-if="m.kind == 'video'" name="play-fill" color="#FFFFFF" font-size="32rpx"></x-icon>
|
||||||
|
<text v-else class="doc-ic-tx">{{ m.icon }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="doc-b">{{ m.title }}</text>
|
||||||
|
<text class="doc-s">{{ m.meta }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="doc-go">
|
||||||
|
<x-icon name="arrow-right-s-line" color="#9CA3AF" font-size="34rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
<view class="doc-hint">
|
||||||
|
<text class="doc-hint-t">✦ 点击任意资料查看详情(视频与文档共用同一详情页)</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { subjectMaterials, courseGroups } from '@/mock/index.uts'
|
||||||
|
import { HrMaterial, HrSubject } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { toRemixIcon } from '@/utils/icon.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s35 课题详情
|
||||||
|
* 不再设章节目录与章节学习;改为「学习进度 + 错题/收藏/顺序练习 三入口 + 文档资料列表」。
|
||||||
|
*/
|
||||||
|
type Entry = { icon : string, name : string, sub : string, bg : string, color : string }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ⚠ 模板里不能直接调用「import 进来的函数」:
|
||||||
|
* uvue 编译器会把 `<x-icon :name="toRemixIcon(e.icon)">` 编译成 `unref(toRemixIcon)(e.icon)`,
|
||||||
|
* UTS/Kotlin 侧报 `找不到名称"invoke"` / `Function invocation 'toRemixIcon(...)' expected`。
|
||||||
|
* 必须包一层本文件内定义的函数(模板对 script 内定义的函数会走 _ctx.xxx,正常)。
|
||||||
|
*/
|
||||||
|
function entryIcon(glyph : string) : string {
|
||||||
|
return toRemixIcon(glyph)
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = ref('成人 CPR(高级生命支持)')
|
||||||
|
const progress = ref(68)
|
||||||
|
const rate = ref(78)
|
||||||
|
const materials = ref<HrMaterial[]>(subjectMaterials)
|
||||||
|
|
||||||
|
const entries : Entry[] = [
|
||||||
|
{ icon: '✗', name: '我的错题', sub: '12 道', bg: '#FEF2F2', color: '#DC2626' } as Entry,
|
||||||
|
{ icon: '★', name: '我的收藏', sub: '8 道', bg: '#FEF3C7', color: '#B45309' } as Entry,
|
||||||
|
{ icon: '▸', name: '顺序练习', sub: '50 题 · 约 30 分钟', bg: '#ECFDF5', color: '#059669' } as Entry
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 全部三级课题,用于上一个 / 下一个切换 */
|
||||||
|
const allSubjects = computed(() : HrSubject[] => {
|
||||||
|
const list : HrSubject[] = []
|
||||||
|
for (let gi = 0; gi < courseGroups.length; gi++) {
|
||||||
|
const g = courseGroups[gi]
|
||||||
|
for (let ii = 0; ii < g.items.length; ii++) {
|
||||||
|
list.push(g.items[ii])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
function indexOfSubject() : number {
|
||||||
|
for (let i = 0; i < allSubjects.value.length; i++) {
|
||||||
|
if (allSubjects.value[i].name == subject.value) { return i }
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function prevSubject() : void {
|
||||||
|
let i = indexOfSubject() - 1
|
||||||
|
if (i < 0) { i = allSubjects.value.length - 1 }
|
||||||
|
subject.value = allSubjects.value[i].name
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextSubject() : void {
|
||||||
|
let i = indexOfSubject() + 1
|
||||||
|
if (i >= allSubjects.value.length) { i = 0 }
|
||||||
|
subject.value = allSubjects.value[i].name
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntry(i : number) : void {
|
||||||
|
if (i == 0) { navTo('/pages/study/wrong-book'); return }
|
||||||
|
if (i == 1) { uni.showToast({ title: '我的收藏 · 8 道题', icon: 'none' }); return }
|
||||||
|
navTo('/pages/course/practice')
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMaterial(m : HrMaterial) : void {
|
||||||
|
navTo('/pages/course/media?id=' + m.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMore() : void {
|
||||||
|
uni.showToast({ title: '资料管理 / 下载全部', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ---------- hero ---------- */
|
||||||
|
.subj-bar {
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1D4ED8);
|
||||||
|
padding: 24rpx 28rpx 24rpx 28rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subj-c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -72rpx;
|
||||||
|
top: -92rpx;
|
||||||
|
width: 260rpx;
|
||||||
|
height: 260rpx;
|
||||||
|
border-radius: 130rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.09);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subj-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subj-nav {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.18);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subj-mid {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 18rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subj-name {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subj-sub {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 4rpx;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 学习进度 ---------- */
|
||||||
|
.sj-lb {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-pct {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-meta {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-meta-gap {
|
||||||
|
margin-left: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-meta-v {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-meta-ok {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #059669;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 功能入口 ---------- */
|
||||||
|
.sj-entries {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-entry {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 26rpx 12rpx 22rpx 12rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-entry-last {
|
||||||
|
margin-right: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-entry-ic {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-entry-b {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sj-entry-s {
|
||||||
|
font-size: 21.2rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 资料行 ---------- */
|
||||||
|
.doc-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 2rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-row-last {
|
||||||
|
border-bottom-width: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-ic {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-ic-tx {
|
||||||
|
font-size: 21.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-b {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-go {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-hint {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16rpx 0rpx 4rpx 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-hint-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="事件详情" :xloading="true" @right-click="onMore">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 事件 hero ============ -->
|
||||||
|
<view class="evt-head">
|
||||||
|
<view class="evt-head-c1"></view>
|
||||||
|
<view class="evt-head-c2"></view>
|
||||||
|
<text class="evt-head-t">{{ detail.name }}</text>
|
||||||
|
<text class="evt-head-s">{{ detail.sub }}</text>
|
||||||
|
|
||||||
|
<view class="eh-team">
|
||||||
|
<text class="eh-team-k">{{ detail.teamKey }}</text>
|
||||||
|
<view class="eh-team-line"></view>
|
||||||
|
<text class="eh-team-v">{{ detail.teamValue }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="evt-tags">
|
||||||
|
<view v-for="(tg, i) in detail.tags" :key="i" class="evt-tag">
|
||||||
|
<text class="evt-tag-t">{{ tg }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="evt-kpi">
|
||||||
|
<view class="evt-kpi-cell">
|
||||||
|
<text class="evt-kpi-b">{{ detail.victimTotal }}</text>
|
||||||
|
<text class="evt-kpi-s">伤员总数</text>
|
||||||
|
</view>
|
||||||
|
<view class="evt-kpi-sep"></view>
|
||||||
|
<view class="evt-kpi-cell">
|
||||||
|
<text class="evt-kpi-b">{{ detail.transferred }}</text>
|
||||||
|
<text class="evt-kpi-s">已转运</text>
|
||||||
|
</view>
|
||||||
|
<view class="evt-kpi-sep"></view>
|
||||||
|
<view class="evt-kpi-cell">
|
||||||
|
<text class="evt-kpi-b">{{ detail.pending }}</text>
|
||||||
|
<text class="evt-kpi-s">待处置</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 4 色分级计数条(常驻) ============ -->
|
||||||
|
<view style="margin-top:10px">
|
||||||
|
<hr-wband :red="detail.redCount" :amber="detail.amberCount" :green="detail.greenCount"
|
||||||
|
:black="detail.blackCount"></hr-wband>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 工作台 ============ -->
|
||||||
|
<hr-sec title="工作台" more="4 个入口 ›"></hr-sec>
|
||||||
|
<view class="ws-grid">
|
||||||
|
<view v-for="(w, i) in works" :key="i" class="ws-item"
|
||||||
|
:class="i == works.length - 1 ? 'ws-item-last' : ''" @click="onWorkClick(i)">
|
||||||
|
<view class="ws-ic" :style="{ backgroundColor: w.bg }">
|
||||||
|
<x-icon :name="w.icon" :color="w.color" font-size="36rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="ws-t">{{ w.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 救援团队 ============ -->
|
||||||
|
<hr-sec title="救援团队" more="5 人"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<x-avatar-group :list="teamAvatars" size="88rpx" round="28rpx" gutter="18rpx" :max-count="5"
|
||||||
|
:show-count="false" :flat="true" :random-bg-color="true" font-size="30rpx"></x-avatar-group>
|
||||||
|
<text class="tm-line">组长 张明远 · 检伤 李承志 · 转运 王志远</text>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 事件时间线 ============ -->
|
||||||
|
<hr-sec title="事件时间线"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<hr-timeline :nodes="detail.timeline"></hr-timeline>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { eventDetail } from '@/mock/index.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s34 事件详情(执行态工作台)
|
||||||
|
* 工作台由 8 入口精简为 4 入口:检伤建档 / 伤员查询 / 批量处置 / 批量转运。
|
||||||
|
* 4 色分级计数条常驻 hero 下方。救援端不开放事件上报入口(救援员是接单方)。
|
||||||
|
*/
|
||||||
|
type WorkItem = { icon : string, name : string, bg : string, color : string, url : string }
|
||||||
|
|
||||||
|
const detail = eventDetail
|
||||||
|
|
||||||
|
/** xAvatarGroup 需要 string[] */
|
||||||
|
const teamAvatars : string[] = ['张', '李', '王', '陈', '赵']
|
||||||
|
|
||||||
|
const works : WorkItem[] = [
|
||||||
|
{ icon: 'heart-pulse-line', name: '检伤建档', bg: '#FEF2F2', color: '#DC2626', url: '/pages/triage/create' } as WorkItem,
|
||||||
|
{ icon: 'file-list-3-line', name: '伤员查询', bg: '#EFF6FF', color: '#2563EB', url: '/pages/triage/victims' } as WorkItem,
|
||||||
|
{ icon: 'clipboard-line', name: '批量处置', bg: '#ECFDF5', color: '#059669', url: '/pages/triage/batch-treat' } as WorkItem,
|
||||||
|
{ icon: 'arrow-left-right-line', name: '批量转运', bg: '#FFFBEB', color: '#D97706', url: '/pages/triage/batch-transfer' } as WorkItem
|
||||||
|
]
|
||||||
|
|
||||||
|
function onWorkClick(i : number) : void {
|
||||||
|
navTo(works[i].url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMore() : void {
|
||||||
|
uni.showToast({ title: '事件归档 / 分享 / 报错', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ---------- hero ---------- */
|
||||||
|
.evt-head {
|
||||||
|
background-image: linear-gradient(to bottom right, #1E3A8A, #2563EB);
|
||||||
|
border-radius: 32rpx;
|
||||||
|
padding: 28rpx 28rpx 28rpx 28rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-head-c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -68rpx;
|
||||||
|
top: -88rpx;
|
||||||
|
width: 252rpx;
|
||||||
|
height: 252rpx;
|
||||||
|
border-radius: 126rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-head-c2 {
|
||||||
|
position: absolute;
|
||||||
|
right: 52rpx;
|
||||||
|
bottom: -96rpx;
|
||||||
|
width: 156rpx;
|
||||||
|
height: 156rpx;
|
||||||
|
border-radius: 78rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-head-t {
|
||||||
|
font-size: 34.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-head-s {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eh-team {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.14);
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.22);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 18rpx 22rpx 18rpx 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eh-team-k {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgba(255, 255, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eh-team-line {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 24rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.28);
|
||||||
|
margin-left: 18rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eh-team-v {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-tag {
|
||||||
|
background-color: rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 14rpx;
|
||||||
|
padding: 6rpx 18rpx 6rpx 18rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-tag-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-kpi {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-kpi-cell {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-kpi-b {
|
||||||
|
font-size: 42.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-kpi-s {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-kpi-sep {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.22);
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 工作台 ---------- */
|
||||||
|
.ws-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 28rpx 8rpx 28rpx 8rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item-last {
|
||||||
|
margin-right: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-ic {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-ic-t {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 团队 ---------- */
|
||||||
|
.tm-avt {
|
||||||
|
margin-right: 18rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tm-line {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-page" style="background-color:#F3F4F6;position:relative">
|
||||||
|
|
||||||
|
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
|
||||||
|
|
||||||
|
<!-- ============ 执行状态 Tab(进行中 / 历史) ============ -->
|
||||||
|
<x-tabs :list="tabs" :model-value="activeId" item-width="50%" height="88rpx" font-size="28rpx"
|
||||||
|
active-font-size="28rpx" title-color="#6B7280" active-title-color="#2563EB" line-color="#2563EB"
|
||||||
|
title-padding="0" :is-item-center="true" @change="onTabChange"></x-tabs>
|
||||||
|
|
||||||
|
<view class="hr-flow">
|
||||||
|
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<view v-for="(e, i) in shownList" :key="e.id" :class="cardClass(e)" @click="onCardClick(e)">
|
||||||
|
<view class="ev-top">
|
||||||
|
<view class="evt-dot" :style="dotStyle(e)"></view>
|
||||||
|
<text class="ev-name">{{ e.name }}</text>
|
||||||
|
<hr-pc :level="e.priority"></hr-pc>
|
||||||
|
</view>
|
||||||
|
<text class="ev-meta">{{ e.kind }}</text>
|
||||||
|
<text class="ev-meta2">现场:{{ e.place }} · {{ e.distance }}</text>
|
||||||
|
|
||||||
|
<view class="ev-foot">
|
||||||
|
<x-avatar-group :list="avatarsOf(e)" size="44rpx" round="14rpx" gutter="12rpx"
|
||||||
|
:max-count="5" :random-bg-color="true" font-size="22rpx"></x-avatar-group>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<view class="hr-row">
|
||||||
|
<text class="ev-go">{{ e.stage == 'history' ? '已完成' : '详情' }}</text>
|
||||||
|
<x-icon name="arrow-right-s-line" color="#2563EB" font-size="26rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 进行中说明 -->
|
||||||
|
<text v-if="tab == 0" class="ev-note">仅显示与我相关的派单</text>
|
||||||
|
<text v-if="tab == 0" class="ev-note2">时间与处理状态由后台同步,APP 端不展示</text>
|
||||||
|
|
||||||
|
<hr-empty v-if="shownList.length == 0" icon="alarm-warning-line" title="暂无事件"
|
||||||
|
sub="分配给你的派单会出现在「进行中」,处置完成后归档到「历史」"></hr-empty>
|
||||||
|
|
||||||
|
<view style="height:28rpx"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 加载中覆盖层(组件库 xLoading) -->
|
||||||
|
<view v-if="loading" class="hr-loading-mask">
|
||||||
|
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB"
|
||||||
|
text-color="#6B7280"></x-loading>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-tabbar :active="1"></hr-tabbar>
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { eventList, eventDetail } from '@/mock/index.uts'
|
||||||
|
import { HrEventItem, HrEventMember } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { mockRequest } from '@/utils/request.uts'
|
||||||
|
import { TABS_ITEM_INFO, TABS_ITEM } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
import { markEventsRead } from '@/utils/unread.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s07 / s42 事件中心(执行态)
|
||||||
|
* 用「执行状态」Tab 切换:进行中 / 历史(组件库 xTabs)。
|
||||||
|
* ⚠ 原「待接收」Tab 及其全屏推送接收流程(60 秒倒计时 / 立即接收 / 稍后处理)已移除,
|
||||||
|
* 页面只服务进行中与历史两类事件。
|
||||||
|
*/
|
||||||
|
const stageIds : string[] = ['0', '1']
|
||||||
|
const activeId = ref('0')
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 紧急事件红点「呼吸」光环
|
||||||
|
* ⚠ App 端(uvue)不支持 @keyframes / animation(编译期直接报错),
|
||||||
|
* 改为定时器逐帧推进 pulseF,再由 dotStyle() 下发 box-shadow。
|
||||||
|
* 周期 1.7s = 25 帧 × 68ms;第 0~19 帧扩散(19/25 ≈ 设计稿 75%),第 20~24 帧静默。
|
||||||
|
* 触发条件只看数据的 urgent 字段(与 stage 无关)。
|
||||||
|
*/
|
||||||
|
const PULSE_FRAMES = 25
|
||||||
|
const PULSE_RAMP = 19
|
||||||
|
|
||||||
|
const pulseF = ref(0)
|
||||||
|
let pulseTimer : number = 0
|
||||||
|
|
||||||
|
function startPulse() : void {
|
||||||
|
if (pulseTimer != 0) { return }
|
||||||
|
pulseTimer = setInterval(() => {
|
||||||
|
const f = pulseF.value + 1
|
||||||
|
pulseF.value = f >= PULSE_FRAMES ? 0 : f
|
||||||
|
}, 68)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPulse() : void {
|
||||||
|
if (pulseTimer != 0) {
|
||||||
|
clearInterval(pulseTimer)
|
||||||
|
pulseTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 红点样式:底色取数据里的 dotColor;紧急事件额外叠加扩散光环 */
|
||||||
|
function dotStyle(e : HrEventItem) : string {
|
||||||
|
const base = 'background-color:' + e.dotColor + ';'
|
||||||
|
if (!e.urgent) { return base }
|
||||||
|
const i = pulseF.value
|
||||||
|
// 扩散结束后的静默帧:半径与透明度都归零(设计稿 75% → 100%)
|
||||||
|
if (i > PULSE_RAMP) { return base + 'box-shadow:0 0 0 0rpx rgba(220, 38, 38, 0);' }
|
||||||
|
const k = i / PULSE_RAMP
|
||||||
|
const r = Math.round(k * 16)
|
||||||
|
const a = Math.round(55 * (1 - k))
|
||||||
|
return base + 'box-shadow:0 0 0 ' + r.toString() + 'rpx rgba(220, 38, 38, 0.' + a.toString() + ');'
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
// 模拟 1s 请求,期间展示 xSkeleton 骨架屏
|
||||||
|
mockRequest(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
startPulse()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 进入事件列表即视为已查看 → TabBar 事件角标隐藏;同时恢复红点呼吸
|
||||||
|
onPageShow(() => {
|
||||||
|
markEventsRead()
|
||||||
|
startPulse()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面不可见时停掉呼吸定时器,避免后台空转
|
||||||
|
onPageHide(() => {
|
||||||
|
stopPulse()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 当前 tab 下标(由 xTabs 的字符串 id 换算) */
|
||||||
|
const tab = computed(() : number => {
|
||||||
|
const i = stageIds.indexOf(activeId.value)
|
||||||
|
return i < 0 ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 顶部执行状态切换:只展示标题,不带数量角标(角标会挤压标题并干扰视线) */
|
||||||
|
const tabs = computed(() : TABS_ITEM_INFO[] => {
|
||||||
|
return [
|
||||||
|
{ title: '进行中', id: '0' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '历史', id: '1' } as TABS_ITEM_INFO
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const shownList = computed(() : HrEventItem[] => {
|
||||||
|
const stage = tab.value == 0 ? 'ongoing' : 'history'
|
||||||
|
const list : HrEventItem[] = []
|
||||||
|
for (let i = 0; i < eventList.length; i++) {
|
||||||
|
if (eventList[i].stage == stage) { list.push(eventList[i]) }
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
|
/** xAvatarGroup 需要 string[] */
|
||||||
|
function avatarsOf(e : HrEventItem) : string[] {
|
||||||
|
const arr : string[] = []
|
||||||
|
for (let i = 0; i < e.members.length; i++) {
|
||||||
|
arr.push(e.members[i].avatar)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠ x-tabs 的 change 下发的是 TABS_ITEM(库内部把 TABS_ITEM_INFO 转成了 TABS_ITEM),
|
||||||
|
// 形参写 TABS_ITEM_INFO 会在 App 端报 Kotlin 类型不匹配
|
||||||
|
function onTabChange(item : TABS_ITEM, index : number) : void {
|
||||||
|
activeId.value = item.id == '' ? index.toString() : item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardClass(e : HrEventItem) : string {
|
||||||
|
return e.urgent ? 'evt-card evt-urgent' : 'evt-card'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFilter() : void {
|
||||||
|
uni.showToast({ title: '筛选:按状态 / 优先级 / 时间', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点卡片统一进入事件详情(原「待接收 → 唤起全屏推送」分支已随待接收 Tab 一并移除) */
|
||||||
|
function onCardClick(e : HrEventItem) : void {
|
||||||
|
navTo('/pages/event/detail')
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
stopPulse()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 顶部标题栏 / 自绘 Tab / 数量角标的样式均已移除:
|
||||||
|
导航由 hr-statusbar 承担,状态切换改用组件库 x-tabs。 */
|
||||||
|
|
||||||
|
/* ---------- 事件卡片 ---------- */
|
||||||
|
.evt-card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 24rpx 26rpx 24rpx 26rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-urgent {
|
||||||
|
border: 2rpx solid #FECACA;
|
||||||
|
background-color: #FFFBFB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evt-dot {
|
||||||
|
width: 16rpx;
|
||||||
|
height: 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
margin: 0 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-name {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-meta {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-meta2 {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-foot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-go {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-note {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ev-note2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 紧急事件红点呼吸(设计稿 evtPulse)
|
||||||
|
⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
|
||||||
|
已改为页面内 setInterval 推进 pulseF,再由 dotStyle() 下发 box-shadow —— 见 script 顶部。
|
||||||
|
原 P0 全屏推送的 .ev-push* 整组样式已随「待接收」流程一并移除。 */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="" :xloading="true">
|
||||||
|
|
||||||
|
<!-- ============ 顶部 hero(日常学习总览) ============ -->
|
||||||
|
<view class="tr-hero">
|
||||||
|
<view class="tr-hero-c1"></view>
|
||||||
|
<view class="tr-hero-c2"></view>
|
||||||
|
|
||||||
|
<view class="tr-hero-row">
|
||||||
|
<view class="hr-row hr-flex1">
|
||||||
|
<view class="tr-avt">
|
||||||
|
<text class="tr-avt-t">{{ user.avatar }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="tr-hi">{{ user.name }} · {{ user.title }}</text>
|
||||||
|
<text class="tr-sub">{{ user.dept }} · 工号 {{ user.jobNo }} · 培训等级 B</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="tr-bell" @click="goMsg">
|
||||||
|
<x-icon name="notification-3-line" color="#FFFFFF" font-size="17"></x-icon>
|
||||||
|
<view class="tr-bell-dot"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tr-statbar">
|
||||||
|
<view class="tr-sb-cell">
|
||||||
|
<view class="hr-row tr-sb-bwrap">
|
||||||
|
<text class="tr-sb-b">3.5</text>
|
||||||
|
<text class="tr-sb-u">h</text>
|
||||||
|
</view>
|
||||||
|
<text class="tr-sb-s">本周学时</text>
|
||||||
|
</view>
|
||||||
|
<view class="tr-sb-sep"></view>
|
||||||
|
<view class="tr-sb-cell">
|
||||||
|
<view class="hr-row tr-sb-bwrap">
|
||||||
|
<text class="tr-sb-b">12</text>
|
||||||
|
<text class="tr-sb-u">门</text>
|
||||||
|
</view>
|
||||||
|
<text class="tr-sb-s">已完成课程</text>
|
||||||
|
</view>
|
||||||
|
<view class="tr-sb-sep"></view>
|
||||||
|
<view class="tr-sb-cell">
|
||||||
|
<view class="hr-row tr-sb-bwrap">
|
||||||
|
<text class="tr-sb-b">3</text>
|
||||||
|
<text class="tr-sb-u">张</text>
|
||||||
|
</view>
|
||||||
|
<text class="tr-sb-s">有效证书</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 待办任务 ============ -->
|
||||||
|
<hr-sec title="待办任务" more="全部 ›" first @more-click="onTodoAll"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow v-for="(t, i) in todos" :key="i" :icon="t.icon" :theme="t.iconTheme" :title="t.title"
|
||||||
|
:sub="t.sub" :value="t.action" value-theme="pri" :value-bold="true" :is-last="i == todos.length - 1"
|
||||||
|
@click="onTodoClick(i)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 学习动态 ============ -->
|
||||||
|
<hr-sec title="学习动态" more="全部 ›" @more-click="onActivityAll"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow v-for="(a, i) in acts" :key="i" :icon="a.icon" :theme="a.iconTheme" :title="a.title"
|
||||||
|
:sub="a.sub" value="›" :is-last="i == acts.length - 1" @click="onActivityClick(i)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 工具箱 ============ -->
|
||||||
|
<hr-sec title="工具箱"></hr-sec>
|
||||||
|
<view class="tr-quick">
|
||||||
|
<view v-for="(q, i) in tools" :key="i" class="tr-q" :class="i == tools.length - 1 ? 'tr-q-last' : ''"
|
||||||
|
@click="onToolClick(i)">
|
||||||
|
<view class="tr-q-ic" :style="{ backgroundColor: q.iconBg }">
|
||||||
|
<x-icon :name="q.icon" :color="q.iconColor" font-size="36rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="tr-q-t">{{ q.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:tabbar>
|
||||||
|
<hr-tabbar :active="0"></hr-tabbar>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { hrSession } from '@/utils/role.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { todoTasks, activities, toolBox } from '@/mock/index.uts'
|
||||||
|
import { HrTodoTask, HrActivity, HrToolBox } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s01 学员端首页(日常态 · 纯学习)
|
||||||
|
* 设计约束:首页只承载日常学习(学习总览 / 待办任务 / 学习动态 / 工具箱),
|
||||||
|
* 不含任何现场作业入口 —— 现场作业从「事件」Tab 进入。
|
||||||
|
*/
|
||||||
|
const user = hrSession
|
||||||
|
const todos = ref<HrTodoTask[]>(todoTasks)
|
||||||
|
const acts = ref<HrActivity[]>(activities)
|
||||||
|
const tools = ref<HrToolBox[]>(toolBox)
|
||||||
|
|
||||||
|
function goMsg() : void {
|
||||||
|
navTo('/pages/message/list')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTodoAll() : void {
|
||||||
|
navTo('/pages/course/library')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTodoClick(i : number) : void {
|
||||||
|
if (i == 0) {
|
||||||
|
navTo('/pages/course/subject')
|
||||||
|
} else {
|
||||||
|
navTo('/pages/study/exams')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActivityAll() : void {
|
||||||
|
navTo('/pages/study/center')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onActivityClick(i : number) : void {
|
||||||
|
navTo('/pages/study/center')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToolClick(i : number) : void {
|
||||||
|
if (i == 0) { navTo('/pages/course/library'); return }
|
||||||
|
if (i == 1) { navTo('/pages/study/exams'); return }
|
||||||
|
if (i == 2) { navTo('/pages/study/certificates'); return }
|
||||||
|
navTo('/pages/study/wrong-book')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ---------- hero ---------- */
|
||||||
|
.tr-hero {
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1E40AF);
|
||||||
|
padding: 28rpx 32rpx 44rpx 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hero-c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -80rpx;
|
||||||
|
top: -100rpx;
|
||||||
|
width: 300rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
border-radius: 150rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hero-c2 {
|
||||||
|
position: absolute;
|
||||||
|
right: 64rpx;
|
||||||
|
bottom: -112rpx;
|
||||||
|
width: 192rpx;
|
||||||
|
height: 192rpx;
|
||||||
|
border-radius: 96rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hero-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-avt {
|
||||||
|
width: 88rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.22);
|
||||||
|
border: 3rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-avt-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hi {
|
||||||
|
font-size: 40.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sub {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.82);
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-bell {
|
||||||
|
width: 68rpx;
|
||||||
|
height: 68rpx;
|
||||||
|
border-radius: 34rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.16);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-bell-dot {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 28rpx;
|
||||||
|
margin-top: -24rpx;
|
||||||
|
width: 14rpx;
|
||||||
|
height: 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background-color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-statbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding: 28rpx 8rpx 28rpx 8rpx;
|
||||||
|
box-shadow: 0 12rpx 36rpx rgba(15, 23, 42, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sb-cell {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sb-bwrap {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sb-b {
|
||||||
|
font-size: 44.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sb-u {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-left: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sb-s {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sb-sep {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 工具箱 ---------- */
|
||||||
|
.tr-quick {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-q {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 24rpx 4rpx 20rpx 4rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-q-last {
|
||||||
|
margin-right: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-q-ic {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-q-ic-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-q-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-page" style="background-color:#FFFFFF">
|
||||||
|
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
|
||||||
|
|
||||||
|
<view class="lg-wrap">
|
||||||
|
<!-- 品牌标识 -->
|
||||||
|
<view class="lg-logo">
|
||||||
|
<x-icon name="add-line" color="#FFFFFF" font-size="34" font-weight="bold"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="lg-title">医院救援管理平台</text>
|
||||||
|
<text class="lg-sub">请使用院内工号登录\n角色由后台根据工号自动识别</text>
|
||||||
|
|
||||||
|
<!-- 角色识别结果(点击可切换演示角色) -->
|
||||||
|
<view class="lg-role" @click="toggleRole">
|
||||||
|
<view class="lg-role-ic">
|
||||||
|
<x-icon :name="role == 'keeper' ? 'archive-line' : 'shield-cross-line'" color="#FFFFFF"
|
||||||
|
font-size="19"></x-icon>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="lg-role-t">{{ roleLabel }}</text>
|
||||||
|
<text class="lg-role-s">工号识别后自动进入对应工作台 · 点此切换演示角色</text>
|
||||||
|
</view>
|
||||||
|
<x-icon name="arrow-left-right-line" color="#3B6BC7" font-size="16"></x-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 表单(xInput) -->
|
||||||
|
<view class="lg-form">
|
||||||
|
<x-input v-model="jobNo" left-text="工号" left-icon="user-3-line" placeholder="请输入院内工号"
|
||||||
|
round="22rpx" height="96rpx" font-size="30rpx" color="#F9FAFB" font-color="#1F2937"
|
||||||
|
icon-color="#9CA3AF"></x-input>
|
||||||
|
<view class="lg-field-gap">
|
||||||
|
<x-input v-model="password" :password="true" left-text="密码" left-icon="lock-line"
|
||||||
|
placeholder="请输入密码" round="22rpx" height="96rpx" font-size="30rpx" color="#F9FAFB"
|
||||||
|
font-color="#1F2937" icon-color="#9CA3AF"></x-input>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="lg-opt">
|
||||||
|
<view class="hr-row">
|
||||||
|
<x-switch v-model="remember" size="small" color="#10B981"></x-switch>
|
||||||
|
<text class="lg-opt-t">记住工号</text>
|
||||||
|
</view>
|
||||||
|
<text class="lg-opt-forget" @click="onForget">忘记密码?</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-btn hr-btn-primary hr-btn-lg lg-btn" @click="onLogin">
|
||||||
|
<text class="lg-btn-t">登 录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="lg-foot">登录即表示同意《服务协议》与《隐私政策》\n遇到问题请联系急救中心值班室 8801</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { loginAs, hrSession } from '@/utils/role.uts'
|
||||||
|
import { relaunchTo } from '@/utils/nav.uts'
|
||||||
|
import { HrRole } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s26 登录
|
||||||
|
* 双角色入口:默认按设计稿为「救援人员 · 急救中心」,
|
||||||
|
* 点击角色卡可在「救援人员端 / 仓管人员端」之间切换,用于演示双角色差异。
|
||||||
|
*/
|
||||||
|
const role = ref<HrRole>('rescuer')
|
||||||
|
const jobNo = ref('1247')
|
||||||
|
const password = ref('123456')
|
||||||
|
const remember = ref(true)
|
||||||
|
|
||||||
|
const roleLabel = computed(() : string => {
|
||||||
|
return role.value == 'keeper' ? '仓管人员 · 后勤保障科' : '救援人员 · 急救中心'
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleRole() : void {
|
||||||
|
if (role.value == 'rescuer') {
|
||||||
|
role.value = 'keeper'
|
||||||
|
jobNo.value = '2083'
|
||||||
|
} else {
|
||||||
|
role.value = 'rescuer'
|
||||||
|
jobNo.value = '1247'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onForget() : void {
|
||||||
|
uni.showModal({ title: '忘记密码', content: '请联系急救中心值班室 8801 重置工号密码。', showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogin() : void {
|
||||||
|
if (jobNo.value == '') {
|
||||||
|
uni.showToast({ title: '请输入工号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.value == '') {
|
||||||
|
uni.showToast({ title: '请输入密码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '登录中' })
|
||||||
|
// 工号 2083 段自动识别为仓管人员端
|
||||||
|
let r : HrRole = role.value
|
||||||
|
if (jobNo.value.startsWith('2')) { r = 'keeper' }
|
||||||
|
if (jobNo.value.startsWith('1')) { r = 'rescuer' }
|
||||||
|
loginAs(r)
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
relaunchTo(hrSession.role == 'keeper' ? '/pages/warehouse/scan' : '/pages/home/index')
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.lg-wrap {
|
||||||
|
flex: 1;
|
||||||
|
padding-left: 52rpx;
|
||||||
|
padding-right: 52rpx;
|
||||||
|
padding-bottom: 60rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-logo {
|
||||||
|
width: 132rpx;
|
||||||
|
height: 132rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1D4ED8);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 52rpx;
|
||||||
|
align-self: center;
|
||||||
|
box-shadow: 0 20rpx 52rpx rgba(37, 99, 235, 0.30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 42.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-top: 36rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-sub {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-role {
|
||||||
|
margin-top: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
border: 2rpx solid #BFDBFE;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 24rpx 26rpx 24rpx 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-role-ic {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background-color: #2563EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-role-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1E40AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-role-s {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #3B6BC7;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-form {
|
||||||
|
margin-top: 36rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-field-gap {
|
||||||
|
margin-top: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-opt {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-opt-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-opt-forget {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-btn {
|
||||||
|
margin-top: 44rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-btn-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-foot {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-foot2 {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 2rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="通讯录" @right-click="onSearch">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="search-line" color="#6B7280" font-size="34rpx"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 分组列表:指挥调度 / 协作医院 / 本队同事 ============ -->
|
||||||
|
<view v-for="(g, gi) in groups" :key="gi">
|
||||||
|
<hr-sec :title="g.title" :first="gi == 0"></hr-sec>
|
||||||
|
|
||||||
|
<!-- 卡片内每行:左侧头像/机构图标 + 姓名与号码 + 呼叫按钮 -->
|
||||||
|
<hr-card padding="4rpx 26rpx">
|
||||||
|
<view v-for="(c, ci) in g.items" :key="ci"
|
||||||
|
:class="ci == g.items.length - 1 ? 'hr-lrow hr-lrow-last' : 'hr-lrow'" @click="onCall(c)">
|
||||||
|
<view class="ct-blk" :style="blkStyle(c)">
|
||||||
|
<!-- 机构(医院 / 协作单位)用图标,个人用姓名首字 -->
|
||||||
|
<x-icon v-if="!c.useAvatar" name="hospital-line" color="#2563EB"
|
||||||
|
font-size="30rpx"></x-icon>
|
||||||
|
<text v-else class="ct-blk-t">{{ blkText(c) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-lmain">
|
||||||
|
<text class="hr-lmain-b">{{ c.name }}</text>
|
||||||
|
<text class="hr-lmain-s">{{ c.phone }}</text>
|
||||||
|
</view>
|
||||||
|
<x-icon name="phone-fill" color="#2563EB" font-size="32rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { contactGroups } from '@/mock/index.uts'
|
||||||
|
import { HrContact, HrContactGroup } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s28 通讯录
|
||||||
|
* 与设计稿一致:按「指挥调度 / 协作医院 / 本队同事」三段分组,
|
||||||
|
* 每组 = 小节标题 + 卡片列表行(头像 + 姓名 + 号码 + 呼叫)。
|
||||||
|
* 数据全部来自 mock/index.uts,换接口只改取数处。
|
||||||
|
*/
|
||||||
|
const groups : HrContactGroup[] = contactGroups
|
||||||
|
|
||||||
|
/** 头像文字:优先用数据里的字母头,缺省回落到姓名首字 */
|
||||||
|
function blkText(c : HrContact) : string {
|
||||||
|
if (c.avatar != '') { return c.avatar }
|
||||||
|
if (c.name.length > 0) { return c.name.substring(0, 1) }
|
||||||
|
return '医'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像底色。uvue 不支持复合类选择器(.ct-blk.ct-blk-gn),
|
||||||
|
* 因此主题色用内联样式下发;机构块固定浅蓝底。
|
||||||
|
*/
|
||||||
|
function blkStyle(c : HrContact) : string {
|
||||||
|
if (!c.useAvatar) { return 'background-color:#EFF6FF;' }
|
||||||
|
if (c.theme == 'gn') { return 'background-color:#059669;' }
|
||||||
|
if (c.theme == 'ag') { return 'background-color:#D97706;' }
|
||||||
|
if (c.theme == 'rd') { return 'background-color:#DC2626;' }
|
||||||
|
if (c.theme == 'gy') { return 'background-color:#94A3B8;' }
|
||||||
|
return 'background-color:#2563EB;'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 只保留数字字符(0-9) */
|
||||||
|
function digitsOnly(s : string) : string {
|
||||||
|
const table = '0123456789'
|
||||||
|
let out = ''
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const ch = s.charAt(i)
|
||||||
|
if (table.indexOf(ch) > -1) { out += ch }
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从「值班室 8801 · 手机 138****2201」这类文案里取出可拨号码。
|
||||||
|
* 规则:按「·」分段,取「标签 号码」中的号码部分,
|
||||||
|
* 纯数字(不含 * 掩码)、长度≥4 才算号码;「工号 1248」这类直接跳过。
|
||||||
|
*/
|
||||||
|
function extractPhone(text : string) : string {
|
||||||
|
const parts = text.split('·')
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
const seg = parts[i].trim()
|
||||||
|
const idx = seg.indexOf(' ')
|
||||||
|
const label = idx >= 0 ? seg.substring(0, idx) : ''
|
||||||
|
const raw = idx >= 0 ? seg.substring(idx + 1) : seg
|
||||||
|
if (label.indexOf('工号') > -1) { continue }
|
||||||
|
const digits = digitsOnly(raw)
|
||||||
|
const compact = raw.replaceAll(' ', '').replaceAll('-', '')
|
||||||
|
if (digits.length >= 4 && digits.length == compact.length) { return digits }
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠ UTS 不提升函数声明:extractPhone / digitsOnly 必须写在 onCall 之前
|
||||||
|
function onCall(c : HrContact) : void {
|
||||||
|
const digits = extractPhone(c.phone)
|
||||||
|
if (digits == '') {
|
||||||
|
uni.showToast({ title: '暂无可用号码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showModal({
|
||||||
|
title: c.name,
|
||||||
|
content: `呼叫 ${digits}?`,
|
||||||
|
confirmText: '呼叫',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) { return }
|
||||||
|
// #ifdef APP
|
||||||
|
uni.makePhoneCall({ phoneNumber: digits })
|
||||||
|
// #endif
|
||||||
|
// #ifndef APP
|
||||||
|
uni.showToast({ title: '已复制号码 ' + digits, icon: 'none' })
|
||||||
|
uni.setClipboardData({ data: digits })
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() : void {
|
||||||
|
uni.showToast({ title: '搜索联系人 / 医院', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 头像 / 机构图标块:设计稿 .avt.sm / .lic(30~32px) */
|
||||||
|
.ct-blk {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-blk-t {
|
||||||
|
font-size: 25rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-page" style="background-color:#F3F4F6">
|
||||||
|
|
||||||
|
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
|
||||||
|
|
||||||
|
<view class="ms-nav">
|
||||||
|
<text class="ms-nav-t">消息</text>
|
||||||
|
<view class="ms-nav-a" @click="onReadAll">
|
||||||
|
<x-icon name="check-double-line" color="#6B7280" font-size="16"></x-icon>
|
||||||
|
<text class="ms-nav-a-t">全部已读</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-flow">
|
||||||
|
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- 消息分类筛选(组件库 xRadioButton 分段切换) -->
|
||||||
|
<view style="margin-bottom:8rpx">
|
||||||
|
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
|
||||||
|
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
|
||||||
|
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-for="(g, gi) in groups" :key="gi">
|
||||||
|
<hr-sec :title="g.title" :more="g.count > 0 ? g.count.toString() + ' 条' : ''"></hr-sec>
|
||||||
|
<hr-card :variant="g.danger ? 'danger' : 'list'">
|
||||||
|
<hr-lrow v-for="(m, i) in g.items" :key="i" :icon="m.icon" :theme="m.theme" :title="m.title"
|
||||||
|
:sub="m.sub" :value="m.action" :value-theme="m.theme == 'red' ? 'red' : 'pri'"
|
||||||
|
:value-bold="true" :is-last="i == g.items.length - 1" @click="onMsgClick(m, gi, i)">
|
||||||
|
<template v-slot:right>
|
||||||
|
<view v-if="isUnread(gi, i, m.unread)" class="hr-badge">
|
||||||
|
<text class="hr-badge-t">{{ m.unread }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-empty v-if="groups.length == 0" icon="✉" title="暂无消息"
|
||||||
|
sub="库存预警、借用审批与系统通知会汇总到这里"></hr-empty>
|
||||||
|
|
||||||
|
<view style="height:28rpx"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 加载中覆盖层(组件库 xLoading) -->
|
||||||
|
<view v-if="loading" class="hr-loading-mask">
|
||||||
|
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB"
|
||||||
|
text-color="#6B7280"></x-loading>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-tabbar :active="3"></hr-tabbar>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { keeperMsgGroups } from '@/mock/index.uts'
|
||||||
|
import { HrMsg, HrMsgGroup } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { mockRequest } from '@/utils/request.uts'
|
||||||
|
import { markMessagesRead } from '@/utils/unread.uts'
|
||||||
|
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s33 消息中心 · 物资端(仓管 Tab「消息」)
|
||||||
|
* 库存预警自动置顶;借用审批「去审批」直达设备借出台账。
|
||||||
|
* 角标 = 未读数,阅读后消失:整页查看清 tabbar/入口角标,点开单条清该条角标。
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 消息分类筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||||||
|
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||||||
|
*/
|
||||||
|
const chipTabs : RADIO_BUTTON[] = [
|
||||||
|
{ id: '0', title: '全部 9' } as RADIO_BUTTON,
|
||||||
|
{ id: '1', title: '库存预警 4' } as RADIO_BUTTON,
|
||||||
|
{ id: '2', title: '借用审批 3' } as RADIO_BUTTON,
|
||||||
|
{ id: '3', title: '系统 2' } as RADIO_BUTTON
|
||||||
|
]
|
||||||
|
const chipId = ref('0')
|
||||||
|
/** 由字符串 id 换算分类序号,供分组筛选使用 */
|
||||||
|
const chip = computed(() : number => {
|
||||||
|
const i = parseInt(chipId.value)
|
||||||
|
return isNaN(i) ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 已读的条目(key = 分组下标-条目下标) */
|
||||||
|
const readKeys = ref<string[]>([])
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
onLoad(() => {
|
||||||
|
mockRequest(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageShow(() => {
|
||||||
|
markMessagesRead()
|
||||||
|
})
|
||||||
|
|
||||||
|
function keyOf(gi : number, i : number) : string {
|
||||||
|
return gi.toString() + '-' + i.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRead(gi : number, i : number) : boolean {
|
||||||
|
return readKeys.value.indexOf(keyOf(gi, i)) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUnread(gi : number, i : number, n : number) : boolean {
|
||||||
|
if (n <= 0) { return false }
|
||||||
|
return !isRead(gi, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
function markRead(gi : number, i : number) : void {
|
||||||
|
const k = keyOf(gi, i)
|
||||||
|
if (readKeys.value.indexOf(k) > -1) { return }
|
||||||
|
const arr = readKeys.value
|
||||||
|
arr.push(k)
|
||||||
|
readKeys.value = arr
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = computed(() : HrMsgGroup[] => {
|
||||||
|
if (chip.value == 1) {
|
||||||
|
return [keeperMsgGroups[0]]
|
||||||
|
}
|
||||||
|
if (chip.value == 2) {
|
||||||
|
return [keeperMsgGroups[1]]
|
||||||
|
}
|
||||||
|
if (chip.value == 3) {
|
||||||
|
return [keeperMsgGroups[2]]
|
||||||
|
}
|
||||||
|
return keeperMsgGroups
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMsgClick(m : HrMsg, gi : number, i : number) : void {
|
||||||
|
markRead(gi, i)
|
||||||
|
if (m.action == '去审批 ›') {
|
||||||
|
navTo('/pages/warehouse/devices')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showModal({ title: m.title, content: m.sub, showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReadAll() : void {
|
||||||
|
// 全部标记已读 → 所有条目角标一并消失
|
||||||
|
const arr : string[] = []
|
||||||
|
for (let gi = 0; gi < groups.value.length; gi++) {
|
||||||
|
const g = groups.value[gi]
|
||||||
|
for (let i = 0; i < g.items.length; i++) {
|
||||||
|
arr.push(keyOf(gi, i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readKeys.value = arr
|
||||||
|
markMessagesRead()
|
||||||
|
uni.showToast({ title: '已全部标记为已读', icon: 'success' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ms-nav {
|
||||||
|
height: 96rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 32rpx;
|
||||||
|
padding-right: 32rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-bottom: 2rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-nav-t {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-nav-a {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ms-nav-a-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="消息中心" :xloading="true" @right-click="onReadAll">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="check-double-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- 消息分类筛选(组件库 xRadioButton 分段切换) -->
|
||||||
|
<view style="margin-bottom:8rpx">
|
||||||
|
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
|
||||||
|
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
|
||||||
|
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-for="(g, gi) in groups" :key="gi">
|
||||||
|
<hr-sec :title="g.title" :more="g.count > 0 ? g.count.toString() + ' 条' : ''"></hr-sec>
|
||||||
|
<hr-card :variant="g.danger ? 'danger' : 'list'">
|
||||||
|
<hr-lrow v-for="(m, i) in g.items" :key="i" :icon="m.icon" :theme="m.theme" :title="m.title"
|
||||||
|
:sub="m.sub" :value="m.action" :value-theme="m.theme == 'red' ? 'red' : 'pri'"
|
||||||
|
:value-bold="true" :is-last="i == g.items.length - 1" @click="onMsgClick(m)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { rescuerMsgGroups } from '@/mock/index.uts'
|
||||||
|
import { HrMsg, HrMsgGroup } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { markMessagesRead } from '@/utils/unread.uts'
|
||||||
|
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s27 消息中心 · 救护端
|
||||||
|
* P0 派单消息可直接「去接收」,跳转到事件中心查看进行中的派单。
|
||||||
|
* 查看后清未读 → 「我的」页消息中心入口角标消失。
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 消息分类筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||||||
|
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||||||
|
*/
|
||||||
|
const chipTabs : RADIO_BUTTON[] = [
|
||||||
|
{ id: '0', title: '全部 5' } as RADIO_BUTTON,
|
||||||
|
{ id: '1', title: '派单通知 2' } as RADIO_BUTTON,
|
||||||
|
{ id: '2', title: '系统消息 3' } as RADIO_BUTTON
|
||||||
|
]
|
||||||
|
const chipId = ref('0')
|
||||||
|
/** 由字符串 id 换算分类序号,供 groups 筛选使用 */
|
||||||
|
const chip = computed(() : number => {
|
||||||
|
const i = parseInt(chipId.value)
|
||||||
|
return isNaN(i) ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
onPageShow(() => {
|
||||||
|
markMessagesRead()
|
||||||
|
})
|
||||||
|
|
||||||
|
const groups = computed(() : HrMsgGroup[] => {
|
||||||
|
if (chip.value == 1) { return [rescuerMsgGroups[0]] }
|
||||||
|
if (chip.value == 2) { return [rescuerMsgGroups[1]] }
|
||||||
|
return rescuerMsgGroups
|
||||||
|
})
|
||||||
|
|
||||||
|
function onMsgClick(m : HrMsg) : void {
|
||||||
|
if (m.action == '去接收 ›') {
|
||||||
|
navTo('/pages/event/index')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (m.title == 'BLS 证书即将到期提醒') {
|
||||||
|
navTo('/pages/study/certificates')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (m.title.indexOf('新课题') > -1) {
|
||||||
|
navTo('/pages/course/library')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showModal({ title: m.title, content: m.sub, showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReadAll() : void {
|
||||||
|
uni.showToast({ title: '已全部标记为已读', icon: 'success' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="" :xloading="true">
|
||||||
|
|
||||||
|
<!-- ============ 个人 hero ============ -->
|
||||||
|
<view class="mine-hero">
|
||||||
|
<view class="mine-c1"></view>
|
||||||
|
<view class="mine-c2"></view>
|
||||||
|
<view class="mine-row">
|
||||||
|
<view class="mine-avt">
|
||||||
|
<text class="mine-avt-t">{{ user.avatar }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="mine-name">{{ user.name }}</text>
|
||||||
|
<text class="mine-role">{{ user.title }} · {{ user.dept }} · 工号 {{ user.jobNo }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="mine-edit" @click="onEdit">
|
||||||
|
<text class="mine-edit-t">编辑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="mine-tags">
|
||||||
|
<view v-for="(t, i) in user.tags" :key="i" class="mine-tag">
|
||||||
|
<text class="mine-tag-t">{{ t }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 数据条(上浮压住 hero) ============ -->
|
||||||
|
<view class="mine-stats">
|
||||||
|
<view class="mine-stat-cell">
|
||||||
|
<text class="mine-stat-b">{{ user.hours }}</text>
|
||||||
|
<text class="mine-stat-s">累计学时</text>
|
||||||
|
</view>
|
||||||
|
<view class="mine-stat-sep"></view>
|
||||||
|
<view class="mine-stat-cell">
|
||||||
|
<text class="mine-stat-b">{{ user.courseCount }}</text>
|
||||||
|
<text class="mine-stat-s">完成课程</text>
|
||||||
|
</view>
|
||||||
|
<view class="mine-stat-sep"></view>
|
||||||
|
<view class="mine-stat-cell">
|
||||||
|
<text class="mine-stat-b">{{ user.certCount }}</text>
|
||||||
|
<text class="mine-stat-s">有效证书</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-body" style="padding-top:16px">
|
||||||
|
|
||||||
|
<!-- 救援人员端:学习档案 -->
|
||||||
|
<hr-card v-if="isRescuer" variant="list">
|
||||||
|
<hr-lrow icon="▦" theme="pri" title="学习中心" sub="错题 12 · 收藏 8 · 笔记 5" value="›"
|
||||||
|
@click="go('/pages/study/center')"></hr-lrow>
|
||||||
|
<hr-lrow icon="▤" theme="grn" title="我的证书" sub="3 张有效 · 1 张即将到期" value="›" :is-last="true"
|
||||||
|
@click="go('/pages/study/certificates')"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- 仓管人员端:仓储概览 -->
|
||||||
|
<hr-card v-if="!isRescuer" variant="list">
|
||||||
|
<hr-lrow icon="!" theme="red" title="库存预警" sub="6 项低于预警线" value="去处理 ›" value-theme="red"
|
||||||
|
:value-bold="true" @click="go('/pages/warehouse/stock')"></hr-lrow>
|
||||||
|
<hr-lrow icon="⚑" theme="amb" title="借用审批" sub="3 条待审批" value="去审批 ›" value-theme="pri"
|
||||||
|
:value-bold="true" @click="go('/pages/message/index')"></hr-lrow>
|
||||||
|
<hr-lrow icon="⚙" theme="pri" title="设备台账" sub="24 台 · 4 台借出中" value="›" :is-last="true"
|
||||||
|
@click="go('/pages/warehouse/devices')"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- 通用条目 -->
|
||||||
|
<hr-card variant="list" style="margin-top:10px">
|
||||||
|
<hr-lrow icon="✉" theme="amb" title="消息中心" :sub="msgSub" value="›"
|
||||||
|
@click="go(isRescuer ? '/pages/message/list' : '/pages/message/index')">
|
||||||
|
<template v-slot:right>
|
||||||
|
<view v-if="hrUnread.msgCenter > 0" class="hr-badge">
|
||||||
|
<text class="hr-badge-t">{{ hrUnread.msgCenter }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
<hr-lrow v-if="isRescuer" icon="☎" title="通讯录" sub="指挥员 / 医院 / 同事" value="›"
|
||||||
|
@click="go('/pages/message/contacts')"></hr-lrow>
|
||||||
|
<hr-lrow v-if="!isRescuer" icon="▦" theme="pri" title="扫码出入库" sub="连续扫码 · 三态即时反馈"
|
||||||
|
value="›" @click="go('/pages/warehouse/scan')"></hr-lrow>
|
||||||
|
<hr-lrow icon="⚙" title="设置" sub="通知 / 隐私 / 关于" value="›" :is-last="true"
|
||||||
|
@click="go('/pages/mine/settings')"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-card variant="list" style="margin-top:10px">
|
||||||
|
<hr-lrow icon="question-line" title="帮助与反馈" sub="使用手册 · 问题上报" value="›" :is-last="true"
|
||||||
|
@click="onHelp"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<view class="mine-logout" @click="onLogout">
|
||||||
|
<text class="mine-logout-t">退出登录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:tabbar>
|
||||||
|
<hr-tabbar :active="isRescuer ? 2 : 4"></hr-tabbar>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import { hrSession, logout } from '@/utils/role.uts'
|
||||||
|
import { navTo, relaunchTo } from '@/utils/nav.uts'
|
||||||
|
import { hrUnread } from '@/utils/unread.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s08 个人中心(双角色共用)
|
||||||
|
* 救援端:学习档案(学习中心 / 证书)+ 消息 + 通讯录
|
||||||
|
* 仓管端:仓储概览(库存预警 / 借用审批 / 设备台账)+ 消息 + 扫码
|
||||||
|
*/
|
||||||
|
const user = hrSession
|
||||||
|
|
||||||
|
const isRescuer = computed(() : boolean => {
|
||||||
|
return user.role != 'keeper'
|
||||||
|
})
|
||||||
|
|
||||||
|
const msgSub = computed(() : string => {
|
||||||
|
return isRescuer.value ? '派单通知 · 系统消息' : '库存预警 · 借用审批 · 系统'
|
||||||
|
})
|
||||||
|
|
||||||
|
function go(url : string) : void {
|
||||||
|
navTo(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEdit() : void {
|
||||||
|
uni.showToast({ title: '编辑个人资料', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHelp() : void {
|
||||||
|
uni.showModal({ title: '帮助与反馈', content: '使用手册 / 问题上报\n值班室电话 8801', showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLogout() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '退出登录',
|
||||||
|
content: '确认退出当前账号?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
logout()
|
||||||
|
relaunchTo('/pages/login/index')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mine-hero {
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1E40AF);
|
||||||
|
padding: 32rpx 32rpx 52rpx 32rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -80rpx;
|
||||||
|
top: -96rpx;
|
||||||
|
width: 296rpx;
|
||||||
|
height: 296rpx;
|
||||||
|
border-radius: 148rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-c2 {
|
||||||
|
position: absolute;
|
||||||
|
right: 68rpx;
|
||||||
|
bottom: -104rpx;
|
||||||
|
width: 184rpx;
|
||||||
|
height: 184rpx;
|
||||||
|
border-radius: 92rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-avt {
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.22);
|
||||||
|
border: 3rpx solid rgba(255, 255, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-avt-t {
|
||||||
|
font-size: 49.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-name {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-role {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-edit {
|
||||||
|
padding: 14rpx 24rpx 14rpx 24rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-edit-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tag {
|
||||||
|
padding: 6rpx 18rpx 6rpx 18rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.18);
|
||||||
|
margin-right: 12rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-tag-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
margin-top: -28rpx;
|
||||||
|
margin-left: 32rpx;
|
||||||
|
margin-right: 32rpx;
|
||||||
|
padding: 26rpx 8rpx 26rpx 8rpx;
|
||||||
|
box-shadow: 0 12rpx 36rpx rgba(15, 23, 42, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-stat-cell {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-stat-b {
|
||||||
|
font-size: 42.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-stat-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-stat-sep {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-logout {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mine-logout-t {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #DC2626;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="设置" :xloading="true">
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 通知 ============ -->
|
||||||
|
<hr-sec title="通知" first></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<view v-for="(s, i) in switches" :key="i" class="hr-lrow"
|
||||||
|
:class="i == switches.length - 1 ? 'hr-lrow-last' : ''">
|
||||||
|
<view class="hr-lmain">
|
||||||
|
<text class="hr-lmain-b">{{ s.title }}</text>
|
||||||
|
<text class="hr-lmain-s">{{ s.sub }}</text>
|
||||||
|
</view>
|
||||||
|
<x-switch v-model="s.on" color="#10B981" size="small"></x-switch>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 离线与存储 ============ -->
|
||||||
|
<hr-sec title="离线与存储"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow v-for="(r, i) in offlineRows" :key="i" :title="r.title" :sub="r.sub" :value="r.value"
|
||||||
|
:is-last="i == offlineRows.length - 1" @click="onRowClick(i, true)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 通用 ============ -->
|
||||||
|
<hr-sec title="通用"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow v-for="(r, i) in commonRows" :key="i" :title="r.title" :sub="r.sub" :value="r.value"
|
||||||
|
:is-last="i == commonRows.length - 1" @click="onRowClick(i, false)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:16px" theme="info" icon="ℹ"
|
||||||
|
text="P0 派单强提醒开启后,收到 P0 派单将全屏告警并强制震动,请保持开启。"></hr-tip>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { settingSwitches, offlineSettingRows, commonSettingRows } from '@/mock/index.uts'
|
||||||
|
import { HrSettingSwitch, HrSettingRow } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/** s39 设置 */
|
||||||
|
const switches = ref<HrSettingSwitch[]>(settingSwitches)
|
||||||
|
const offlineRows = ref<HrSettingRow[]>(offlineSettingRows)
|
||||||
|
const commonRows = ref<HrSettingRow[]>(commonSettingRows)
|
||||||
|
|
||||||
|
function onRowClick(i : number, isOffline : boolean) : void {
|
||||||
|
if (isOffline) {
|
||||||
|
if (i == 0) { navTo('/pages/triage/offline-sync'); return }
|
||||||
|
uni.showModal({
|
||||||
|
title: '清理已下载资料',
|
||||||
|
content: '将删除 8.6 MB 离线学习资料,无网络时需重新下载。',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { uni.showToast({ title: '已清理 8.6 MB', icon: 'success' }) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i == 0) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '清理缓存',
|
||||||
|
content: '当前缓存 24.3 MB,清理后不影响账号数据。',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { uni.showToast({ title: '已清理 24.3 MB', icon: 'success' }) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i == 1) {
|
||||||
|
uni.showModal({ title: '医院救援管理平台', content: '版本 v2.0.0\n移动端逻辑重构版', showCancel: false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showModal({ title: '隐私政策', content: '本平台严格保护伤患与医护人员的个人信息,数据仅用于院前急救调度。', showCancel: false })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="学习中心" :xloading="true" @right-click="onSort">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 四大类目(xTabs) ============ -->
|
||||||
|
<x-tabs :list="catTabs" :model-value="activeId" item-width="25%" height="84rpx" font-size="28rpx"
|
||||||
|
active-font-size="28rpx" title-color="#6B7280" active-title-color="#2563EB" line-color="#2563EB"
|
||||||
|
title-padding="0" :is-item-center="true" @change="onCatChange"></x-tabs>
|
||||||
|
|
||||||
|
<!-- ============ 课题筛选(组件库 xRadioButton 分段切换) ============ -->
|
||||||
|
<view style="margin-top:10px;margin-bottom:8rpx">
|
||||||
|
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
|
||||||
|
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
|
||||||
|
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 题目列表 ============ -->
|
||||||
|
<view v-if="cat == 0" style="margin-top:10px">
|
||||||
|
<view v-for="(q, i) in wrongList" :key="q.id" class="qa-card">
|
||||||
|
<view class="qa-top">
|
||||||
|
<text class="qa-stem">{{ q.stem }}</text>
|
||||||
|
<hr-chip :text="wrongTimes(i)" theme="red"></hr-chip>
|
||||||
|
</view>
|
||||||
|
<text class="qa-ans">正确答案 <text class="qa-ans-hl">{{ rightAnswer(q) }}</text></text>
|
||||||
|
<view class="qa-meta">
|
||||||
|
<text class="qa-meta-t">来自 <text class="qa-meta-hl">{{ q.from }}</text></text>
|
||||||
|
<text class="qa-meta-t qa-meta-date">{{ wrongDates[i] }}</text>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<view class="qa-fold" @click="toggle(i)">
|
||||||
|
<x-icon :name="opened[i] ? 'arrow-up-s-line' : 'arrow-down-s-line'" color="#2563EB"
|
||||||
|
font-size="26rpx"></x-icon>
|
||||||
|
<text class="qa-fold-t">{{ opened[i] ? '收起解析' : '查看解析' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 手风琴展开:展开进度由 accStyle() 逐帧下发 -->
|
||||||
|
<view v-if="opened[i]" :style="accStyle(i)">
|
||||||
|
<view class="qa-analysis">
|
||||||
|
<text class="qa-analysis-t">{{ q.analysis }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 其它类目占位 -->
|
||||||
|
<hr-empty v-else :icon="emptyIcon" :title="emptyTitle" :sub="emptySub" action="去课程库学习"
|
||||||
|
@action="goLibrary"></hr-empty>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { wrongQuestions } from '@/mock/index.uts'
|
||||||
|
import { HrQuestion } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { TABS_ITEM_INFO, TABS_ITEM, RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s38 学习中心
|
||||||
|
* 汇总「错题 / 收藏 / 历史 / 笔记」四类目,替代原先散落各处的入口。
|
||||||
|
* 四类目改用组件库 xTabs(原 lc-tabs 自绘分段控件已移除)。
|
||||||
|
*/
|
||||||
|
const catTabs : TABS_ITEM_INFO[] = [
|
||||||
|
{ title: '错题 12', id: '0' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '收藏 8', id: '1' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '历史', id: '2' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '笔记 5', id: '3' } as TABS_ITEM_INFO
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 课题筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||||||
|
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||||||
|
*/
|
||||||
|
const chipTabs : RADIO_BUTTON[] = [
|
||||||
|
{ id: '0', title: '全部' } as RADIO_BUTTON,
|
||||||
|
{ id: '1', title: '心肺复苏 5' } as RADIO_BUTTON,
|
||||||
|
{ id: '2', title: '气道管理 4' } as RADIO_BUTTON,
|
||||||
|
{ id: '3', title: 'AED 3' } as RADIO_BUTTON
|
||||||
|
]
|
||||||
|
const chipId = ref('0')
|
||||||
|
|
||||||
|
const wrongDates : string[] = ['09-15', '09-14', '09-12']
|
||||||
|
|
||||||
|
const activeId = ref('0')
|
||||||
|
const opened = ref<boolean[]>([false, false, false])
|
||||||
|
const wrongList = ref<HrQuestion[]>(wrongQuestions)
|
||||||
|
|
||||||
|
const cat = computed(() : number => {
|
||||||
|
const i = parseInt(activeId.value)
|
||||||
|
return isNaN(i) ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
// ⚠ x-tabs 的 change 下发的是 TABS_ITEM(不是 TABS_ITEM_INFO),形参类型必须一致
|
||||||
|
function onCatChange(item : TABS_ITEM, index : number) : void {
|
||||||
|
activeId.value = item.id == '' ? index.toString() : item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyIcon = computed(() : string => {
|
||||||
|
if (cat.value == 1) { return '★' }
|
||||||
|
if (cat.value == 2) { return '◔' }
|
||||||
|
return '✎'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptyTitle = computed(() : string => {
|
||||||
|
if (cat.value == 1) { return '暂无收藏题目' }
|
||||||
|
if (cat.value == 2) { return '暂无学习历史' }
|
||||||
|
return '暂无学习笔记'
|
||||||
|
})
|
||||||
|
|
||||||
|
const emptySub = computed(() : string => {
|
||||||
|
return '在课题中收藏题目、记录笔记后会自动汇总到这里'
|
||||||
|
})
|
||||||
|
|
||||||
|
function wrongTimes(i : number) : string {
|
||||||
|
if (i == 0) { return '错 2 次' }
|
||||||
|
return '错 1 次'
|
||||||
|
}
|
||||||
|
|
||||||
|
function rightAnswer(q : HrQuestion) : string {
|
||||||
|
if (q.answer < q.options.length) {
|
||||||
|
const o = q.options[q.answer]
|
||||||
|
return `${o.key}. ${o.text}`
|
||||||
|
}
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 手风琴展开入场动画
|
||||||
|
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
|
||||||
|
* 改为定时器逐帧推进 accT(0 → 1),再由 accStyle() 下发 opacity + translateY。
|
||||||
|
* 240ms(30ms × 8 帧),与设计稿 qa-acc-in 一致。
|
||||||
|
*/
|
||||||
|
const accIdx = ref(-1)
|
||||||
|
const accT = ref(1)
|
||||||
|
let accTimer : number = 0
|
||||||
|
|
||||||
|
function stopAcc() : void {
|
||||||
|
if (accTimer != 0) {
|
||||||
|
clearInterval(accTimer)
|
||||||
|
accTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAcc(i : number) : void {
|
||||||
|
stopAcc()
|
||||||
|
accIdx.value = i
|
||||||
|
accT.value = 0
|
||||||
|
accTimer = setInterval(() => {
|
||||||
|
const t = accT.value + 0.16
|
||||||
|
if (t >= 1) {
|
||||||
|
accT.value = 1
|
||||||
|
stopAcc()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accT.value = t
|
||||||
|
}, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅正在展开的那一项需要过渡样式,其余返回空串(即最终态) */
|
||||||
|
function accStyle(i : number) : string {
|
||||||
|
if (i != accIdx.value || accT.value >= 1) { return '' }
|
||||||
|
const t = accT.value
|
||||||
|
const y = Math.round(-16 * (1 - t))
|
||||||
|
return 'opacity:' + t.toString() + ';transform:translateY(' + y.toString() + 'rpx);'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手风琴:展开一项时其余自动收起 */
|
||||||
|
function toggle(i : number) : void {
|
||||||
|
const wasOpen = opened.value[i]
|
||||||
|
const arr : boolean[] = []
|
||||||
|
for (let k = 0; k < wrongList.value.length; k++) { arr.push(false) }
|
||||||
|
arr[i] = !wasOpen
|
||||||
|
opened.value = arr
|
||||||
|
if (wasOpen) {
|
||||||
|
stopAcc()
|
||||||
|
accIdx.value = -1
|
||||||
|
accT.value = 1
|
||||||
|
} else {
|
||||||
|
runAcc(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
stopAcc()
|
||||||
|
})
|
||||||
|
|
||||||
|
function goLibrary() : void {
|
||||||
|
navTo('/pages/course/library')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort() : void {
|
||||||
|
uni.showToast({ title: '按时间 / 次数排序', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ---------- 题目卡 ---------- */
|
||||||
|
.qa-card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-left: 8rpx solid #2563EB;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 22rpx 24rpx 22rpx 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-stem {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-ans {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-ans-hl {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #059669;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta-hl {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta-date {
|
||||||
|
margin-left: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-fold {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-fold-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 手风琴展开 ----------
|
||||||
|
⚠ App 端 uvue 不支持 @keyframes/animation,
|
||||||
|
入场过渡已改为 script 内定时器推进 accT + accStyle() 下发样式。 */
|
||||||
|
.qa-analysis {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 18rpx 20rpx 18rpx 20rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-analysis-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="我的证书" :xloading="true" @right-click="onSort">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<hr-card>
|
||||||
|
<view class="hr-row">
|
||||||
|
<view class="hr-avt hr-avt-lg hr-avt-grn ct-avt">
|
||||||
|
<text>▤</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="ct-title">有效证书 {{ certs.length }} 张</text>
|
||||||
|
<text class="ct-sub">其中 1 张将于 2026-10-15 到期</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-sec title="证书列表"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow v-for="(c, i) in certs" :key="i" :icon="c.statusTheme == 'grn' ? '▤' : '!'"
|
||||||
|
:theme="c.statusTheme == 'grn' ? 'grn' : 'amb'" :title="c.name"
|
||||||
|
:sub="c.gotAt + ' 取得 · ' + (c.statusTheme == 'grn' ? '有效至 ' + c.expireAt : c.expireAt + ' 到期')"
|
||||||
|
:value="c.status + ' ›'" :value-theme="c.statusTheme" :value-bold="true"
|
||||||
|
:is-last="i == certs.length - 1" @click="onCertClick(c)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:14px" theme="warn" icon="⚑"
|
||||||
|
text="BLS 证书将在 22 天后到期,请完成年度复训后自动续期。"></hr-tip>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { certList } from '@/mock/index.uts'
|
||||||
|
import { HrCert } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/** s13 我的证书 */
|
||||||
|
const certs = ref<HrCert[]>(certList)
|
||||||
|
|
||||||
|
function onCertClick(c : HrCert) : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: c.name,
|
||||||
|
content: `取得时间:${c.gotAt}\n有效期至:${c.expireAt}\n状态:${c.status}`,
|
||||||
|
cancelText: '关闭',
|
||||||
|
confirmText: '查看证书',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showToast({ title: '已生成证书图片', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort() : void {
|
||||||
|
uni.showToast({ title: '按取得时间排序', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ct-avt {
|
||||||
|
margin-right: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-title {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ct-sub {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="我的考试" :xloading="true" @right-click="onSort">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<view class="hr-seg">
|
||||||
|
<view v-for="(s, i) in segs" :key="i" :class="seg == i ? 'hr-seg-i hr-seg-i-on' : 'hr-seg-i'"
|
||||||
|
@click="seg = i">
|
||||||
|
<text :class="seg == i ? 'ex-seg-t ex-seg-t-on' : 'ex-seg-t'">{{ s }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 待考(P0 紧急) ============ -->
|
||||||
|
<view v-if="seg != 2 && plans.length > 0" class="ex-plan">
|
||||||
|
<view class="hr-row-top">
|
||||||
|
<view class="ex-plan-ic">
|
||||||
|
<text class="ex-plan-ic-t">!</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<view class="hr-row-between">
|
||||||
|
<text class="ex-plan-t">{{ plans[0].name }}</text>
|
||||||
|
<hr-pc :level="plans[0].priority"></hr-pc>
|
||||||
|
</view>
|
||||||
|
<text class="ex-plan-cd">{{ plans[0].countdown }}</text>
|
||||||
|
<text class="ex-plan-meta">{{ plans[0].time }} · {{ plans[0].place }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ex-plan-btn" @click="onPrepare">
|
||||||
|
<text class="ex-plan-btn-t">进入备考</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 已完成 ============ -->
|
||||||
|
<hr-card v-if="seg != 1" variant="list" style="margin-top:10px">
|
||||||
|
<hr-lrow v-for="(r, i) in shownRecords" :key="i" :icon="r.passed ? '✓' : '✗'"
|
||||||
|
:theme="r.passed ? 'grn' : 'red'" :title="r.name"
|
||||||
|
:sub="r.date + ' · 得分 ' + r.score + ' · ' + (r.passed ? '已通过' : '未通过')"
|
||||||
|
:value="r.score.toString() + ' ›'" :value-theme="r.passed ? 'grn' : 'red'" :value-bold="true"
|
||||||
|
:is-last="i == shownRecords.length - 1" @click="onRecordClick(r)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-empty v-if="seg == 1 && plans.length == 0" icon="✓" title="暂无待考安排"
|
||||||
|
sub="有新的考试安排时会在消息中心提醒你"></hr-empty>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:14px" theme="info" icon="ℹ"
|
||||||
|
text="理论测验通过后自动计入课题完成度;未通过的测验可在 7 天后重新参加。"></hr-tip>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { examPlans, examRecords } from '@/mock/index.uts'
|
||||||
|
import { HrExamRecord, HrExamPlan } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/** s12 我的考试 */
|
||||||
|
const segs : string[] = ['全部', '待考', '已完成']
|
||||||
|
const seg = ref(0)
|
||||||
|
const plans = ref<HrExamPlan[]>(examPlans)
|
||||||
|
const records = ref<HrExamRecord[]>(examRecords)
|
||||||
|
const passOnly = ref(false)
|
||||||
|
|
||||||
|
const shownRecords = computed(() : HrExamRecord[] => {
|
||||||
|
if (passOnly.value) {
|
||||||
|
const list : HrExamRecord[] = []
|
||||||
|
for (let i = 0; i < records.value.length; i++) {
|
||||||
|
if (records.value[i].passed) { list.push(records.value[i]) }
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
return records.value
|
||||||
|
})
|
||||||
|
|
||||||
|
function onPrepare() : void {
|
||||||
|
navTo('/pages/course/practice')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRecordClick(r : HrExamRecord) : void {
|
||||||
|
if (r.passed) {
|
||||||
|
uni.showModal({
|
||||||
|
title: r.name,
|
||||||
|
content: `得分 ${r.score} 分 · ${r.date}\n可查看错题解析与答卷详情。`,
|
||||||
|
cancelText: '关闭',
|
||||||
|
confirmText: '查看错题',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { navTo('/pages/study/wrong-book') }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showModal({
|
||||||
|
title: r.name,
|
||||||
|
content: `得分 ${r.score} 分 · 未通过\n补考开放时间:${r.date} 起 7 天后`,
|
||||||
|
cancelText: '关闭',
|
||||||
|
confirmText: '进入复习',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { navTo('/pages/course/practice') }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort() : void {
|
||||||
|
uni.showToast({ title: '按时间排序', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.ex-seg-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-seg-t-on {
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
background-color: #FFF5F5;
|
||||||
|
border: 2rpx solid #FECACA;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 24rpx 26rpx 24rpx 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-ic {
|
||||||
|
width: 68rpx;
|
||||||
|
height: 68rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
background-color: #DC2626;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-ic-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-t {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-cd {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #DC2626;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-meta {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-btn {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background-color: #DC2626;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ex-plan-btn-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="错题本" :xloading="true" @right-click="onSort">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- 课题筛选(组件库 xRadioButton 分段切换) -->
|
||||||
|
<view style="margin-bottom:8rpx">
|
||||||
|
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
|
||||||
|
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
|
||||||
|
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 错题卡片 -->
|
||||||
|
<view style="margin-top:10px">
|
||||||
|
<view v-for="(q, i) in list" :key="q.id" class="qa-card">
|
||||||
|
<view class="qa-top">
|
||||||
|
<text class="qa-stem">{{ q.stem }}</text>
|
||||||
|
<hr-chip :text="wrongTimes[i]" theme="red"></hr-chip>
|
||||||
|
</view>
|
||||||
|
<view class="qa-row">
|
||||||
|
<text class="qa-k">你的答案</text>
|
||||||
|
<text class="qa-wrong">{{ wrongKeys[i] }}</text>
|
||||||
|
<text class="qa-k qa-k2">正确答案</text>
|
||||||
|
<text class="qa-right">{{ rightAnswer(q) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="qa-meta">
|
||||||
|
<text class="qa-meta-t">来自 <text class="qa-meta-hl">{{ q.from }}</text></text>
|
||||||
|
<text class="qa-meta-t qa-meta-date">{{ dates[i] }} 答错</text>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<view class="qa-fold" @click="toggle(i)">
|
||||||
|
<x-icon :name="opened[i] ? 'arrow-up-s-line' : 'arrow-down-s-line'" color="#2563EB"
|
||||||
|
font-size="26rpx"></x-icon>
|
||||||
|
<text class="qa-fold-t">{{ opened[i] ? '收起解析' : '查看解析' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 手风琴展开:展开进度由 accStyle() 逐帧下发 -->
|
||||||
|
<view v-if="opened[i]" :style="accStyle(i)">
|
||||||
|
<view class="qa-analysis">
|
||||||
|
<text class="qa-analysis-t">{{ q.analysis }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-tip theme="info" icon="ℹ" text="已复习的错题不会自动移除;连续答对 2 次后可手动移出错题本。"></hr-tip>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { wrongQuestions } from '@/mock/index.uts'
|
||||||
|
import { HrQuestion } from '@/mock/types.uts'
|
||||||
|
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/** s16 错题本 */
|
||||||
|
/**
|
||||||
|
* 课题筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||||||
|
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||||||
|
*/
|
||||||
|
const chipTabs : RADIO_BUTTON[] = [
|
||||||
|
{ id: '0', title: '全部 12' } as RADIO_BUTTON,
|
||||||
|
{ id: '1', title: '心肺复苏 5' } as RADIO_BUTTON,
|
||||||
|
{ id: '2', title: '气道管理 4' } as RADIO_BUTTON,
|
||||||
|
{ id: '3', title: 'AED 3' } as RADIO_BUTTON
|
||||||
|
]
|
||||||
|
const chipId = ref('0')
|
||||||
|
|
||||||
|
const wrongTimes : string[] = ['答错 2 次', '答错 1 次', '答错 1 次']
|
||||||
|
const dates : string[] = ['09-15', '09-14', '09-12']
|
||||||
|
const wrongKeys : string[] = ['A. 3~4 cm', 'C. 60~80 次', 'D. 每 6 秒 1 次']
|
||||||
|
|
||||||
|
const opened = ref<boolean[]>([false, false, false])
|
||||||
|
const all = ref<HrQuestion[]>(wrongQuestions)
|
||||||
|
|
||||||
|
const list = computed(() : HrQuestion[] => {
|
||||||
|
return all.value
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 选项文案里若已含「A. 」前缀则去掉,避免出现「A. A. xxx」 */
|
||||||
|
function stripKey(text : string) : string {
|
||||||
|
const i = text.indexOf('. ')
|
||||||
|
if (i == 1 || i == 2) { return text.substring(i + 2) }
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠ UTS 不提升函数声明:stripKey 必须写在 rightAnswer 之前
|
||||||
|
function rightAnswer(q : HrQuestion) : string {
|
||||||
|
if (q.answer < q.options.length) {
|
||||||
|
const o = q.options[q.answer]
|
||||||
|
return `${o.key}. ${stripKey(o.text)}`
|
||||||
|
}
|
||||||
|
return '—'
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 手风琴展开入场动画
|
||||||
|
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
|
||||||
|
* 改为定时器逐帧推进 accT(0 → 1),再由 accStyle() 下发 opacity + translateY。
|
||||||
|
* 240ms(30ms × 8 帧),与设计稿 qa-acc-in 一致。
|
||||||
|
*/
|
||||||
|
const accIdx = ref(-1)
|
||||||
|
const accT = ref(1)
|
||||||
|
let accTimer : number = 0
|
||||||
|
|
||||||
|
function stopAcc() : void {
|
||||||
|
if (accTimer != 0) {
|
||||||
|
clearInterval(accTimer)
|
||||||
|
accTimer = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runAcc(i : number) : void {
|
||||||
|
stopAcc()
|
||||||
|
accIdx.value = i
|
||||||
|
accT.value = 0
|
||||||
|
accTimer = setInterval(() => {
|
||||||
|
const t = accT.value + 0.16
|
||||||
|
if (t >= 1) {
|
||||||
|
accT.value = 1
|
||||||
|
stopAcc()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
accT.value = t
|
||||||
|
}, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅正在展开的那一项需要过渡样式,其余返回空串(即最终态) */
|
||||||
|
function accStyle(i : number) : string {
|
||||||
|
if (i != accIdx.value || accT.value >= 1) { return '' }
|
||||||
|
const t = accT.value
|
||||||
|
const y = Math.round(-16 * (1 - t))
|
||||||
|
return 'opacity:' + t.toString() + ';transform:translateY(' + y.toString() + 'rpx);'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手风琴:先全部收起,再切换当前项(同时只展开一条) */
|
||||||
|
function toggle(i : number) : void {
|
||||||
|
const wasOpen = opened.value[i]
|
||||||
|
const arr : boolean[] = []
|
||||||
|
for (let k = 0; k < list.value.length; k++) { arr.push(false) }
|
||||||
|
arr[i] = !wasOpen
|
||||||
|
opened.value = arr
|
||||||
|
if (wasOpen) {
|
||||||
|
stopAcc()
|
||||||
|
accIdx.value = -1
|
||||||
|
accT.value = 1
|
||||||
|
} else {
|
||||||
|
runAcc(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnload(() => {
|
||||||
|
stopAcc()
|
||||||
|
})
|
||||||
|
|
||||||
|
function onSort() : void {
|
||||||
|
uni.showToast({ title: '按错误次数 / 时间排序', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.qa-card {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-left: 8rpx solid #2563EB;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 22rpx 24rpx 22rpx 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-stem {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-k {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-k2 {
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-wrong {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #DC2626;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-right {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #059669;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta-hl {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-meta-date {
|
||||||
|
margin-left: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-fold {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-fold-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 手风琴展开 ----------
|
||||||
|
⚠ App 端 uvue 不支持 @keyframes/animation,
|
||||||
|
入场过渡已改为 script 内定时器推进 accT + accStyle() 下发样式。 */
|
||||||
|
.qa-analysis {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 18rpx 20rpx 18rpx 20rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qa-analysis-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="批量转运" @right-click="onHistory">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="op-nav-act">历史</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="op-page">
|
||||||
|
|
||||||
|
<!-- ① 顶部:连续扫码 -->
|
||||||
|
<hr-scan-cam :count="list.length" @scan="onScan" @manual="onManual"></hr-scan-cam>
|
||||||
|
|
||||||
|
<!-- ② 中部:伤员记录(唯一滚动区;死亡伤员不参与转运,故未列入) -->
|
||||||
|
<hr-rec-list :list="list" @remove="onRemove" @clear="onClear"></hr-rec-list>
|
||||||
|
|
||||||
|
<!-- ③ 底部:转运表单 + 右下角悬浮提交 -->
|
||||||
|
<view class="op-form">
|
||||||
|
<text class="op-lb">接收医院</text>
|
||||||
|
<view class="op-pick" @click="openHos">
|
||||||
|
<text class="op-pick-n">{{ hospitalList[hosIdx] }}</text>
|
||||||
|
<text :class="hospitalAvail[hosIdx] ? 'op-pick-b' : 'op-pick-b op-pick-b-no'">{{ hospitalBed[hosIdx] }}</text>
|
||||||
|
<x-icon name="arrow-down-s-line" color="#9CA3AF" font-size="14"></x-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="op-lb op-lb-gap">转运方式</text>
|
||||||
|
<view class="hr-seg">
|
||||||
|
<view v-for="(m, i) in modes" :key="i"
|
||||||
|
:class="i == modeIdx ? 'hr-seg-i hr-seg-i-on' : 'hr-seg-i'" @click="modeIdx = i">
|
||||||
|
<text :class="i == modeIdx ? 'op-seg-t op-seg-t-on' : 'op-seg-t'">{{ m }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="op-lb op-lb-gap">随车联系人</text>
|
||||||
|
<view class="op-foot">
|
||||||
|
<view class="op-foot-col">
|
||||||
|
<view class="hr-field">
|
||||||
|
<text class="hr-field-label">姓名</text>
|
||||||
|
<input class="hr-field-input" type="text" v-model="contactName" placeholder-style="color:#9CA3AF" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="op-foot-col op-foot-col-last">
|
||||||
|
<view class="hr-field">
|
||||||
|
<text class="hr-field-label">联系电话</text>
|
||||||
|
<input class="hr-field-input" type="number" v-model="contactPhone" placeholder-style="color:#9CA3AF" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="op-send" @click="onSubmit">
|
||||||
|
<x-icon name="check-line" color="#FFFFFF" font-size="24"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 接收医院选择:hr-sheet 底部弹层(点击上方「接收医院」整行唤起) -->
|
||||||
|
<template v-slot:overlay>
|
||||||
|
<view v-if="hosSheet" class="hr-mask" @click="closeHos">
|
||||||
|
<view class="hr-sheet" @click="stop">
|
||||||
|
<view class="hr-sheet-h">
|
||||||
|
<text class="hr-sheet-h-b">选择接收医院</text>
|
||||||
|
<view class="hr-sheet-h-x" @click="closeHos">
|
||||||
|
<x-icon name="close-line" color="#9CA3AF" font-size="18"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-for="(h, i) in hospitalList" :key="i" :class="hosOptClass(i)" @click="pickHospital(i)">
|
||||||
|
<view class="os-txt">
|
||||||
|
<text :class="hosNameClass(i)">{{ h }}</text>
|
||||||
|
<text class="os-meta">{{ hospitalMeta[i] }}</text>
|
||||||
|
</view>
|
||||||
|
<text :class="hospitalAvail[i] ? 'op-pick-b' : 'op-pick-b op-pick-b-no'">{{ hospitalBed[i] }}</text>
|
||||||
|
<x-icon v-if="i == hosIdx" name="check-line" color="#2563EB" font-size="17"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { scanVictims, hospitalList, hospitalMeta, hospitalBed, hospitalAvail } from '@/mock/index.uts'
|
||||||
|
import { HrVictim } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s44 批量转运(扫腕带 · 三段式作业页)
|
||||||
|
* 与批量处置同构:顶部相机(点击开启)/ 中部记录列表 / 底部表单;
|
||||||
|
* 底部换成 接收医院(hr-sheet 弹层选择)/ 转运方式 / 随车联系人。
|
||||||
|
*/
|
||||||
|
const modes : string[] = ['救护车 🚑', '直升机 🚁']
|
||||||
|
const modeIdx = ref(0)
|
||||||
|
const hosIdx = ref(0)
|
||||||
|
const hosSheet = ref(false)
|
||||||
|
const contactName = ref('刘伟')
|
||||||
|
const contactPhone = ref('13800006621')
|
||||||
|
|
||||||
|
const list = ref<HrVictim[]>([
|
||||||
|
scanVictims[0], scanVictims[1], scanVictims[2], scanVictims[3]
|
||||||
|
])
|
||||||
|
|
||||||
|
/* ⚠ UTS 不提升函数声明,被调用的函数一律写在调用点之前 */
|
||||||
|
|
||||||
|
/** 把新伤员追加进列表(uvue 原地改数组不刷新,必须重建数组) */
|
||||||
|
function push(v : HrVictim) : void {
|
||||||
|
const arr : HrVictim[] = []
|
||||||
|
for (let i = 0; i < list.value.length; i++) { arr.push(list.value[i]) }
|
||||||
|
arr.push(v)
|
||||||
|
list.value = arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function addByCode(code : string) : void {
|
||||||
|
for (let i = 0; i < list.value.length; i++) {
|
||||||
|
if (list.value[i].code == code) {
|
||||||
|
uni.showToast({ title: '该伤员已在列表中', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < scanVictims.length; i++) {
|
||||||
|
const v = scanVictims[i]
|
||||||
|
if (v.code == code) {
|
||||||
|
if (v.level == 4) {
|
||||||
|
uni.showToast({ title: '死亡伤员不参与转运', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
push(v)
|
||||||
|
uni.showToast({ title: '已加入 ' + v.name, icon: 'success' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push({
|
||||||
|
id: 'tmp_' + code, name: '未登记伤员', gender: '未知', age: 0, code: code, level: 3,
|
||||||
|
injury: '待现场检伤补录', status: '', statusTheme: '', createdAt: ''
|
||||||
|
} as HrVictim)
|
||||||
|
uni.showToast({ title: '未登记腕带,已临时加入', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScan(code : string) : void {
|
||||||
|
addByCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemove(i : number) : void {
|
||||||
|
const arr : HrVictim[] = []
|
||||||
|
for (let k = 0; k < list.value.length; k++) {
|
||||||
|
if (k != i) { arr.push(list.value[k]) }
|
||||||
|
}
|
||||||
|
list.value = arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '清空列表',
|
||||||
|
content: '将移除当前已识别的全部伤员记录,确认清空?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { list.value = [] as HrVictim[] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onManual() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '手动输入腕带编号',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: '如 TRIAGE-20260922-0006',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) { return }
|
||||||
|
const code = res.content
|
||||||
|
if (code == null || code == '') { return }
|
||||||
|
addByCode(code as string)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 接收医院:hr-sheet 底部弹层 ---------- */
|
||||||
|
|
||||||
|
function openHos() : void {
|
||||||
|
hosSheet.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeHos() : void {
|
||||||
|
hosSheet.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 点击弹层面板本身时阻止冒泡,避免误关闭 */
|
||||||
|
function stop() : void {
|
||||||
|
}
|
||||||
|
|
||||||
|
function hosOptClass(i : number) : string {
|
||||||
|
if (i == hospitalList.length - 1) { return 'hr-sheet-opt hr-sheet-opt-last' }
|
||||||
|
return 'hr-sheet-opt'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 弹层选项:医院名(两行式第一行,选中蓝 / 满员置灰) */
|
||||||
|
function hosNameClass(i : number) : string {
|
||||||
|
if (!hospitalAvail[i]) { return 'os-name os-name-dim' }
|
||||||
|
if (i == hosIdx.value) { return 'os-name os-name-on' }
|
||||||
|
return 'os-name'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickHospital(i : number) : void {
|
||||||
|
if (!hospitalAvail[i]) {
|
||||||
|
uni.showToast({ title: '该医院急诊已满员,不可选择', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hosIdx.value = i
|
||||||
|
hosSheet.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() : void {
|
||||||
|
if (list.value.length == 0) {
|
||||||
|
uni.showToast({ title: '请先扫码添加伤员', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (contactName.value == '' || contactPhone.value == '') {
|
||||||
|
uni.showToast({ title: '请填写随车联系人', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '提交中' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '已转运登记 ' + list.value.length + ' 人', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
navTo('/pages/triage/transfer')
|
||||||
|
}, 700)
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHistory() : void {
|
||||||
|
navTo('/pages/triage/treat-records')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.op-page {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-nav-act {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-form {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-top: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 32rpx 32rpx 0rpx 0rpx;
|
||||||
|
padding: 22rpx 32rpx 28rpx 32rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-lb {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-lb-gap {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 接收医院:折叠字段(点击整行唤起 hr-sheet 弹层) */
|
||||||
|
.op-pick {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 20rpx 24rpx 20rpx 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-pick-n {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-pick-b {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #059669;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-pick-b-no {
|
||||||
|
color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹层选项:左侧「医院名 + 距离/预计时间」两行 */
|
||||||
|
.os-txt {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-name {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-name-on {
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-name-dim {
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-meta {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-seg-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-seg-t-on {
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-foot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-right: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-foot-col {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-foot-col-last {
|
||||||
|
margin-right: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-send {
|
||||||
|
position: absolute;
|
||||||
|
right: 32rpx;
|
||||||
|
bottom: 32rpx;
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 56rpx;
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1D4ED8);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 12rpx 36rpx rgba(37, 99, 235, 0.42);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="批量处置" @right-click="onHistory">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="op-nav-act">历史</text>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="op-page">
|
||||||
|
|
||||||
|
<!-- ① 顶部:连续扫码 -->
|
||||||
|
<hr-scan-cam :count="list.length" @scan="onScan" @manual="onManual"></hr-scan-cam>
|
||||||
|
|
||||||
|
<!-- ② 中部:伤员记录(唯一滚动区) -->
|
||||||
|
<hr-rec-list :list="list" @remove="onRemove" @clear="onClear"></hr-rec-list>
|
||||||
|
|
||||||
|
<!-- ③ 底部:处置表单 + 右下角悬浮提交 -->
|
||||||
|
<view class="op-form">
|
||||||
|
<text class="op-lb">处置类型</text>
|
||||||
|
<view class="op-chips">
|
||||||
|
<view v-for="(t, i) in types" :key="i" :class="i == typeIdx ? 'op-chip op-chip-on' : 'op-chip'"
|
||||||
|
@click="typeIdx = i">
|
||||||
|
<text :class="i == typeIdx ? 'op-chip-t op-chip-t-on' : 'op-chip-t'">{{ t }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="op-lb op-lb-gap">处置内容</text>
|
||||||
|
<textarea class="op-ta" v-model="content" placeholder="记录处置措施、用药与伤情变化…"
|
||||||
|
placeholder-style="color:#9CA3AF" />
|
||||||
|
<text class="op-note">提交后同时写入 {{ list.length }} 名伤员的处置记录</text>
|
||||||
|
|
||||||
|
<view class="op-send" @click="onSubmit">
|
||||||
|
<x-icon name="check-line" color="#FFFFFF" font-size="24"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { scanVictims, treatTypes } from '@/mock/index.uts'
|
||||||
|
import { HrVictim } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s43 批量处置(扫腕带 · 三段式作业页)
|
||||||
|
* 顶部相机(点击取景区才开启)→ 中部伤员记录滚动列表 → 底部处置表单 + 右下角悬浮提交。
|
||||||
|
*/
|
||||||
|
const types : string[] = treatTypes
|
||||||
|
const typeIdx = ref(0)
|
||||||
|
const content = ref('现场统一止血包扎,右前臂夹板固定,建立静脉通路并给氧。')
|
||||||
|
|
||||||
|
const list = ref<HrVictim[]>([
|
||||||
|
scanVictims[0], scanVictims[1], scanVictims[2], scanVictims[3], scanVictims[4]
|
||||||
|
])
|
||||||
|
|
||||||
|
/* ⚠ UTS 不提升函数声明,被调用的函数一律写在调用点之前 */
|
||||||
|
|
||||||
|
/** 把新伤员追加进列表(uvue 原地改数组不刷新,必须重建数组) */
|
||||||
|
function push(v : HrVictim) : void {
|
||||||
|
const arr : HrVictim[] = []
|
||||||
|
for (let i = 0; i < list.value.length; i++) { arr.push(list.value[i]) }
|
||||||
|
arr.push(v)
|
||||||
|
list.value = arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function addByCode(code : string) : void {
|
||||||
|
for (let i = 0; i < list.value.length; i++) {
|
||||||
|
if (list.value[i].code == code) {
|
||||||
|
uni.showToast({ title: '该伤员已在列表中', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i = 0; i < scanVictims.length; i++) {
|
||||||
|
if (scanVictims[i].code == code) {
|
||||||
|
push(scanVictims[i])
|
||||||
|
uni.showToast({ title: '已加入 ' + scanVictims[i].name, icon: 'success' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 未登记腕带:兜底创建临时记录
|
||||||
|
push({
|
||||||
|
id: 'tmp_' + code, name: '未登记伤员', gender: '未知', age: 0, code: code, level: 3,
|
||||||
|
injury: '待现场检伤补录', status: '', statusTheme: '', createdAt: ''
|
||||||
|
} as HrVictim)
|
||||||
|
uni.showToast({ title: '未登记腕带,已临时加入', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫码结果处理:按腕带编码匹配伤员并加入列表 */
|
||||||
|
function onScan(code : string) : void {
|
||||||
|
addByCode(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemove(i : number) : void {
|
||||||
|
const arr : HrVictim[] = []
|
||||||
|
for (let k = 0; k < list.value.length; k++) {
|
||||||
|
if (k != i) { arr.push(list.value[k]) }
|
||||||
|
}
|
||||||
|
list.value = arr
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClear() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '清空列表',
|
||||||
|
content: '将移除当前已识别的全部伤员记录,确认清空?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { list.value = [] as HrVictim[] }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onManual() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '手动输入腕带编号',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: '如 TRIAGE-20260922-0006',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) { return }
|
||||||
|
const code = res.content
|
||||||
|
if (code == null || code == '') { return }
|
||||||
|
addByCode(code as string)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() : void {
|
||||||
|
if (list.value.length == 0) {
|
||||||
|
uni.showToast({ title: '请先扫码添加伤员', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (content.value == '') {
|
||||||
|
uni.showToast({ title: '请填写处置内容', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '提交中' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '已写入 ' + list.value.length + ' 名伤员', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
navTo('/pages/triage/treat-records')
|
||||||
|
}, 700)
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHistory() : void {
|
||||||
|
navTo('/pages/triage/treat-records')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.op-page {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-nav-act {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 底部表单 ---------- */
|
||||||
|
.op-form {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-top: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 32rpx 32rpx 0rpx 0rpx;
|
||||||
|
padding: 22rpx 32rpx 28rpx 32rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-lb {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-lb-gap {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-chip {
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 0rpx 26rpx 0rpx 26rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 14rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-chip-on {
|
||||||
|
background-color: #2563EB;
|
||||||
|
border: 2rpx solid #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-chip-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-chip-t-on {
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-ta {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
padding: 20rpx 20rpx 20rpx 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
height: 192rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-note {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
padding-right: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-send {
|
||||||
|
position: absolute;
|
||||||
|
right: 32rpx;
|
||||||
|
bottom: 32rpx;
|
||||||
|
width: 112rpx;
|
||||||
|
height: 112rpx;
|
||||||
|
border-radius: 56rpx;
|
||||||
|
background-image: linear-gradient(to bottom right, #2563EB, #1D4ED8);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 12rpx 36rpx rgba(37, 99, 235, 0.42);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,693 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-page" style="background-color:#F3F4F6">
|
||||||
|
|
||||||
|
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
|
||||||
|
|
||||||
|
<!-- ============ 顶部伤情头信息 ============ -->
|
||||||
|
<view class="dh">
|
||||||
|
<view class="dh-c1"></view>
|
||||||
|
<view class="dh-top">
|
||||||
|
<view class="dh-back" @click="onBack">
|
||||||
|
<x-icon name="arrow-left-s-line" color="#FFFFFF" font-size="20"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="dh-title">伤员详情</text>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<text class="dh-state">⌖ 转运中</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="dh-w">{{ victim.code }}</text>
|
||||||
|
|
||||||
|
<view class="dh-n">
|
||||||
|
<text class="dh-name">{{ victim.name }}</text>
|
||||||
|
<text class="dh-age">{{ victim.gender }} · {{ victim.age }} 岁</text>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<view class="dh-level">
|
||||||
|
<text class="dh-level-t">{{ levelLabel }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="dh-ch">
|
||||||
|
<view class="dh-chip">
|
||||||
|
<text class="dh-chip-t">⚑ 事件 EVT-20260922-014</text>
|
||||||
|
</view>
|
||||||
|
<view class="dh-chip">
|
||||||
|
<text class="dh-chip-t">⌖ 转运中</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<text class="dh-foot">建档 14:23 · 操作人 张明 · 现场检伤组 3 组</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 快捷操作 ============ -->
|
||||||
|
<view class="dh-acts">
|
||||||
|
<view v-for="(a, i) in acts" :key="i" :class="i == 0 ? 'dha' : 'dha dha-sep'" @click="onAct(i)">
|
||||||
|
<view class="dha-ic" :style="{ backgroundColor: a.bg }">
|
||||||
|
<x-icon :name="a.icon" :color="a.color" font-size="32rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
<text class="dha-t" :style="{ color: a.color }">{{ a.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 分页 Tab(基本信息 / 伤情 / 转运 / 处置) ============ -->
|
||||||
|
<x-tabs :list="tabList" :model-value="activeId" item-width="25%" height="84rpx" font-size="27rpx"
|
||||||
|
active-font-size="27rpx" title-color="#6B7280" active-title-color="#2563EB" line-color="#2563EB"
|
||||||
|
title-padding="0" :is-item-center="true" @change="onTabChange"></x-tabs>
|
||||||
|
|
||||||
|
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ---------- 基本信息 ---------- -->
|
||||||
|
<view v-if="tab == 0">
|
||||||
|
<hr-sec title="基本信息" more="编辑 ›" :first="true"></hr-sec>
|
||||||
|
<view class="ig">
|
||||||
|
<view class="ig-row">
|
||||||
|
<view class="ig-it">
|
||||||
|
<text class="ig-l">身份证号</text>
|
||||||
|
<text class="ig-b">4406041991••••1234</text>
|
||||||
|
</view>
|
||||||
|
<view class="ig-it ig-it-r">
|
||||||
|
<text class="ig-l">联系电话</text>
|
||||||
|
<text class="ig-b">138••••6621</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ig-row">
|
||||||
|
<view class="ig-it">
|
||||||
|
<text class="ig-l">送诊时间</text>
|
||||||
|
<text class="ig-b">2026-09-22 14:35</text>
|
||||||
|
</view>
|
||||||
|
<view class="ig-it ig-it-r">
|
||||||
|
<text class="ig-l">事件来源</text>
|
||||||
|
<text class="ig-b">建筑坍塌 · 现场检伤</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ig-row ig-row-last">
|
||||||
|
<view class="ig-it">
|
||||||
|
<text class="ig-l">检伤分级</text>
|
||||||
|
<text class="ig-b" style="color:#B45309">{{ levelLabel }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="ig-it ig-it-r">
|
||||||
|
<text class="ig-l">检伤状态</text>
|
||||||
|
<text class="ig-b">已检伤 · 转运中</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-sec title="生命体征(最近一次 14:35)" more="趋势 ›"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<view class="vs5">
|
||||||
|
<view v-for="(v, i) in vitals" :key="i"
|
||||||
|
:class="v.abnormal ? 'vc vc-warn' : 'vc'">
|
||||||
|
<text :class="v.abnormal ? 'vc-b vc-b-warn' : 'vc-b'">{{ v.value }}</text>
|
||||||
|
<text :class="v.abnormal ? 'vc-s vc-s-warn' : 'vc-s'">{{ v.unit }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="dh-warn">
|
||||||
|
<x-icon name="alert-line" color="#DC2626" font-size="14"></x-icon>
|
||||||
|
<text class="dh-warn-t">脉搏偏快 · 血压偏低 · 血氧 88% 低于阈值,建议优先处置</text>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ---------- 伤情 ---------- -->
|
||||||
|
<view v-if="tab == 1">
|
||||||
|
<hr-sec title="伤情标记" more="查看人体图 ›" :first="true"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<view class="dh-mks">
|
||||||
|
<view v-for="(m, i) in marks" :key="i" class="dh-mk">
|
||||||
|
<view class="dh-mk-no" :style="{ backgroundColor: m.theme == 'red' ? '#DC2626' : '#F59E0B' }">
|
||||||
|
<text class="dh-mk-no-t">{{ m.no }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="dh-mk-t">{{ m.title }}</text>
|
||||||
|
<text class="dh-mk-s">{{ m.sub }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="dh-mk-time">14:2{{ i + 3 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-sec title="多模态记录" more="+ 添加 ›"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<text class="dh-desc">{{ victim.injury }}。14:23 于坍塌现场 B 区发现,俯卧位,呼之可睁眼。移除压覆物后前臂出现活动性出血,已加压包扎。</text>
|
||||||
|
<view class="dh-upls">
|
||||||
|
<view class="dh-upl">
|
||||||
|
<x-icon name="image-line" color="#9CA3AF" font-size="19"></x-icon>
|
||||||
|
<text class="dh-upl-t">现场照片</text>
|
||||||
|
<view class="dh-upl-tag">
|
||||||
|
<text class="dh-upl-tag-t">图片 · 1.2 MB</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="dh-upl">
|
||||||
|
<x-icon name="music-2-line" color="#9CA3AF" font-size="19"></x-icon>
|
||||||
|
<text class="dh-upl-t">现场录音</text>
|
||||||
|
<view class="dh-upl-tag">
|
||||||
|
<text class="dh-upl-tag-t">音频 · 0:42</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="dh-upl">
|
||||||
|
<x-icon name="video-line" color="#9CA3AF" font-size="19"></x-icon>
|
||||||
|
<text class="dh-upl-t">处置视频</text>
|
||||||
|
<view class="dh-upl-tag">
|
||||||
|
<text class="dh-upl-tag-t">视频 · 0:18</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ---------- 转运 ---------- -->
|
||||||
|
<view v-if="tab == 2">
|
||||||
|
<hr-sec title="转运信息" more="重新登记 ›" :first="true" @more-click="goTransfer"></hr-sec>
|
||||||
|
<view class="ig">
|
||||||
|
<view class="ig-row">
|
||||||
|
<view class="ig-it">
|
||||||
|
<text class="ig-l">接收医院</text>
|
||||||
|
<text class="ig-b">协和医院 · 急诊创伤中心</text>
|
||||||
|
</view>
|
||||||
|
<view class="ig-it ig-it-r">
|
||||||
|
<text class="ig-l">转运方式</text>
|
||||||
|
<text class="ig-b">救护车 🚑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="ig-row ig-row-last">
|
||||||
|
<view class="ig-it">
|
||||||
|
<text class="ig-l">转出方</text>
|
||||||
|
<text class="ig-b">张明(急救中心)</text>
|
||||||
|
</view>
|
||||||
|
<view class="ig-it ig-it-r">
|
||||||
|
<text class="ig-l">接收方</text>
|
||||||
|
<text class="ig-b">刘伟(急诊创伤中心)</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<hr-sec title="流转时间线" more="全部 4 条"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<hr-timeline :nodes="timeline"></hr-timeline>
|
||||||
|
</hr-card>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ---------- 处置 ---------- -->
|
||||||
|
<view v-if="tab == 3">
|
||||||
|
<hr-sec title="处置记录" more="全部记录 ›" :first="true" @more-click="goRecords"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<hr-timeline :nodes="treatNodes"></hr-timeline>
|
||||||
|
</hr-card>
|
||||||
|
<view class="hr-btn hr-btn-soft" style="margin-top:12px" @click="goRecords">
|
||||||
|
<x-icon name="add-line" color="#2563EB" font-size="16"></x-icon>
|
||||||
|
<text class="dh-add-t">新增处置记录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { victimList, vitalSigns, injuryMarks, victimTimeline, treatRecords } from '@/mock/index.uts'
|
||||||
|
import { HrVictim, HrVital, HrInjuryMark, HrTimelineNode } from '@/mock/types.uts'
|
||||||
|
import { navTo, navBack } from '@/utils/nav.uts'
|
||||||
|
import { TABS_ITEM_INFO, TABS_ITEM } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s21 伤员详情
|
||||||
|
* 顶部伤情头信息 + 3 个快捷操作 + 4 个分页(基本信息 / 伤情 / 转运 / 处置,xTabs 驱动)。
|
||||||
|
*/
|
||||||
|
type QuickAct = { icon : string, name : string, color : string, bg : string }
|
||||||
|
|
||||||
|
const tabList : TABS_ITEM_INFO[] = [
|
||||||
|
{ title: '基本信息', id: '0' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '伤情', id: '1' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '转运', id: '2' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '处置', id: '3' } as TABS_ITEM_INFO
|
||||||
|
]
|
||||||
|
const activeId = ref('0')
|
||||||
|
|
||||||
|
const tab = computed(() : number => {
|
||||||
|
const i = parseInt(activeId.value)
|
||||||
|
return isNaN(i) ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
const victim = ref<HrVictim>(victimList[1])
|
||||||
|
const vitals = ref<HrVital[]>(vitalSigns)
|
||||||
|
const marks = ref<HrInjuryMark[]>(injuryMarks)
|
||||||
|
const timeline = ref<HrTimelineNode[]>(victimTimeline)
|
||||||
|
const treatNodes = ref<HrTimelineNode[]>(treatRecords)
|
||||||
|
|
||||||
|
const acts : QuickAct[] = [
|
||||||
|
{ icon: 'arrow-left-right-line', name: '调整分级', color: '#B45309', bg: '#FFFBEB' } as QuickAct,
|
||||||
|
{ icon: 'route-line', name: '转运登记', color: '#2563EB', bg: '#EFF6FF' } as QuickAct,
|
||||||
|
{ icon: 'phone-line', name: '联系家属', color: '#6B7280', bg: '#F3F4F6' } as QuickAct
|
||||||
|
]
|
||||||
|
|
||||||
|
// ⚠ x-tabs 的 change 下发的是 TABS_ITEM(不是 TABS_ITEM_INFO),形参类型必须一致
|
||||||
|
function onTabChange(item : TABS_ITEM, index : number) : void {
|
||||||
|
activeId.value = item.id == '' ? index.toString() : item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelLabel = computed(() : string => {
|
||||||
|
const l = victim.value.level
|
||||||
|
if (l == 1) { return '危重' }
|
||||||
|
if (l == 2) { return '重伤' }
|
||||||
|
if (l == 3) { return '轻伤' }
|
||||||
|
return '死亡'
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((opt) => {
|
||||||
|
const raw = opt['id']
|
||||||
|
if (raw != null) {
|
||||||
|
const id = raw as string
|
||||||
|
for (let i = 0; i < victimList.length; i++) {
|
||||||
|
if (victimList[i].id == id) {
|
||||||
|
victim.value = victimList[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function onBack() : void {
|
||||||
|
navBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠ UTS 不提升函数声明:goTransfer 必须写在调用它的 onAct 之前
|
||||||
|
function goTransfer() : void {
|
||||||
|
navTo('/pages/triage/transfer')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAct(i : number) : void {
|
||||||
|
if (i == 0) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '调整检伤分级',
|
||||||
|
content: '当前分级:' + levelLabel.value + '\n调整后需现场检伤组复核。',
|
||||||
|
cancelText: '取消',
|
||||||
|
confirmText: '去调整',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) { navTo('/pages/triage/create') }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (i == 1) { goTransfer(); return }
|
||||||
|
uni.showToast({ title: '正在呼叫登记的联系电话', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goRecords() : void {
|
||||||
|
navTo('/pages/triage/treat-records')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dh {
|
||||||
|
background-image: linear-gradient(to bottom right, #F59E0B, #D97706);
|
||||||
|
padding: 0rpx 28rpx 32rpx 28rpx;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-c1 {
|
||||||
|
position: absolute;
|
||||||
|
right: -80rpx;
|
||||||
|
top: -100rpx;
|
||||||
|
width: 300rpx;
|
||||||
|
height: 300rpx;
|
||||||
|
border-radius: 150rpx;
|
||||||
|
background-color: rgba(255, 255, 255, 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-top {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-back {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-title {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-state {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-w {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
letter-spacing: 0.8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-n {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-name {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-age {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-level {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 4rpx 20rpx 4rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-level-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #B45309;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-ch {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-chip {
|
||||||
|
background-color: rgba(255, 255, 255, 0.22);
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.38);
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 4rpx 18rpx 4rpx 18rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-chip-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-foot {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 快捷操作 ---------- */
|
||||||
|
.dh-acts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-bottom: 2rpx solid #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dha {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 22rpx 8rpx 20rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dha-sep {
|
||||||
|
border-left: 2rpx solid #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dha-ic {
|
||||||
|
width: 68rpx;
|
||||||
|
height: 68rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dha-ic-t {
|
||||||
|
font-size: 35.8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dha-t {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 分页 Tab ---------- */
|
||||||
|
.tabs2 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-bottom: 2rpx solid #E5E7EB;
|
||||||
|
padding: 0rpx 8rpx 0rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs2-tb {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
padding-bottom: 18rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs2-t {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs2-t-on {
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs2-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0rpx;
|
||||||
|
width: 52rpx;
|
||||||
|
height: 5rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
background-color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 信息栅格 ---------- */
|
||||||
|
.ig {
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
border-bottom: 2rpx solid #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-row-last {
|
||||||
|
border-bottom-width: 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-it {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
padding: 18rpx 20rpx 18rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-it-r {
|
||||||
|
border-left: 2rpx solid #E5E7EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-l {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ig-b {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 生命体征 ---------- */
|
||||||
|
.vs5 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
padding: 16rpx 2rpx 16rpx 2rpx;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-warn {
|
||||||
|
background-color: #FEF2F2;
|
||||||
|
border: 2rpx solid #FECACA;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-b {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-b-warn {
|
||||||
|
color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-s {
|
||||||
|
font-size: 20.2rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vc-s-warn {
|
||||||
|
color: #B91C1C;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-warn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-warn-t {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #DC2626;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 伤情标记 ---------- */
|
||||||
|
.dh-mks {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-mk {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
padding: 20rpx 24rpx 20rpx 24rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-mk-no {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-mk-no-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-mk-t {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-mk-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-mk-time {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 多模态 ---------- */
|
||||||
|
.dh-desc {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-upls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-upl {
|
||||||
|
width: 31%;
|
||||||
|
height: 168rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 2.5%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-upl-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-upl-tag {
|
||||||
|
position: absolute;
|
||||||
|
left: 0rpx;
|
||||||
|
right: 0rpx;
|
||||||
|
bottom: 0rpx;
|
||||||
|
background-color: rgba(17, 24, 39, 0.62);
|
||||||
|
padding: 4rpx 0rpx 4rpx 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-upl-tag-t {
|
||||||
|
font-size: 21.2rpx;
|
||||||
|
color: #FFFFFF;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-add-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="离线同步" :xloading="true" @right-click="onSort">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<view class="os-box">
|
||||||
|
<x-icon name="cloud-off-line" color="#92400E" font-size="15"></x-icon>
|
||||||
|
<text class="os-box-t">当前网络不可用,以下记录已暂存本机</text>
|
||||||
|
<view class="hr-offbar-cnt">
|
||||||
|
<text class="os-box-c">{{ pendingCount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-sec title="待上传队列" :more="'共 ' + tasks.length + ' 条'"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow v-for="(t, i) in tasks" :key="i" :icon="t.icon" :theme="t.theme" :title="t.title"
|
||||||
|
:sub="t.sub" :value="t.status" :value-bold="true" :is-last="i == tasks.length - 1"
|
||||||
|
@click="onTaskClick(t)"></hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:14px" theme="info" icon="ℹ"
|
||||||
|
text="提交失败的记录会自动进入本队列;网络恢复后按时间顺序自动重传,无需手动操作。"></hr-tip>
|
||||||
|
|
||||||
|
<view v-if="syncing" class="os-progress">
|
||||||
|
<view class="hr-row-between" style="margin-bottom:9px">
|
||||||
|
<text class="os-p-t">正在重传…</text>
|
||||||
|
<text class="os-p-v">{{ progress }}%</text>
|
||||||
|
</view>
|
||||||
|
<hr-progress :percent="progress"></hr-progress>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view :class="syncing ? 'hr-btn hr-btn-disabled' : 'hr-btn hr-btn-primary'" style="flex:1"
|
||||||
|
@click="onRetryAll">
|
||||||
|
<text class="os-foot-t">{{ syncing ? '正在重传…' : '立即重试全部上传' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { offlineTasks } from '@/mock/index.uts'
|
||||||
|
import { HrOfflineTask } from '@/mock/types.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s19 离线同步
|
||||||
|
* 提交失败就地拦截 + 进离线队列;网络恢复后按时间顺序自动重传。
|
||||||
|
*/
|
||||||
|
const tasks = ref<HrOfflineTask[]>(offlineTasks)
|
||||||
|
const syncing = ref(false)
|
||||||
|
const progress = ref(0)
|
||||||
|
let timer : number = 0
|
||||||
|
|
||||||
|
const pendingCount = computed(() : number => {
|
||||||
|
return tasks.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
function onTaskClick(t : HrOfflineTask) : void {
|
||||||
|
uni.showModal({ title: t.title, content: t.sub, showCancel: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRetryAll() : void {
|
||||||
|
if (syncing.value) { return }
|
||||||
|
syncing.value = true
|
||||||
|
progress.value = 0
|
||||||
|
timer = setInterval(() => {
|
||||||
|
progress.value = progress.value + 10
|
||||||
|
if (progress.value >= 100) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = 0
|
||||||
|
syncing.value = false
|
||||||
|
tasks.value = [] as HrOfflineTask[]
|
||||||
|
uni.showToast({ title: '全部同步完成', icon: 'success' })
|
||||||
|
}
|
||||||
|
}, 260)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort() : void {
|
||||||
|
uni.showToast({ title: '按暂存时间排序', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.os-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFBEB;
|
||||||
|
border: 2rpx solid #FDE68A;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 18rpx 32rpx 18rpx 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-box-t {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #92400E;
|
||||||
|
margin-left: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-box-c {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-progress {
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-p-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-p-v {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.os-foot-t {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="转运登记" @right-click="onMore">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 伤员摘要 ============ -->
|
||||||
|
<hr-card>
|
||||||
|
<view class="hr-row">
|
||||||
|
<view class="tr-bar"></view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<view class="hr-row">
|
||||||
|
<text class="tr-name">李华</text>
|
||||||
|
<text class="tr-sub">男 · 35 岁</text>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<hr-wb :level="2"></hr-wb>
|
||||||
|
</view>
|
||||||
|
<text class="tr-inj">头部开放性外伤 · 右前臂骨折 · 已夹板固定</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 接收医院(床位实时查询) ============ -->
|
||||||
|
<hr-sec title="接收医院" more="🔍 搜索" @more-click="onSearchHos"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<view v-for="(h, i) in hospitalList" :key="i" :class="hosClass(i)" @click="pickHospital(i)">
|
||||||
|
<view :class="i == hosIdx ? 'tr-radio tr-radio-on' : 'tr-radio'"></view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="tr-hos-n">{{ h }}</text>
|
||||||
|
<text class="tr-hos-s">{{ hospitalMeta[i] }}</text>
|
||||||
|
</view>
|
||||||
|
<text :class="hospitalAvail[i] ? 'tr-hos-b' : 'tr-hos-b tr-hos-b-no'">{{ hospitalBed[i] }}</text>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:10px" theme="warn" icon="⚠"
|
||||||
|
text="同仁医院急诊床位已满,不可选择。若接收医院拒绝接收,系统将自动回到本页提示重派。"></hr-tip>
|
||||||
|
|
||||||
|
<!-- ============ 转运方式 ============ -->
|
||||||
|
<hr-sec title="转运方式" :first="false"></hr-sec>
|
||||||
|
<view class="hr-seg">
|
||||||
|
<view v-for="(m, i) in modes" :key="i" :class="i == modeIdx ? 'hr-seg-i hr-seg-i-on' : 'hr-seg-i'"
|
||||||
|
@click="modeIdx = i">
|
||||||
|
<text :class="i == modeIdx ? 'tr-seg-t tr-seg-t-on' : 'tr-seg-t'">{{ m }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 随车联系人 ============ -->
|
||||||
|
<hr-sec title="随车联系人"></hr-sec>
|
||||||
|
<view class="hr-field">
|
||||||
|
<text class="hr-field-label">接收方联系人</text>
|
||||||
|
<input class="hr-field-input" type="text" v-model="contact" placeholder-style="color:#9CA3AF" />
|
||||||
|
</view>
|
||||||
|
<view class="hr-field" style="margin-top:9px">
|
||||||
|
<text class="hr-field-label">联系电话</text>
|
||||||
|
<input class="hr-field-input hr-mono" type="number" v-model="phone" placeholder-style="color:#9CA3AF" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-ghost tr-foot-cancel" @click="onCancel">
|
||||||
|
<text class="tr-foot-t">取消</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-btn hr-btn-primary" style="flex:1" @click="onSubmit">
|
||||||
|
<text class="tr-foot-t2">转运登记</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { hospitalList, hospitalMeta, hospitalBed, hospitalAvail } from '@/mock/index.uts'
|
||||||
|
import { navBack } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s14 转运登记
|
||||||
|
* 接收医院床位实时查询,满员自动拦截并提示重派。
|
||||||
|
*/
|
||||||
|
const modes : string[] = ['救护车 🚑', '直升机 🚁']
|
||||||
|
const modeIdx = ref(0)
|
||||||
|
const hosIdx = ref(0)
|
||||||
|
const contact = ref('刘主任(急诊)')
|
||||||
|
const phone = ref('13800006621')
|
||||||
|
|
||||||
|
function hosClass(i : number) : string {
|
||||||
|
if (i == 0) {
|
||||||
|
return i == hosIdx.value ? 'tr-hos tr-hos-on' : 'tr-hos'
|
||||||
|
}
|
||||||
|
if (i == 2) { return 'tr-hos tr-hos-dim' }
|
||||||
|
return i == hosIdx.value ? 'tr-hos tr-hos-on' : 'tr-hos'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickHospital(i : number) : void {
|
||||||
|
if (!hospitalAvail[i]) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '无法选择',
|
||||||
|
content: '同仁医院急诊床位已满,请改选其他接收医院。',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hosIdx.value = i
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearchHos() : void {
|
||||||
|
uni.showToast({ title: '搜索附近的接收医院', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() : void {
|
||||||
|
if (contact.value == '' || phone.value == '') {
|
||||||
|
uni.showToast({ title: '请填写随车联系人', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '登记中' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '转运登记成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
navBack()
|
||||||
|
}, 700)
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() : void {
|
||||||
|
navBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMore() : void {
|
||||||
|
uni.showToast({ title: '转运单 / 取消转运', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tr-bar {
|
||||||
|
width: 12rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background-color: #F59E0B;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-name {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-sub {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-inj {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 2rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos-on {
|
||||||
|
/* 选中态仅体现在单选圈 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos-dim {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-radio {
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
border: 3.2rpx solid #E5E7EB;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-radio-on {
|
||||||
|
border: 3.2rpx solid #2563EB;
|
||||||
|
background-color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos-n {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos-b {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #059669;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-hos-b-no {
|
||||||
|
color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-seg-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-seg-t-on {
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-foot-cancel {
|
||||||
|
flex: 0 0 200rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-foot-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr-foot-t2 {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="处置记录 · 李华" :xloading="true"
|
||||||
|
@right-click="onAdd">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="add-line" color="#6B7280" font-size="19"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- 处置类型筛选(组件库 xRadioButton 分段切换) -->
|
||||||
|
<view style="margin-bottom:8rpx">
|
||||||
|
<x-radio-button v-model="chipId" :list="chipTabs" height="64rpx" space="4rpx" round="22rpx"
|
||||||
|
text-style="font-weight:bold;" bg-color="#E5E7EB" active-color="#2563EB"
|
||||||
|
active-font-color="#FFFFFF" font-color="#6B7280" font-size="22rpx"></x-radio-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-card style="margin-top:10px">
|
||||||
|
<hr-timeline :nodes="shownNodes"></hr-timeline>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-empty v-if="shownNodes.length == 0" icon="▤" title="该分类下暂无处置记录"
|
||||||
|
sub="切换其他分类,或点击右下角新增一条处置记录"></hr-empty>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-primary" style="flex:1" @click="onAdd">
|
||||||
|
<x-icon name="add-line" color="#FFFFFF" font-size="16"></x-icon>
|
||||||
|
<text class="trc-foot-t">新增处置记录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { treatRecords } from '@/mock/index.uts'
|
||||||
|
import { HrTimelineNode } from '@/mock/types.uts'
|
||||||
|
import { RADIO_BUTTON } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s15 处置记录
|
||||||
|
* 按处置类型分类查看,时间线倒序。
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 处置类型筛选(组件库 xRadioButton 分段切换,替代原 hr-chip 横向滚动)
|
||||||
|
* ⚠ RADIO_BUTTON 的 id 是 string:统一用下标字符串,选中值即筛选序号。
|
||||||
|
*/
|
||||||
|
const chipTabs : RADIO_BUTTON[] = [
|
||||||
|
{ id: '0', title: '全部 4' } as RADIO_BUTTON,
|
||||||
|
{ id: '1', title: '止血 1' } as RADIO_BUTTON,
|
||||||
|
{ id: '2', title: '循环 1' } as RADIO_BUTTON,
|
||||||
|
{ id: '3', title: '分级 2' } as RADIO_BUTTON
|
||||||
|
]
|
||||||
|
const chipId = ref('0')
|
||||||
|
/** 由字符串 id 换算分类序号,供 shownNodes 筛选使用 */
|
||||||
|
const chip = computed(() : number => {
|
||||||
|
const i = parseInt(chipId.value)
|
||||||
|
return isNaN(i) ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
const shownNodes = computed(() : HrTimelineNode[] => {
|
||||||
|
if (chip.value == 0) { return treatRecords }
|
||||||
|
const kw = chip.value == 1 ? '止血' : (chip.value == 2 ? '静脉' : '分级')
|
||||||
|
const arr : HrTimelineNode[] = []
|
||||||
|
for (let i = 0; i < treatRecords.length; i++) {
|
||||||
|
if (treatRecords[i].title.indexOf(kw) > -1) { arr.push(treatRecords[i]) }
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
|
||||||
|
function onAdd() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '新增处置记录',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: '如:15:40 清创缝合完成',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm) { return }
|
||||||
|
const text = res.content
|
||||||
|
if (text == null || text == '') { return }
|
||||||
|
uni.showToast({ title: '处置记录已提交', icon: 'success' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.trc-foot-t {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="伤员查询" :xloading="true"
|
||||||
|
@right-click="onSort">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="17"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 离线条 -->
|
||||||
|
<view class="hr-offbar" @click="goOffline">
|
||||||
|
<x-icon name="cloud-off-line" color="#92400E" font-size="15"></x-icon>
|
||||||
|
<text class="vs-off-t">离线中 · 数据暂存本地</text>
|
||||||
|
<view class="hr-offbar-cnt">
|
||||||
|
<text class="vs-off-cnt-t">3</text>
|
||||||
|
</view>
|
||||||
|
<text class="hr-offbar-go">查看队列 ›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 搜索(xSearch) ============ -->
|
||||||
|
<x-search v-model="keyword" placeholder="姓名 / 身份证 / 编号" round="24rpx" height="84rpx"
|
||||||
|
:border="searchBorder" input-bg-color="#FFFFFF" icon-color="#9CA3AF" font-color="#1F2937"
|
||||||
|
:show-cancel="false"></x-search>
|
||||||
|
|
||||||
|
<!-- ============ 双下拉筛选 ============ -->
|
||||||
|
<view class="vs-filter">
|
||||||
|
<view :class="levelIdx > 0 ? 'vs-sel vs-sel-on' : 'vs-sel'" @click="openSheet(0)">
|
||||||
|
<text class="vs-sel-l">检伤等级</text>
|
||||||
|
<text :class="levelIdx > 0 ? 'vs-sel-v vs-sel-v-on' : 'vs-sel-v'">{{ levelOptions[levelIdx] }}</text>
|
||||||
|
<x-icon name="arrow-down-s-line" color="#9CA3AF" font-size="14"></x-icon>
|
||||||
|
</view>
|
||||||
|
<view :class="statusIdx > 0 ? 'vs-sel vs-sel-on' : 'vs-sel'"
|
||||||
|
style="margin-left:8px" @click="openSheet(1)">
|
||||||
|
<text class="vs-sel-l">检伤状态</text>
|
||||||
|
<text :class="statusIdx > 0 ? 'vs-sel-v vs-sel-v-on' : 'vs-sel-v'">{{ statusOptions[statusIdx] }}</text>
|
||||||
|
<x-icon name="arrow-down-s-line" color="#9CA3AF" font-size="14"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 伤员列表 ============ -->
|
||||||
|
<view style="margin-top:4px">
|
||||||
|
<view v-for="(v, i) in shown" :key="v.id" :class="cardClass(v.level)" @click="openDetail(v)">
|
||||||
|
<view class="hr-row">
|
||||||
|
<view :class="avatarClass(v.level)">
|
||||||
|
<text class="vs-avt-t">{{ v.name.substring(0, 1) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<view class="hr-row">
|
||||||
|
<text class="vs-name">{{ v.name }}</text>
|
||||||
|
<hr-wb :level="v.level" small></hr-wb>
|
||||||
|
</view>
|
||||||
|
<text class="vs-code">{{ v.gender }} · {{ v.age }} 岁 · {{ v.code }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="vs-inj">
|
||||||
|
<text class="vs-inj-t">伤情:{{ v.injury }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="vs-bot">
|
||||||
|
<view class="vs-status" :style="statusStyle(v.statusTheme)">
|
||||||
|
<text class="vs-status-t" :style="statusTextStyle(v.statusTheme)">{{ v.status }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1"></view>
|
||||||
|
<text class="vs-time">{{ v.createdAt }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-empty v-if="shown.length == 0" icon="♡" title="没有匹配的伤员"
|
||||||
|
sub="换个筛选条件,或点击右上角扫码按腕带编号查询"></hr-empty>
|
||||||
|
|
||||||
|
<view v-if="shown.length > 0" class="vs-hint">
|
||||||
|
<text class="vs-hint-t">✦ 点击卡片查看伤员详情</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- ============ 筛选选择器 ============ -->
|
||||||
|
<template v-slot:overlay>
|
||||||
|
<view v-if="sheet >= 0" class="hr-mask" @click="closeSheet">
|
||||||
|
<view class="hr-sheet" @click="stop">
|
||||||
|
<view class="hr-sheet-h">
|
||||||
|
<text class="hr-sheet-h-b">{{ sheet == 0 ? '选择检伤等级' : '选择检伤状态' }}</text>
|
||||||
|
<view class="hr-sheet-h-x" @click="closeSheet">
|
||||||
|
<x-icon name="close-line" color="#9CA3AF" font-size="18"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-for="(o, i) in currentOptions" :key="i" class="hr-sheet-opt"
|
||||||
|
:class="i == currentOptions.length - 1 ? 'hr-sheet-opt-last' : ''" @click="pickOption(i)">
|
||||||
|
<text :class="isPicked(i) ? 'hr-sheet-opt-t hr-sheet-opt-on' : 'hr-sheet-opt-t'">{{ o }}</text>
|
||||||
|
<x-icon v-if="isPicked(i)" name="check-line" color="#2563EB" font-size="17"></x-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { victimList } from '@/mock/index.uts'
|
||||||
|
import { HrVictim } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s04 伤员查询
|
||||||
|
* 支持按「检伤等级 / 检伤状态」双下拉筛选 + 关键字搜索 + 扫码查询。
|
||||||
|
*/
|
||||||
|
const levelOptions : string[] = ['全部', '危重', '重伤', '轻伤', '死亡']
|
||||||
|
const statusOptions : string[] = ['全部', '已检伤', '转运中', '待转运', '未转运']
|
||||||
|
|
||||||
|
const keyword = ref('')
|
||||||
|
const levelIdx = ref(0)
|
||||||
|
const statusIdx = ref(0)
|
||||||
|
const sheet = ref(-1)
|
||||||
|
const list = ref<HrVictim[]>(victimList)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* xSearch 的 border 是「数组」prop,固定 3 项 = [宽度, 线型, 颜色],
|
||||||
|
* 少于 3 项内部直接返回 none。设计稿搜索框为 1px solid #E5E7EB。
|
||||||
|
*/
|
||||||
|
const searchBorder : string[] = ['1px', 'solid', '#E5E7EB']
|
||||||
|
|
||||||
|
const currentOptions = computed(() : string[] => {
|
||||||
|
return sheet.value == 0 ? levelOptions : statusOptions
|
||||||
|
})
|
||||||
|
|
||||||
|
const shown = computed(() : HrVictim[] => {
|
||||||
|
const out : HrVictim[] = []
|
||||||
|
for (let i = 0; i < list.value.length; i++) {
|
||||||
|
const v = list.value[i]
|
||||||
|
if (levelIdx.value > 0 && v.level != levelIdx.value) { continue }
|
||||||
|
if (statusIdx.value > 0) {
|
||||||
|
const label = statusOptions[statusIdx.value]
|
||||||
|
if (label == '转运中' && v.status.indexOf('转运中') < 0) { continue }
|
||||||
|
if (label == '待转运' && v.status.indexOf('待转运') < 0) { continue }
|
||||||
|
if (label == '未转运' && v.status.indexOf('未转运') < 0) { continue }
|
||||||
|
if (label == '已检伤' && v.status.indexOf('已转运') < 0 && v.status.indexOf('转运中') < 0) { continue }
|
||||||
|
}
|
||||||
|
if (keyword.value != '') {
|
||||||
|
const kw = keyword.value.toLowerCase()
|
||||||
|
const hit = v.name.toLowerCase().indexOf(kw) > -1 || v.code.toLowerCase().indexOf(kw) > -1
|
||||||
|
if (!hit) { continue }
|
||||||
|
}
|
||||||
|
out.push(v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
function cardClass(level : number) : string {
|
||||||
|
return `vs-card vs-card-l${level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function avatarClass(level : number) : string {
|
||||||
|
return `vs-avt vs-avt-l${level}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusStyle(theme : string) : string {
|
||||||
|
if (theme == 'pri') { return 'background-color:#EFF6FF;border:1px solid #BFDBFE' }
|
||||||
|
if (theme == 'amb') { return 'background-color:#FFFBEB;border:1px solid #FDE68A' }
|
||||||
|
if (theme == 'grn') { return 'background-color:#ECFDF5;border:1px solid #A7F3D0' }
|
||||||
|
return 'background-color:#F3F4F6;border:1px solid #E5E7EB'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTextStyle(theme : string) : string {
|
||||||
|
if (theme == 'pri') { return 'color:#1D4ED8' }
|
||||||
|
if (theme == 'amb') { return 'color:#B45309' }
|
||||||
|
if (theme == 'grn') { return 'color:#047857' }
|
||||||
|
return 'color:#6B7280'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSheet(i : number) : void {
|
||||||
|
sheet.value = i
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSheet() : void {
|
||||||
|
sheet.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() : void {
|
||||||
|
// 阻止点击面板时关闭
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPicked(i : number) : boolean {
|
||||||
|
return sheet.value == 0 ? levelIdx.value == i : statusIdx.value == i
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickOption(i : number) : void {
|
||||||
|
if (sheet.value == 0) { levelIdx.value = i } else { statusIdx.value = i }
|
||||||
|
sheet.value = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(v : HrVictim) : void {
|
||||||
|
navTo('/pages/triage/detail?id=' + v.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort() : void {
|
||||||
|
uni.showToast({ title: '按建档时间 / 分级排序', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goOffline() : void {
|
||||||
|
navTo('/pages/triage/offline-sync')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vs-off-t {
|
||||||
|
font-size: 26.8rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #92400E;
|
||||||
|
margin-left: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-off-cnt-t {
|
||||||
|
font-size: 22.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 搜索 ---------- */
|
||||||
|
.vs-srch {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
height: 84rpx;
|
||||||
|
padding: 0rpx 20rpx 0rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-srch-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #1F2937;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0rpx 16rpx 0rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-srch-scan {
|
||||||
|
width: 52rpx;
|
||||||
|
height: 52rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-scan-btn {
|
||||||
|
width: 84rpx;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 筛选 ---------- */
|
||||||
|
.vs-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-sel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
padding: 18rpx 22rpx 18rpx 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-sel-on {
|
||||||
|
border: 2rpx solid #2563EB;
|
||||||
|
background-color: #EFF6FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-sel-l {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-right: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-sel-v {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-sel-v-on {
|
||||||
|
color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 伤员卡 ---------- */
|
||||||
|
.vs-card {
|
||||||
|
position: relative;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 22rpx 24rpx 22rpx 32rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-card-bar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0rpx;
|
||||||
|
top: 0rpx;
|
||||||
|
bottom: 0rpx;
|
||||||
|
width: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-card-l1 {
|
||||||
|
border-left: 8rpx solid #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-card-l2 {
|
||||||
|
border-left: 8rpx solid #F59E0B;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-card-l3 {
|
||||||
|
border-left: 8rpx solid #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-card-l4 {
|
||||||
|
border-left: 8rpx solid #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-avt {
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
border-radius: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-avt-l1 {
|
||||||
|
background-color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-avt-l2 {
|
||||||
|
background-color: #D97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-avt-l3 {
|
||||||
|
background-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-avt-l4 {
|
||||||
|
background-color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-avt-t {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-name {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-code {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-inj {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
background-color: #F9FAFB;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 12rpx 16rpx 12rpx 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-inj-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-bot {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-status {
|
||||||
|
border-radius: 18rpx;
|
||||||
|
padding: 8rpx 20rpx 8rpx 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-status-t {
|
||||||
|
font-size: 24.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-time {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-hint {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12rpx 0rpx 16rpx 0rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vs-hint-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<template>
|
||||||
|
<hr-page page-bg="#F3F4F6" status-bg="#FFFFFF" nav-title="设备详情" :xloading="true" @right-click="onMore">
|
||||||
|
<template v-slot:right>
|
||||||
|
<x-icon name="more-fill" color="#6B7280" font-size="18"></x-icon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- ============ 概览 ============ -->
|
||||||
|
<hr-card>
|
||||||
|
<view class="hr-row">
|
||||||
|
<view class="hr-avt hr-avt-lg dd-avt">
|
||||||
|
<text>{{ device.icon }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="dd-name">{{ device.name }}</text>
|
||||||
|
<text class="dd-meta">{{ device.meta }}</text>
|
||||||
|
<view style="margin-top:7px">
|
||||||
|
<hr-pc :level="2" :text="device.status"></hr-pc>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 借用前自检 ============ -->
|
||||||
|
<hr-sec :title="'借用前自检(5 项缺一不可)'" :more="checkedCount + ' / 5'"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<view v-for="(c, i) in checks" :key="i" :class="c.checked ? 'dd-ck dd-ck-on' : 'dd-ck'"
|
||||||
|
@click="toggleCheck(i)">
|
||||||
|
<view :class="c.checked ? 'dd-ck-box dd-ck-box-on' : 'dd-ck-box'">
|
||||||
|
<x-icon v-if="c.checked" name="check-line" color="#FFFFFF" font-size="14"></x-icon>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="dd-ck-t">{{ c.title }}</text>
|
||||||
|
<text class="dd-ck-s">{{ c.sub }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<!-- ============ 维护记录 ============ -->
|
||||||
|
<hr-sec title="维护记录" :first="false"></hr-sec>
|
||||||
|
<hr-card>
|
||||||
|
<hr-timeline :nodes="device.maintain"></hr-timeline>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<hr-tip style="margin-top:14px" theme="warn" icon="⚠"
|
||||||
|
text="发现设备异常可直接一键报修,系统将自动流转至设备科并暂停借出。"></hr-tip>
|
||||||
|
|
||||||
|
<view style="height:14px"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-ghost dd-foot-report" @click="onReport">
|
||||||
|
<x-icon name="error-warning-line" color="#1F2937" font-size="16"></x-icon>
|
||||||
|
<text class="dd-foot-t">报修</text>
|
||||||
|
</view>
|
||||||
|
<view :class="allChecked ? 'hr-btn hr-btn-primary' : 'hr-btn hr-btn-disabled'" style="flex:1"
|
||||||
|
@click="onBorrow">
|
||||||
|
<text :class="allChecked ? 'dd-foot-t2' : 'dd-foot-t3'">
|
||||||
|
{{ allChecked ? '确认自检并借用' : '请完成 5 项自检' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</hr-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { deviceDetail } from '@/mock/index.uts'
|
||||||
|
import { HrDeviceCheck } from '@/mock/types.uts'
|
||||||
|
import { navBack } from '@/utils/nav.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s32 设备详情
|
||||||
|
* 借用前 5 项自检缺一不可,全部确认后才可借出。
|
||||||
|
*/
|
||||||
|
const device = deviceDetail
|
||||||
|
const checks = ref<HrDeviceCheck[]>([])
|
||||||
|
|
||||||
|
// 深拷贝一份,避免污染 mock 数据
|
||||||
|
const arr : HrDeviceCheck[] = []
|
||||||
|
for (let i = 0; i < device.checkList.length; i++) {
|
||||||
|
const c = device.checkList[i]
|
||||||
|
arr.push({ title: c.title, sub: c.sub, checked: c.checked } as HrDeviceCheck)
|
||||||
|
}
|
||||||
|
checks.value = arr
|
||||||
|
|
||||||
|
const checkedCount = computed(() : number => {
|
||||||
|
let n = 0
|
||||||
|
for (let i = 0; i < checks.value.length; i++) {
|
||||||
|
if (checks.value[i].checked) { n++ }
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
})
|
||||||
|
|
||||||
|
const allChecked = computed(() : boolean => {
|
||||||
|
return checkedCount.value == checks.value.length
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggleCheck(i : number) : void {
|
||||||
|
const list : HrDeviceCheck[] = []
|
||||||
|
for (let k = 0; k < checks.value.length; k++) {
|
||||||
|
const c = checks.value[k]
|
||||||
|
list.push({
|
||||||
|
title: c.title, sub: c.sub, checked: k == i ? !c.checked : c.checked
|
||||||
|
} as HrDeviceCheck)
|
||||||
|
}
|
||||||
|
checks.value = list
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBorrow() : void {
|
||||||
|
if (!allChecked.value) {
|
||||||
|
uni.showToast({ title: '请先完成 5 项自检', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showLoading({ title: '提交中' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showModal({
|
||||||
|
title: '借用成功',
|
||||||
|
content: `${device.name}\n预计归还时间:3 天后(09-27)\n已生成借用单,逾期将自动提醒。`,
|
||||||
|
showCancel: false,
|
||||||
|
success: (res) => {
|
||||||
|
navBack()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 700)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReport() : void {
|
||||||
|
uni.showModal({
|
||||||
|
title: '一键报修',
|
||||||
|
editable: true,
|
||||||
|
placeholderText: '请描述设备异常,如:屏幕无法点亮',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showToast({ title: '已流转至设备科', icon: 'success' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMore() : void {
|
||||||
|
uni.showToast({ title: '设备档案 / 借还记录', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.dd-avt {
|
||||||
|
margin-right: 26rpx;
|
||||||
|
background-color: #2563EB;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-name {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-meta {
|
||||||
|
font-size: 25.8rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-ck {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 22rpx;
|
||||||
|
padding-bottom: 22rpx;
|
||||||
|
border-bottom: 2rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-ck-on {
|
||||||
|
/* 选中态体现在勾选框 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-ck-box {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
border: 3rpx solid #D1D5DB;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-ck-box-on {
|
||||||
|
border: 3rpx solid #10B981;
|
||||||
|
background-color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-ck-t {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-ck-s {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-foot-report {
|
||||||
|
flex: 0 0 220rpx;
|
||||||
|
margin-right: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-foot-t {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
margin-left: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-foot-t2 {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dd-foot-t3 {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #9CA3AF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<view class="hr-page" style="background-color:#F3F4F6">
|
||||||
|
|
||||||
|
<hr-statusbar bg="#FFFFFF"></hr-statusbar>
|
||||||
|
|
||||||
|
<view class="wk-nav">
|
||||||
|
<text class="wk-nav-t">设备借用</text>
|
||||||
|
<view class="wk-nav-a" @click="onLedger">
|
||||||
|
<x-icon name="arrow-up-down-line" color="#6B7280" font-size="16"></x-icon>
|
||||||
|
<text class="wk-nav-a-t">台账</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-flow">
|
||||||
|
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
|
||||||
|
<view class="hr-body">
|
||||||
|
|
||||||
|
<!-- 状态筛选(xTabs) -->
|
||||||
|
<x-tabs :list="tabList" :model-value="activeId" item-width="33.33%" height="84rpx" font-size="28rpx"
|
||||||
|
active-font-size="28rpx" title-color="#6B7280" active-title-color="#2563EB" line-color="#2563EB"
|
||||||
|
title-padding="0" :is-item-center="true" @change="onTabChange"></x-tabs>
|
||||||
|
|
||||||
|
<view style="margin-top:20rpx">
|
||||||
|
<view v-for="(d, i) in shown" :key="d.id" class="dv-card" @click="openDetail(d)">
|
||||||
|
<view class="dv-ic" :style="{ backgroundColor: d.iconBg }">
|
||||||
|
<x-icon :name="d.icon" :color="d.iconColor" font-size="44rpx"></x-icon>
|
||||||
|
</view>
|
||||||
|
<view class="hr-flex1">
|
||||||
|
<text class="dv-name">{{ d.name }}</text>
|
||||||
|
<text class="dv-meta">{{ d.meta }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="dv-st" :style="stStyle(d.theme)">
|
||||||
|
<text class="dv-st-t" :style="stTextStyle(d.theme)">{{ d.status }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-empty v-if="shown.length == 0" icon="⚙" title="该状态下暂无设备"
|
||||||
|
sub="切换上方分段查看其它状态的设备"></hr-empty>
|
||||||
|
|
||||||
|
<hr-empty v-if="shown.length == 0" icon="⚙" title="该状态下暂无设备"
|
||||||
|
sub="切换上方分段查看其它状态的设备"></hr-empty>
|
||||||
|
|
||||||
|
<hr-sec title="设备台账" more="全部 24 台 ›"></hr-sec>
|
||||||
|
<hr-card variant="list">
|
||||||
|
<hr-lrow icon="check-line" theme="grn" title="状态正常" sub="可随时借用">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="dv-v dv-v-grn">18 台</text>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
<hr-lrow icon="file-list-3-line" theme="amb" title="借用中" sub="预计陆续归还">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="dv-v dv-v-amb">4 台</text>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
<hr-lrow icon="monitor-line" theme="red" title="维修 / 保养中" sub="暂不可借" :is-last="true">
|
||||||
|
<template v-slot:right>
|
||||||
|
<text class="dv-v dv-v-red">2 台</text>
|
||||||
|
</template>
|
||||||
|
</hr-lrow>
|
||||||
|
</hr-card>
|
||||||
|
|
||||||
|
<view style="height:28rpx"></view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 加载中覆盖层(组件库 xLoading) -->
|
||||||
|
<view v-if="loading" class="hr-loading-mask">
|
||||||
|
<x-loading label="加载中…" :icon-size="'44rpx'" :text-size="'26rpx'" :vertical="true" color="#2563EB"
|
||||||
|
text-color="#6B7280"></x-loading>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="hr-fixedbar">
|
||||||
|
<view class="hr-btn hr-btn-primary" style="flex:1" @click="onScanBorrow">
|
||||||
|
<x-icon name="scan-line" color="#FFFFFF" font-size="17"></x-icon>
|
||||||
|
<text class="dv-bt">扫码借用设备</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<hr-tabbar :active="2"></hr-tabbar>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="uts" setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { deviceList } from '@/mock/index.uts'
|
||||||
|
import { HrDevice } from '@/mock/types.uts'
|
||||||
|
import { navTo } from '@/utils/nav.uts'
|
||||||
|
import { mockRequest } from '@/utils/request.uts'
|
||||||
|
import { TABS_ITEM_INFO, TABS_ITEM } from '@/uni_modules/tmx-ui/interface.uts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* s31 设备借用(仓管 Tab「设备」)
|
||||||
|
* 按「可借用 / 借用中 / 全部」分段筛选,维修中设备不可借。
|
||||||
|
*/
|
||||||
|
const loading = ref(true)
|
||||||
|
const segIds : string[] = ['0', '1', '2']
|
||||||
|
const activeId = ref('0')
|
||||||
|
|
||||||
|
/** xTabs 数据 */
|
||||||
|
const tabList : TABS_ITEM_INFO[] = [
|
||||||
|
{ title: '可借用', id: '0' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '借用中', id: '1' } as TABS_ITEM_INFO,
|
||||||
|
{ title: '全部', id: '2' } as TABS_ITEM_INFO
|
||||||
|
]
|
||||||
|
|
||||||
|
const seg = computed(() : number => {
|
||||||
|
const i = parseInt(activeId.value)
|
||||||
|
return isNaN(i) ? 0 : i
|
||||||
|
})
|
||||||
|
|
||||||
|
// ⚠ x-tabs 的 change 下发的是 TABS_ITEM(不是 TABS_ITEM_INFO),形参类型必须一致
|
||||||
|
function onTabChange(item : TABS_ITEM, index : number) : void {
|
||||||
|
activeId.value = item.id == '' ? index.toString() : item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
mockRequest(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const list = ref<HrDevice[]>(deviceList)
|
||||||
|
|
||||||
|
const shown = computed(() : HrDevice[] => {
|
||||||
|
const arr : HrDevice[] = []
|
||||||
|
for (let i = 0; i < list.value.length; i++) {
|
||||||
|
const d = list.value[i]
|
||||||
|
if (seg.value == 0 && d.status != '可借') { continue }
|
||||||
|
if (seg.value == 1 && d.status != '借用中') { continue }
|
||||||
|
arr.push(d)
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
})
|
||||||
|
|
||||||
|
function stStyle(theme : string) : string {
|
||||||
|
if (theme == 'grn') { return 'background-color:#ECFDF5' }
|
||||||
|
if (theme == 'amb') { return 'background-color:#FEF3C7' }
|
||||||
|
return 'background-color:#FEF2F2'
|
||||||
|
}
|
||||||
|
|
||||||
|
function stTextStyle(theme : string) : string {
|
||||||
|
if (theme == 'grn') { return 'color:#059669' }
|
||||||
|
if (theme == 'amb') { return 'color:#92400E' }
|
||||||
|
return 'color:#DC2626'
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDetail(d : HrDevice) : void {
|
||||||
|
navTo('/pages/warehouse/device-detail?id=' + d.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScanBorrow() : void {
|
||||||
|
uni.showToast({ title: '对准设备二维码开始借用', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLedger() : void {
|
||||||
|
uni.showToast({ title: '设备台账(24 台)', icon: 'none' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wk-nav {
|
||||||
|
height: 96rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 32rpx;
|
||||||
|
padding-right: 32rpx;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border-bottom: 2rpx solid #F3F4F6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wk-nav-t {
|
||||||
|
font-size: 33.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wk-nav-a {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wk-nav-a-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-seg-t {
|
||||||
|
font-size: 29.2rpx;
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-seg-t-on {
|
||||||
|
color: #2563EB;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
border: 2rpx solid #E5E7EB;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
padding: 24rpx 24rpx 24rpx 24rpx;
|
||||||
|
margin-bottom: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-ic {
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
border-radius: 26rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 22rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-ic-t {
|
||||||
|
font-size: 49.2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-name {
|
||||||
|
font-size: 30.2rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1F2937;
|
||||||
|
lines: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-meta {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
color: #9CA3AF;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-st {
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 6rpx 16rpx 6rpx 16rpx;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-st-t {
|
||||||
|
font-size: 23.6rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-v {
|
||||||
|
font-size: 31.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-v-grn {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-v-amb {
|
||||||
|
color: #D97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-v-red {
|
||||||
|
color: #DC2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dv-bt {
|
||||||
|
font-size: 32.4rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFFFFF;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||