1
This commit is contained in:
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -0,0 +1,273 @@
|
||||
<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="gd-ring">
|
||||
<x-circle-progress :size="76" :line-width="8" color="#F3F4F6" active-color="#F59E0B"
|
||||
:model-value="ringPercent" :show-label="false" :duration="600"></x-circle-progress>
|
||||
<view class="gd-ring-label">
|
||||
<text class="gd-ring-b">{{ goods.stock }}</text>
|
||||
<text class="gd-ring-u">{{ goods.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="hr-flex1">
|
||||
<text class="gd-name">{{ goods.name }}</text>
|
||||
<text class="gd-code">编码 {{ goods.code }} · {{ goods.spec }}</text>
|
||||
<view style="margin-top:7px">
|
||||
<hr-pc :level="0" :text="goods.statusLabel"></hr-pc>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</hr-card>
|
||||
|
||||
<!-- ============ 库存信息 ============ -->
|
||||
<hr-sec title="库存信息"></hr-sec>
|
||||
<hr-card variant="list">
|
||||
<hr-lrow icon="▦" title="当前库存" :sub="'预警线 ' + goods.warnLine + ' ' + goods.unit">
|
||||
<template v-slot:right>
|
||||
<text class="gd-v gd-v-red">{{ goods.stock }} {{ goods.unit }}</text>
|
||||
</template>
|
||||
</hr-lrow>
|
||||
<hr-lrow icon="⇅" theme="pri" title="近 30 天出库" :sub="goods.dailyUse">
|
||||
<template v-slot:right>
|
||||
<text class="gd-v">{{ goods.out30 }} {{ goods.unit }}</text>
|
||||
</template>
|
||||
</hr-lrow>
|
||||
<hr-lrow icon="▤" theme="grn" title="最近入库" :sub="goods.lastIn" :is-last="true">
|
||||
<template v-slot:right>
|
||||
<text class="gd-v gd-v-grn">{{ goods.lastInQty }}</text>
|
||||
</template>
|
||||
</hr-lrow>
|
||||
</hr-card>
|
||||
|
||||
<!-- ============ 消耗趋势 ============ -->
|
||||
<hr-sec title="30 天消耗趋势" more="查看明细 ›"></hr-sec>
|
||||
<hr-card>
|
||||
<view class="gd-chart">
|
||||
<view v-for="(h, i) in barHeights" :key="i" class="gd-bar-wrap">
|
||||
<view class="gd-bar" :style="barStyle(i, h)"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gd-xaxis">
|
||||
<text v-for="(x, i) in xLabels" :key="i" class="gd-x">{{ x }}</text>
|
||||
</view>
|
||||
<view class="gd-legend">
|
||||
<view class="gd-lg">
|
||||
<view class="gd-lg-d" style="background-color:#EFF6FF"></view>
|
||||
<text class="gd-lg-t">正常区间</text>
|
||||
</view>
|
||||
<view class="gd-lg">
|
||||
<view class="gd-lg-d" style="background-color:#F59E0B"></view>
|
||||
<text class="gd-lg-t">接近预警</text>
|
||||
</view>
|
||||
<view class="gd-lg">
|
||||
<view class="gd-lg-d" style="background-color:#DC2626"></view>
|
||||
<text class="gd-lg-t">低于预警线</text>
|
||||
</view>
|
||||
</view>
|
||||
</hr-card>
|
||||
|
||||
<hr-tip style="margin-top:14px" theme="warn" icon="⚠"
|
||||
:text="'按当前消耗速度(' + goods.dailyUse + '),库存预计 ' + daysLeft + ' 天后耗尽,建议尽快补货。'"></hr-tip>
|
||||
|
||||
<view class="hr-btn hr-btn-primary hr-btn-lg" style="margin-top:12px" @click="onRestock">
|
||||
<x-icon name="add-line" color="#FFFFFF" font-size="17"></x-icon>
|
||||
<text class="gd-bt">发起补货申请</text>
|
||||
</view>
|
||||
|
||||
<view style="height:14px"></view>
|
||||
</view>
|
||||
</hr-page>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { goodsDetail } from '@/mock/index.uts'
|
||||
import { navTo } from '@/utils/nav.uts'
|
||||
|
||||
/**
|
||||
* s25 物资详情
|
||||
* 库存环 + 库存信息 + 30 天消耗趋势 + 补货入口。
|
||||
*/
|
||||
const goods = goodsDetail
|
||||
const xLabels : string[] = ['08-24', '08-31', '09-07', '09-14', '09-21']
|
||||
|
||||
const ringPercent = computed(() : number => {
|
||||
// 库存 / (预警线 × 2.5) —— 8 / 50 ≈ 16%,与设计稿 18% 量级一致
|
||||
return Math.round(goods.stock * 100 / (goods.warnLine * 2.5))
|
||||
})
|
||||
|
||||
const barHeights = computed(() : number[] => {
|
||||
const arr : number[] = []
|
||||
for (let i = 0; i < goods.trend.length; i++) {
|
||||
arr.push(Math.round(176 * goods.trend[i] / 100))
|
||||
}
|
||||
return arr
|
||||
})
|
||||
|
||||
const daysLeft = computed(() : number => {
|
||||
const daily = goods.out30 / 30
|
||||
if (daily <= 0) { return 0 }
|
||||
return Math.floor(goods.stock / daily)
|
||||
})
|
||||
|
||||
function barStyle(i : number, h : number) : string {
|
||||
let color = '#EFF6FF'
|
||||
if (i == goods.trend.length - 1) { color = '#DC2626' }
|
||||
else if (i == goods.trend.length - 2) { color = '#F59E0B' }
|
||||
return `height:${h}rpx;background-color:${color};`
|
||||
}
|
||||
|
||||
function onMore() : void {
|
||||
uni.showToast({ title: '出入库流水 / 编辑物资', icon: 'none' })
|
||||
}
|
||||
|
||||
function onRestock() : void {
|
||||
uni.showModal({
|
||||
title: '发起补货申请',
|
||||
content: `物资:${goods.name}\n建议补货量:40 ${goods.unit}(约 2 周用量)`,
|
||||
cancelText: '取消',
|
||||
confirmText: '提交申请',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({ title: '补货申请已提交', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
navTo('/pages/message/index')
|
||||
}, 700)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.gd-ring {
|
||||
width: 152rpx;
|
||||
height: 152rpx;
|
||||
position: relative;
|
||||
margin-right: 26rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gd-ring-label {
|
||||
position: absolute;
|
||||
left: 0rpx;
|
||||
top: 0rpx;
|
||||
width: 152rpx;
|
||||
height: 152rpx;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gd-ring-b {
|
||||
font-size: 42.6rpx;
|
||||
font-weight: bold;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.gd-ring-u {
|
||||
font-size: 22.4rpx;
|
||||
color: #9CA3AF;
|
||||
margin-left: 2rpx;
|
||||
}
|
||||
|
||||
.gd-name {
|
||||
font-size: 33.6rpx;
|
||||
font-weight: bold;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.gd-code {
|
||||
font-size: 25.8rpx;
|
||||
color: #6B7280;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.gd-v {
|
||||
font-size: 33.6rpx;
|
||||
font-weight: bold;
|
||||
color: #1F2937;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.gd-v-red {
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
.gd-v-grn {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* ---------- 趋势图 ---------- */
|
||||
.gd-chart {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
height: 176rpx;
|
||||
}
|
||||
|
||||
.gd-bar-wrap {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.gd-bar {
|
||||
border-radius: 8rpx 8rpx 0rpx 0rpx;
|
||||
}
|
||||
|
||||
.gd-xaxis {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
|
||||
.gd-x {
|
||||
font-size: 22.4rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
.gd-legend {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.gd-lg {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-right: 28rpx;
|
||||
}
|
||||
|
||||
.gd-lg-d {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.gd-lg-t {
|
||||
font-size: 23.6rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.gd-bt {
|
||||
font-size: 35.8rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,397 @@
|
||||
<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="toggleFlash">
|
||||
<x-icon name="flashlight-line" :color="flash ? '#2563EB' : '#6B7280'" font-size="16"></x-icon>
|
||||
<text class="wk-nav-a-t">手电</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="hr-scroll" :scroll-y="true" :show-scrollbar="false">
|
||||
<view class="hr-body">
|
||||
|
||||
<!-- ============ 取景框 ============ -->
|
||||
<view class="sc-box">
|
||||
<!-- 真实相机(APP / H5) -->
|
||||
<!-- #ifdef APP || WEB -->
|
||||
<x-mlkit-scannig-u :flash="flash" :autoOpenCamera="true" :cameraWidth="1080"
|
||||
:cameraeiHght="1080" style="width:100%;height:100%;position:absolute;left:0;top:0"
|
||||
@scan="onScan"></x-mlkit-scannig-u>
|
||||
<!-- #endif -->
|
||||
|
||||
<view class="sc-frame">
|
||||
<view class="sc-cf sc-cf-tl"></view>
|
||||
<view class="sc-cf sc-cf-tr"></view>
|
||||
<view class="sc-cf sc-cf-bl"></view>
|
||||
<view class="sc-cf sc-cf-br"></view>
|
||||
<view class="sc-line" :style="laserStyle"></view>
|
||||
</view>
|
||||
<text class="sc-tip">将条码对准框内 · 自动识别</text>
|
||||
</view>
|
||||
|
||||
<!-- ============ 业务动作 ============ -->
|
||||
<view class="hr-seg" style="margin-top:12px">
|
||||
<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 ? 'sc-seg-t sc-seg-t-on' : 'sc-seg-t'">{{ m }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ============ 识别反馈(三态) ============ -->
|
||||
<hr-sec title="识别反馈" :first="false"></hr-sec>
|
||||
<view v-if="feedbacks.length == 0" class="sc-empty">
|
||||
<x-icon name="scan-line" color="#9CA3AF" font-size="24"></x-icon>
|
||||
<text class="sc-empty-t">等待扫码…</text>
|
||||
<text class="sc-empty-s">识别后会在此即时给出 ✅ 成功 / ⚠ 库存不足 / ❌ 未登记 三种反馈</text>
|
||||
</view>
|
||||
<view v-for="(f, i) in feedbacks" :key="i" :class="feedbackClass(f.theme)">
|
||||
<view class="sc-st-ic" :style="{ backgroundColor: feedbackColor(f.theme) }">
|
||||
<text class="sc-st-ic-t">{{ f.icon }}</text>
|
||||
</view>
|
||||
<view class="hr-flex1">
|
||||
<text class="sc-st-b">{{ f.title }}</text>
|
||||
<text class="sc-st-s">{{ f.sub }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ============ 本次连续扫入 ============ -->
|
||||
<hr-sec title="本次连续扫入" :more="records.length + ' 条'"></hr-sec>
|
||||
<hr-card variant="list">
|
||||
<hr-lrow v-for="(r, i) in records" :key="i" :icon="r.icon"
|
||||
:theme="r.theme == 'grn' ? 'grn' : (r.theme == 'amb' ? 'amb' : 'red')" :title="r.name"
|
||||
:sub="r.time + ' · ' + r.delta" :value="r.status"
|
||||
:value-theme="r.theme == 'grn' ? 'grn' : 'amb'" :value-bold="true"
|
||||
:is-last="i == records.length - 1" @click="onRecord(r)"></hr-lrow>
|
||||
</hr-card>
|
||||
|
||||
<view style="height:14px"></view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<hr-tabbar :active="0"></hr-tabbar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { scanFeedbacks, scanRecords } from '@/mock/index.uts'
|
||||
import { HrScanFeedback, HrScanRecord } from '@/mock/types.uts'
|
||||
|
||||
/**
|
||||
* s23 扫码出入库(仓管 Tab「扫码」)
|
||||
* 连续扫码三态即时反馈:✅ 入库成功 / ⚠ 库存不足 / ❌ 未登记。
|
||||
*/
|
||||
const modes : string[] = ['入库', '出库', '盘点']
|
||||
const modeIdx = ref(0)
|
||||
const flash = ref(false)
|
||||
|
||||
const feedbacks = ref<HrScanFeedback[]>([
|
||||
scanFeedbacks[0], scanFeedbacks[1], scanFeedbacks[2]
|
||||
])
|
||||
const records = ref<HrScanRecord[]>([
|
||||
scanRecords[0], scanRecords[1], scanRecords[2]
|
||||
])
|
||||
|
||||
let seq = 3
|
||||
|
||||
function shortCode(code : string) : string {
|
||||
if (code.length <= 10) { return code }
|
||||
return code.substring(0, 10)
|
||||
}
|
||||
|
||||
function nowTime() : string {
|
||||
const d = new Date()
|
||||
const hh = d.getHours() < 10 ? '0' + d.getHours().toString() : d.getHours().toString()
|
||||
const mm = d.getMinutes() < 10 ? '0' + d.getMinutes().toString() : d.getMinutes().toString()
|
||||
return hh + ':' + mm
|
||||
}
|
||||
|
||||
/** 依据条码特征给出三态反馈(mock 规则,后续替换为接口返回) */
|
||||
function handleCode(code : string) : void {
|
||||
seq = seq + 1
|
||||
const fb : HrScanFeedback = {
|
||||
theme: 'ok', icon: '✓',
|
||||
title: '一次性医用口罩 · ' + modes[modeIdx.value] + '成功',
|
||||
sub: '规格 50 只/盒 · 现存 320 盒 · 张明远 · 编码 ' + shortCode(code)
|
||||
} as HrScanFeedback
|
||||
|
||||
const rec : HrScanRecord = {
|
||||
name: '一次性医用口罩', time: nowTime(), delta: '+1 盒', status: '已入库', theme: 'grn', icon: '✓'
|
||||
} as HrScanRecord
|
||||
|
||||
const fbArr : HrScanFeedback[] = []
|
||||
fbArr.push(fb)
|
||||
for (let i = 0; i < feedbacks.value.length; i++) {
|
||||
if (i < 2) { fbArr.push(feedbacks.value[i]) }
|
||||
}
|
||||
feedbacks.value = fbArr
|
||||
|
||||
const recArr : HrScanRecord[] = []
|
||||
recArr.push(rec)
|
||||
for (let i = 0; i < records.value.length; i++) {
|
||||
if (i < 4) { recArr.push(records.value[i]) }
|
||||
}
|
||||
records.value = recArr
|
||||
|
||||
uni.showToast({ title: '识别成功', icon: 'success' })
|
||||
}
|
||||
|
||||
function onScan(res : any) : void {
|
||||
const arr = res as string[]
|
||||
if (arr == null || arr.length == 0) { return }
|
||||
handleCode(arr[0])
|
||||
}
|
||||
|
||||
function feedbackClass(theme : string) : string {
|
||||
return `sc-st sc-st-${theme}`
|
||||
}
|
||||
|
||||
function feedbackColor(theme : string) : string {
|
||||
if (theme == 'ok') { return '#10B981' }
|
||||
if (theme == 'warn') { return '#F59E0B' }
|
||||
return '#DC2626'
|
||||
}
|
||||
|
||||
function onRecord(r : HrScanRecord) : void {
|
||||
uni.showModal({ title: r.name, content: r.time + ' · ' + r.delta + '\n状态:' + r.status, showCancel: false })
|
||||
}
|
||||
|
||||
function toggleFlash() : void {
|
||||
flash.value = !flash.value
|
||||
uni.showToast({ title: flash.value ? '已打开手电' : '已关闭手电', icon: 'none' })
|
||||
}
|
||||
|
||||
/*
|
||||
* 取景框扫描线
|
||||
* ⚠ App 端 uvue 不支持 @keyframes / animation(编译期直接报 ERROR),
|
||||
* 改为定时器逐帧推进 top:sc-frame 高 372rpx,16%~84% ⇒ 60~313rpx。
|
||||
* 周期 2.2s(68ms × 32 帧),与设计稿 hrScanMove 一致。
|
||||
*/
|
||||
const laserTop = ref(60)
|
||||
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(60 + k * 253)
|
||||
}, 68)
|
||||
}
|
||||
|
||||
function stopLaser() : void {
|
||||
if (laserTimer != 0) {
|
||||
clearInterval(laserTimer)
|
||||
laserTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
startLaser()
|
||||
})
|
||||
|
||||
onUnload(() => {
|
||||
stopLaser()
|
||||
})
|
||||
</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;
|
||||
}
|
||||
|
||||
/* ---------- 取景框 ---------- */
|
||||
.sc-box {
|
||||
width: 100%;
|
||||
height: 686rpx;
|
||||
background-color: #0F172A;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sc-frame {
|
||||
width: 372rpx;
|
||||
height: 372rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sc-cf {
|
||||
position: absolute;
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border: 6rpx solid #10B981;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.sc-cf-tl {
|
||||
top: 0rpx;
|
||||
left: 0rpx;
|
||||
border-right-width: 0rpx;
|
||||
border-bottom-width: 0rpx;
|
||||
}
|
||||
|
||||
.sc-cf-tr {
|
||||
top: 0rpx;
|
||||
right: 0rpx;
|
||||
border-left-width: 0rpx;
|
||||
border-bottom-width: 0rpx;
|
||||
}
|
||||
|
||||
.sc-cf-bl {
|
||||
bottom: 0rpx;
|
||||
left: 0rpx;
|
||||
border-right-width: 0rpx;
|
||||
border-top-width: 0rpx;
|
||||
}
|
||||
|
||||
.sc-cf-br {
|
||||
bottom: 0rpx;
|
||||
right: 0rpx;
|
||||
border-left-width: 0rpx;
|
||||
border-top-width: 0rpx;
|
||||
}
|
||||
|
||||
/* 扫描线:⚠ App 端 uvue 不支持 @keyframes/animation,
|
||||
上下位移已改为 script 内定时器推进 laserTop + laserStyle 下发 top。 */
|
||||
.sc-line {
|
||||
position: absolute;
|
||||
left: 9%;
|
||||
right: 9%;
|
||||
top: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: #10B981;
|
||||
}
|
||||
|
||||
.sc-tip {
|
||||
position: absolute;
|
||||
bottom: 24rpx;
|
||||
left: 0rpx;
|
||||
right: 0rpx;
|
||||
text-align: center;
|
||||
font-size: 25.8rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.sc-seg-t {
|
||||
font-size: 29.2rpx;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.sc-seg-t-on {
|
||||
color: #2563EB;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ---------- 三态反馈 ---------- */
|
||||
.sc-st {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 22rpx 24rpx 22rpx 24rpx;
|
||||
border-radius: 22rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.sc-st-ok {
|
||||
background-color: #ECFDF5;
|
||||
border: 2rpx solid #A7F3D0;
|
||||
}
|
||||
|
||||
.sc-st-warn {
|
||||
background-color: #FFFBEB;
|
||||
border: 2rpx solid #FDE68A;
|
||||
}
|
||||
|
||||
.sc-st-err {
|
||||
background-color: #FEF2F2;
|
||||
border: 2rpx solid #FECACA;
|
||||
}
|
||||
|
||||
.sc-st-ic {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.sc-st-ic-t {
|
||||
font-size: 29.2rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.sc-st-b {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sc-st-s {
|
||||
font-size: 23.6rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.sc-empty {
|
||||
align-items: center;
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.sc-empty-t {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #6B7280;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.sc-empty-s {
|
||||
font-size: 23.6rpx;
|
||||
color: #9CA3AF;
|
||||
margin-top: 8rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<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="onExport">
|
||||
<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">
|
||||
|
||||
<!-- 搜索(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="st-bar">
|
||||
<view class="st-cell">
|
||||
<text class="st-b">128</text>
|
||||
<text class="st-s">物资品类</text>
|
||||
</view>
|
||||
<view class="st-sep"></view>
|
||||
<view class="st-cell">
|
||||
<text class="st-b" style="color:#DC2626">6</text>
|
||||
<text class="st-s">低库存预警</text>
|
||||
</view>
|
||||
<view class="st-sep"></view>
|
||||
<view class="st-cell">
|
||||
<text class="st-b" style="color:#2563EB">14</text>
|
||||
<text class="st-s">借用中</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ============ 库存预警 ============ -->
|
||||
<hr-sec title="库存预警" :more="warnList.length + ' 项 ›'"></hr-sec>
|
||||
<hr-card variant="danger">
|
||||
<hr-lrow v-for="(w, i) in warnList" :key="i" :icon="w.icon" :theme="w.low ? 'red' : 'amb'"
|
||||
:title="w.name" :sub="w.spec" :value="w.value" :value-theme="w.low ? 'red' : 'amb'"
|
||||
:value-bold="true" :is-last="i == warnList.length - 1" @click="openGoods(w)"></hr-lrow>
|
||||
</hr-card>
|
||||
|
||||
<!-- ============ 全部物资 ============ -->
|
||||
<hr-sec title="全部物资" more="按库存排序 ›"></hr-sec>
|
||||
<hr-card variant="list">
|
||||
<hr-lrow v-for="(s, i) in shownAll" :key="i" :icon="s.icon"
|
||||
:theme="s.low ? 'amb' : 'pri'" :title="s.name" :sub="s.spec" :value="s.value"
|
||||
:value-theme="s.valueTheme" :value-bold="true" :value-size="'26rpx'"
|
||||
:is-last="i == shownAll.length - 1" @click="openGoods(s)"></hr-lrow>
|
||||
</hr-card>
|
||||
|
||||
<hr-empty v-if="shownAll.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="1"></hr-tabbar>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { stockWarn, stockAll } from '@/mock/index.uts'
|
||||
import { HrStockItem } from '@/mock/types.uts'
|
||||
import { navTo } from '@/utils/nav.uts'
|
||||
import { mockRequest } from '@/utils/request.uts'
|
||||
|
||||
/**
|
||||
* s24 库存查询(仓管 Tab「仓库」)
|
||||
* 库存预警自动置顶,支持关键字过滤。
|
||||
*/
|
||||
const loading = ref(true)
|
||||
const keyword = ref('')
|
||||
|
||||
/**
|
||||
* xSearch 的 border 是「数组」prop,固定 3 项 = [宽度, 线型, 颜色],
|
||||
* 少于 3 项内部直接返回 none。设计稿搜索框为 1px solid #E5E7EB。
|
||||
*/
|
||||
const searchBorder : string[] = ['1px', 'solid', '#E5E7EB']
|
||||
|
||||
onLoad(() => {
|
||||
mockRequest(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
|
||||
const warnList = ref<HrStockItem[]>(stockWarn)
|
||||
const allList = ref<HrStockItem[]>(stockAll)
|
||||
|
||||
const shownAll = computed(() : HrStockItem[] => {
|
||||
if (keyword.value == '') { return allList.value }
|
||||
const kw = keyword.value.toLowerCase()
|
||||
const arr : HrStockItem[] = []
|
||||
for (let i = 0; i < allList.value.length; i++) {
|
||||
if (allList.value[i].name.toLowerCase().indexOf(kw) > -1) { arr.push(allList.value[i]) }
|
||||
}
|
||||
return arr
|
||||
})
|
||||
|
||||
function openGoods(s : HrStockItem) : void {
|
||||
navTo('/pages/warehouse/goods-detail?name=' + s.name)
|
||||
}
|
||||
|
||||
function onExport() : void {
|
||||
uni.showToast({ title: '已生成库存清单 Excel', 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;
|
||||
}
|
||||
|
||||
.st-srch {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #FFFFFF;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
border-radius: 22rpx;
|
||||
height: 92rpx;
|
||||
padding: 0rpx 24rpx 0rpx 24rpx;
|
||||
}
|
||||
|
||||
.st-srch-input {
|
||||
flex: 1;
|
||||
font-size: 29.2rpx;
|
||||
color: #1F2937;
|
||||
height: 88rpx;
|
||||
padding: 0rpx 18rpx 0rpx 18rpx;
|
||||
}
|
||||
|
||||
.st-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #FFFFFF;
|
||||
border: 2rpx solid #E5E7EB;
|
||||
border-radius: 26rpx;
|
||||
padding: 26rpx 8rpx 26rpx 8rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.st-cell {
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.st-b {
|
||||
font-size: 44.8rpx;
|
||||
font-weight: bold;
|
||||
color: #1F2937;
|
||||
}
|
||||
|
||||
.st-s {
|
||||
font-size: 23.6rpx;
|
||||
color: #6B7280;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.st-sep {
|
||||
width: 2rpx;
|
||||
height: 60rpx;
|
||||
background-color: #F3F4F6;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user