1
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user