refactor: 移除UI项目中的mobile代码,准备独立移动端项目
This commit is contained in:
@@ -1,439 +0,0 @@
|
||||
<template>
|
||||
<MobileLayout :title="patientName + ' - 护理评估'">
|
||||
<div class="assessment-form">
|
||||
<div class="patient-bar">
|
||||
<span>{{ patientName }}</span>
|
||||
<span class="bed">{{ bedName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-tabs">
|
||||
<div
|
||||
v-for="tool in tools"
|
||||
:key="tool.key"
|
||||
class="tool-tab"
|
||||
:class="{ active: selectedTool === tool.key }"
|
||||
@click="selectTool(tool.key)"
|
||||
>
|
||||
{{ tool.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-indicator">
|
||||
<span class="step-text">第 {{ currentStep + 1 }} / {{ currentItems.length }} 项</span>
|
||||
<div class="step-bar">
|
||||
<div class="step-fill" :style="{ width: stepProgress + '%' }" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="question-card">
|
||||
<div class="question-title">{{ currentItem?.label }}</div>
|
||||
<div class="question-options">
|
||||
<div
|
||||
v-for="opt in currentItem?.options"
|
||||
:key="opt.value"
|
||||
class="option-btn"
|
||||
:class="{ selected: itemScores[currentItem.key] === opt.value }"
|
||||
@click="selectOption(currentItem.key, opt.value)"
|
||||
>
|
||||
<span class="option-label">{{ opt.label }}</span>
|
||||
<span class="option-score">{{ opt.value }}分</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-buttons">
|
||||
<el-button
|
||||
:disabled="currentStep === 0"
|
||||
round
|
||||
@click="prevStep"
|
||||
>
|
||||
上一项
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="currentStep < currentItems.length - 1"
|
||||
type="primary"
|
||||
round
|
||||
:disabled="itemScores[currentItem.key] == null"
|
||||
@click="nextStep"
|
||||
>
|
||||
下一项
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
round
|
||||
:disabled="!allFilled"
|
||||
@click="handleSubmit"
|
||||
:loading="submitting"
|
||||
>
|
||||
提交评估
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="score-display">
|
||||
<div class="score-total">
|
||||
<span class="score-label">当前总分</span>
|
||||
<span class="score-value">{{ totalScore }}</span>
|
||||
</div>
|
||||
<div class="risk-display">
|
||||
<span class="risk-label">风险等级</span>
|
||||
<el-tag :type="getRiskType(currentRiskLevel)" size="large" effect="dark">
|
||||
{{ getRiskText(currentRiskLevel) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MobileLayout from './MobileLayout.vue'
|
||||
import { submitAssessment } from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const submitting = ref(false)
|
||||
|
||||
const patientId = ref(route.query.patientId)
|
||||
const encounterId = ref(route.query.encounterId)
|
||||
const patientName = ref(route.query.patientName || '')
|
||||
const bedName = ref(route.query.bedName || '')
|
||||
|
||||
const tools = [
|
||||
{ key: 'BRADEN', label: 'Braden压疮' },
|
||||
{ key: 'MORSE', label: 'Morse跌倒' },
|
||||
{ key: 'NRS2002', label: 'NRS2002营养' }
|
||||
]
|
||||
|
||||
const bradenItems = [
|
||||
{ key: 'sensory', label: '感觉感知', options: [
|
||||
{ label: '完全受限', value: 1 }, { label: '严重受限', value: 2 },
|
||||
{ label: '轻度受限', value: 3 }, { label: '未受损', value: 4 }
|
||||
]},
|
||||
{ key: 'moisture', label: '潮湿程度', options: [
|
||||
{ label: '持续潮湿', value: 1 }, { label: '经常潮湿', value: 2 },
|
||||
{ label: '有时潮湿', value: 3 }, { label: '很少潮湿', value: 4 }
|
||||
]},
|
||||
{ key: 'activity', label: '活动能力', options: [
|
||||
{ label: '卧床不起', value: 1 }, { label: '仅限于椅', value: 2 },
|
||||
{ label: '偶尔步行', value: 3 }, { label: '经常步行', value: 4 }
|
||||
]},
|
||||
{ key: 'mobility', label: '移动能力', options: [
|
||||
{ label: '完全不动', value: 1 }, { label: '严重受限', value: 2 },
|
||||
{ label: '轻度受限', value: 3 }, { label: '不受限', value: 4 }
|
||||
]},
|
||||
{ key: 'nutrition', label: '营养摄取', options: [
|
||||
{ label: '非常差', value: 1 }, { label: '可能不足', value: 2 },
|
||||
{ label: '充足', value: 3 }, { label: '丰富', value: 4 }
|
||||
]},
|
||||
{ key: 'friction', label: '摩擦力和剪切力', options: [
|
||||
{ label: '存在问题', value: 1 }, { label: '有潜在问题', value: 2 },
|
||||
{ label: '无明显问题', value: 3 }
|
||||
]}
|
||||
]
|
||||
|
||||
const morseItems = [
|
||||
{ key: 'fallHistory', label: '跌倒史', options: [
|
||||
{ label: '无', value: 0 }, { label: '有', value: 25 }
|
||||
]},
|
||||
{ key: 'diagnosis', label: '超过一个医学诊断', options: [
|
||||
{ label: '无', value: 0 }, { label: '有', value: 15 }
|
||||
]},
|
||||
{ key: 'ambulation', label: '行走辅助', options: [
|
||||
{ label: '卧床/轮椅', value: 0 }, { label: '拐杖/助行器', value: 15 },
|
||||
{ label: '扶家具', value: 30 }
|
||||
]},
|
||||
{ key: 'ivTherapy', label: '静脉输液', options: [
|
||||
{ label: '无', value: 0 }, { label: '有', value: 20 }
|
||||
]},
|
||||
{ key: 'gait', label: '步态', options: [
|
||||
{ label: '正常/卧床/轮椅', value: 0 }, { label: '虚弱', value: 10 },
|
||||
{ label: '受损', value: 20 }
|
||||
]},
|
||||
{ key: 'mental', label: '精神状态', options: [
|
||||
{ label: '正确评估自身能力', value: 0 }, { label: '高估或忘记限制', value: 15 }
|
||||
]}
|
||||
]
|
||||
|
||||
const nrs2002Items = [
|
||||
{ key: 'bmi', label: 'BMI(kg/m²)', options: [
|
||||
{ label: '<18.5', value: 3 }, { label: '18.5-20.5', value: 1 },
|
||||
{ label: '>20.5', value: 0 }
|
||||
]},
|
||||
{ key: 'weightLoss', label: '体重下降', options: [
|
||||
{ label: '>10%/月', value: 3 }, { label: '5-10%/月', value: 2 },
|
||||
{ label: '2-5%/月', value: 1 }, { label: '无/1月', value: 0 }
|
||||
]},
|
||||
{ key: 'intake', label: '饮食摄入', options: [
|
||||
{ label: '无', value: 3 }, { label: '差', value: 2 },
|
||||
{ label: '中等', value: 1 }, { label: '好', value: 0 }
|
||||
]},
|
||||
{ key: 'illness', label: '疾病严重程度', options: [
|
||||
{ label: '大手术/创伤', value: 3 }, { label: '骨盆骨折', value: 2 },
|
||||
{ label: '慢性病急性发作', value: 1 }, { label: '无/轻度', value: 0 }
|
||||
]}
|
||||
]
|
||||
|
||||
const itemMap = { BRADEN: bradenItems, MORSE: morseItems, NRS2002: nrs2002Items }
|
||||
const itemScores = reactive({})
|
||||
const selectedTool = ref('BRADEN')
|
||||
const currentStep = ref(0)
|
||||
|
||||
const currentItems = computed(() => itemMap[selectedTool.value])
|
||||
const currentItem = computed(() => currentItems.value[currentStep.value])
|
||||
const stepProgress = computed(() => ((currentStep.value + 1) / currentItems.value.length) * 100)
|
||||
|
||||
const allFilled = computed(() => {
|
||||
return currentItems.value.every(i => itemScores[i.key] != null)
|
||||
})
|
||||
|
||||
const totalScore = computed(() => {
|
||||
let sum = 0
|
||||
for (const item of currentItems.value) {
|
||||
sum += itemScores[item.key] || 0
|
||||
}
|
||||
return sum
|
||||
})
|
||||
|
||||
const currentRiskLevel = computed(() => {
|
||||
const tool = selectedTool.value
|
||||
const score = totalScore.value
|
||||
if (tool === 'BRADEN') {
|
||||
if (score <= 12) return 'HIGH'
|
||||
if (score <= 14) return 'MEDIUM'
|
||||
return 'LOW'
|
||||
} else if (tool === 'MORSE') {
|
||||
if (score >= 45) return 'HIGH'
|
||||
if (score >= 25) return 'MEDIUM'
|
||||
return 'LOW'
|
||||
} else if (tool === 'NRS2002') {
|
||||
if (score >= 3) return 'HIGH'
|
||||
return 'LOW'
|
||||
}
|
||||
return 'NORMAL'
|
||||
})
|
||||
|
||||
const selectTool = (key) => {
|
||||
selectedTool.value = key
|
||||
currentStep.value = 0
|
||||
for (const k of Object.keys(itemScores)) {
|
||||
delete itemScores[k]
|
||||
}
|
||||
}
|
||||
|
||||
const selectOption = (key, value) => {
|
||||
itemScores[key] = value
|
||||
}
|
||||
|
||||
const prevStep = () => {
|
||||
if (currentStep.value > 0) currentStep.value--
|
||||
}
|
||||
|
||||
const nextStep = () => {
|
||||
if (itemScores[currentItem.value.key] != null && currentStep.value < currentItems.value.length - 1) {
|
||||
currentStep.value++
|
||||
}
|
||||
}
|
||||
|
||||
const getRiskType = (level) => {
|
||||
const map = { HIGH: 'danger', MEDIUM: 'warning', LOW: 'success', NORMAL: 'info' }
|
||||
return map[level] || 'info'
|
||||
}
|
||||
|
||||
const getRiskText = (level) => {
|
||||
const map = { HIGH: '高风险', MEDIUM: '中风险', LOW: '低风险', NORMAL: '正常' }
|
||||
return map[level] || '未知'
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!allFilled.value) {
|
||||
ElMessage.warning('请完成所有评估项目')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await submitAssessment({
|
||||
patientId: patientId.value ? Number(patientId.value) : null,
|
||||
encounterId: encounterId.value ? Number(encounterId.value) : null,
|
||||
patientName: patientName.value,
|
||||
assessmentType: 'MOBILE_H5',
|
||||
assessmentTool: selectedTool.value,
|
||||
totalScore: totalScore.value,
|
||||
itemScores: { ...itemScores },
|
||||
assessmentTime: new Date()
|
||||
})
|
||||
ElMessage.success('评估提交成功')
|
||||
router.back()
|
||||
} catch (e) {
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.assessment-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.patient-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bed {
|
||||
font-size: 13px;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.tool-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tool-tab {
|
||||
flex: 1;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--mobile-text-secondary);
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tool-tab.active {
|
||||
background: var(--mobile-primary);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.step-indicator {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.step-bar {
|
||||
height: 4px;
|
||||
background: #f0f2f5;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.step-fill {
|
||||
height: 100%;
|
||||
background: var(--mobile-primary);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.question-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.option-btn {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid var(--mobile-border);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
background: var(--mobile-card);
|
||||
}
|
||||
|
||||
.option-btn.selected {
|
||||
border-color: var(--mobile-primary);
|
||||
background: #ecf5ff;
|
||||
}
|
||||
|
||||
.option-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.option-label {
|
||||
font-size: 14px;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.option-score {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-info);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-buttons .el-button {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.score-display {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.score-label, .risk-label {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--mobile-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,197 +0,0 @@
|
||||
<template>
|
||||
<div class="mobile-layout">
|
||||
<div class="mobile-header" v-if="showHeader">
|
||||
<div class="header-left" @click="goBack">
|
||||
<el-icon :size="20"><ArrowLeft /></el-icon>
|
||||
</div>
|
||||
<div class="header-title">{{ title }}</div>
|
||||
<div class="header-right"><slot name="header-right" /></div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-content" :class="{ 'has-header': showHeader, 'has-tabs': showTabs }">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="showTabs" class="mobile-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="switchTab(tab)"
|
||||
>
|
||||
<el-icon :size="22"><component :is="tab.icon" /></el-icon>
|
||||
<span class="tab-label">{{ tab.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ArrowLeft, List, User, Document, Setting } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '' },
|
||||
showHeader: { type: Boolean, default: true },
|
||||
showTabs: { type: Boolean, default: true },
|
||||
activeTab: { type: String, default: 'tasks' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['switch-tab'])
|
||||
const router = useRouter()
|
||||
|
||||
const tabs = [
|
||||
{ key: 'tasks', label: '任务', icon: List, path: '/mobile/tasks' },
|
||||
{ key: 'patients', label: '患者', icon: User, path: '/mobile/patients' },
|
||||
{ key: 'assess', label: '评估', icon: Document, path: '/mobile/assess' },
|
||||
{ key: 'mine', label: '我的', icon: Setting, path: '/mobile/mine' }
|
||||
]
|
||||
|
||||
const goBack = () => {
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
router.push('/mobile/tasks')
|
||||
}
|
||||
}
|
||||
|
||||
const switchTab = (tab) => {
|
||||
emit('switch-tab', tab.key)
|
||||
router.push(tab.path)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--mobile-primary: #409eff;
|
||||
--mobile-success: #67c23a;
|
||||
--mobile-warning: #e6a23c;
|
||||
--mobile-danger: #f56c6c;
|
||||
--mobile-info: #909399;
|
||||
--mobile-bg: #f5f7fa;
|
||||
--mobile-card: #ffffff;
|
||||
--mobile-text: #303133;
|
||||
--mobile-text-secondary: #606266;
|
||||
--mobile-text-placeholder: #c0c4cc;
|
||||
--mobile-border: #ebeef5;
|
||||
--mobile-header-height: 44px;
|
||||
--mobile-tab-height: 50px;
|
||||
--mobile-safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
.mobile-layout {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--mobile-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-header {
|
||||
height: var(--mobile-header-height);
|
||||
min-height: var(--mobile-header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
background: var(--mobile-card);
|
||||
border-bottom: 1px solid var(--mobile-border);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--mobile-primary);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.mobile-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.mobile-content.has-header {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.mobile-content.has-tabs {
|
||||
padding-bottom: calc(var(--mobile-tab-height) + var(--mobile-safe-bottom) + 8px);
|
||||
}
|
||||
|
||||
.mobile-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: calc(var(--mobile-tab-height) + var(--mobile-safe-bottom));
|
||||
padding-bottom: var(--mobile-safe-bottom);
|
||||
background: var(--mobile-card);
|
||||
border-top: 1px solid var(--mobile-border);
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
color: var(--mobile-info);
|
||||
transition: color 0.2s;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: var(--mobile-primary);
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,509 +0,0 @@
|
||||
<template>
|
||||
<MobileLayout :title="patientName + ' - ' + activeTabLabel">
|
||||
<div class="patient-detail">
|
||||
<div class="patient-info-bar">
|
||||
<span class="info-name">{{ patientName }}</span>
|
||||
<span class="info-bed">{{ bedName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="detail-tabs">
|
||||
<div
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
class="detail-tab"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
@click="activeTab = tab.key"
|
||||
>
|
||||
{{ tab.label }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'orders'" class="tab-content">
|
||||
<div v-for="order in orders" :key="order.requestId" class="order-card">
|
||||
<div class="order-row">
|
||||
<span class="order-name">{{ order.adviceName }}</span>
|
||||
<el-tag :type="getOrderStatusType(order.requestStatus)" size="small">
|
||||
{{ order.requestStatusText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="order-row sub">
|
||||
<span>{{ order.therapyEnumText }}</span>
|
||||
<span v-if="order.frequencyUsage">{{ order.frequencyUsage }}</span>
|
||||
<span v-if="order.singleDose">{{ order.singleDose }}</span>
|
||||
</div>
|
||||
<div class="order-actions">
|
||||
<el-button
|
||||
v-if="order.requestStatus === 2 || order.requestStatus === 10"
|
||||
type="success"
|
||||
size="small"
|
||||
round
|
||||
@click="handleOrderExecute(order)"
|
||||
>
|
||||
执行
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="orders.length === 0" description="暂无医嘱" />
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'vitals'" class="tab-content">
|
||||
<div class="vital-grid">
|
||||
<div class="vital-item" @click="goVitalEntry('temperature')">
|
||||
<div class="vital-value">{{ latestVitals.temperature || '-' }}</div>
|
||||
<div class="vital-unit">°C</div>
|
||||
<div class="vital-label">体温</div>
|
||||
</div>
|
||||
<div class="vital-item" @click="goVitalEntry('pulse')">
|
||||
<div class="vital-value">{{ latestVitals.pulse || '-' }}</div>
|
||||
<div class="vital-unit">次/分</div>
|
||||
<div class="vital-label">脉搏</div>
|
||||
</div>
|
||||
<div class="vital-item" @click="goVitalEntry('bp')">
|
||||
<div class="vital-value">
|
||||
{{ latestVitals.systolicBp || '-' }}/{{ latestVitals.diastolicBp || '-' }}
|
||||
</div>
|
||||
<div class="vital-unit">mmHg</div>
|
||||
<div class="vital-label">血压</div>
|
||||
</div>
|
||||
<div class="vital-item" @click="goVitalEntry('spo2')">
|
||||
<div class="vital-value">{{ latestVitals.spo2 || '-' }}</div>
|
||||
<div class="vital-unit">%</div>
|
||||
<div class="vital-label">血氧</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
round
|
||||
class="entry-btn"
|
||||
@click="goVitalEntry('all')"
|
||||
>
|
||||
录入生命体征
|
||||
</el-button>
|
||||
<div class="trend-section">
|
||||
<div class="section-title">近期趋势</div>
|
||||
<div class="mini-trend">
|
||||
<div v-for="(point, idx) in tempTrend" :key="idx" class="trend-point">
|
||||
<div class="trend-bar" :style="{ height: getBarHeight(point.value, 35, 42) + 'px' }" />
|
||||
<div class="trend-val">{{ point.value }}</div>
|
||||
</div>
|
||||
<div v-if="tempTrend.length === 0" class="no-trend">暂无趋势数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'assessments'" class="tab-content">
|
||||
<el-button
|
||||
type="primary"
|
||||
round
|
||||
class="entry-btn"
|
||||
@click="goAssessForm"
|
||||
>
|
||||
新建评估
|
||||
</el-button>
|
||||
<div v-for="record in assessments" :key="record.id" class="assess-card">
|
||||
<div class="assess-row">
|
||||
<span class="assess-tool">{{ getToolName(record.assessmentTool) }}</span>
|
||||
<el-tag :type="getRiskType(record.riskLevel)" size="small">
|
||||
{{ getRiskText(record.riskLevel) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="assess-row sub">
|
||||
<span>评分: {{ record.totalScore }}</span>
|
||||
<span>{{ formatTime(record.assessmentTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="assessments.length === 0" description="暂无评估记录" />
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'records'" class="tab-content">
|
||||
<div v-for="record in records" :key="record.id" class="record-card">
|
||||
<div class="record-time">{{ formatTime(record.recordTime) }}</div>
|
||||
<div class="record-content">{{ record.content }}</div>
|
||||
<div class="record-author">{{ record.nurseName }}</div>
|
||||
</div>
|
||||
<el-empty v-if="records.length === 0" description="暂无病历记录" />
|
||||
</div>
|
||||
</div>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MobileLayout from './MobileLayout.vue'
|
||||
import {
|
||||
getPatientDetail, getPatientOrders, executeOrder,
|
||||
getVitalSignTrend, getAssessmentList, getPatientRecords
|
||||
} from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const patientId = ref(route.query.patientId)
|
||||
const encounterId = ref(route.query.encounterId)
|
||||
const patientName = ref(route.query.patientName || '')
|
||||
const bedName = ref(route.query.bedName || '')
|
||||
|
||||
const activeTab = ref('orders')
|
||||
const tabs = [
|
||||
{ key: 'orders', label: '医嘱' },
|
||||
{ key: 'vitals', label: '体征' },
|
||||
{ key: 'assessments', label: '评估' },
|
||||
{ key: 'records', label: '病历' }
|
||||
]
|
||||
|
||||
const activeTabLabel = computed(() => {
|
||||
return tabs.find(t => t.key === activeTab.value)?.label || ''
|
||||
})
|
||||
|
||||
const orders = ref([])
|
||||
const latestVitals = ref({})
|
||||
const tempTrend = ref([])
|
||||
const assessments = ref([])
|
||||
const records = ref([])
|
||||
|
||||
const fetchOrders = async () => {
|
||||
if (!patientId.value) return
|
||||
const res = await getPatientOrders(patientId.value)
|
||||
orders.value = res.data || []
|
||||
}
|
||||
|
||||
const fetchVitals = async () => {
|
||||
if (!patientId.value) return
|
||||
const res = await getVitalSignTrend(patientId.value, { days: 3 })
|
||||
latestVitals.value = res.data?.latest || {}
|
||||
tempTrend.value = res.data?.temperatureData || []
|
||||
}
|
||||
|
||||
const fetchAssessments = async () => {
|
||||
if (!patientId.value) return
|
||||
const res = await getAssessmentList(patientId.value)
|
||||
assessments.value = (res.data || []).slice(0, 20)
|
||||
}
|
||||
|
||||
const fetchRecords = async () => {
|
||||
if (!patientId.value) return
|
||||
const res = await getPatientRecords(patientId.value)
|
||||
records.value = res.data || []
|
||||
}
|
||||
|
||||
const handleOrderExecute = async (order) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认执行: ${order.adviceName}?`,
|
||||
'确认执行',
|
||||
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
await executeOrder({
|
||||
requestId: order.requestId,
|
||||
adviceTable: order.adviceTable,
|
||||
encounterId: order.encounterId,
|
||||
patientId: order.patientId
|
||||
})
|
||||
ElMessage.success('执行成功')
|
||||
fetchOrders()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error('执行失败')
|
||||
}
|
||||
}
|
||||
|
||||
const goVitalEntry = (type) => {
|
||||
router.push({
|
||||
path: '/mobile/vital-entry',
|
||||
query: {
|
||||
patientId: patientId.value,
|
||||
encounterId: encounterId.value,
|
||||
patientName: patientName.value,
|
||||
bedName: bedName.value,
|
||||
vitalType: type
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goAssessForm = () => {
|
||||
router.push({
|
||||
path: '/mobile/assessment',
|
||||
query: {
|
||||
patientId: patientId.value,
|
||||
encounterId: encounterId.value,
|
||||
patientName: patientName.value,
|
||||
bedName: bedName.value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getOrderStatusType = (status) => {
|
||||
const map = { 2: 'primary', 3: 'success', 6: 'info', 10: 'warning' }
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const getBarHeight = (value, min, max) => {
|
||||
if (!value) return 10
|
||||
const normalized = (value - min) / (max - min)
|
||||
return Math.max(10, Math.min(60, normalized * 60))
|
||||
}
|
||||
|
||||
const getToolName = (tool) => {
|
||||
const map = { BRADEN: 'Braden压疮', MORSE: 'Morse跌倒', NRS2002: 'NRS2002营养' }
|
||||
return map[tool] || tool
|
||||
}
|
||||
|
||||
const getRiskType = (level) => {
|
||||
const map = { HIGH: 'danger', MEDIUM: 'warning', LOW: 'success', NORMAL: 'info' }
|
||||
return map[level] || 'info'
|
||||
}
|
||||
|
||||
const getRiskText = (level) => {
|
||||
const map = { HIGH: '高风险', MEDIUM: '中风险', LOW: '低风险', NORMAL: '正常' }
|
||||
return map[level] || '未知'
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '-'
|
||||
const d = new Date(time)
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
fetchVitals()
|
||||
fetchAssessments()
|
||||
fetchRecords()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.patient-detail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.patient-info-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.info-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.info-bed {
|
||||
font-size: 13px;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 4px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
flex: 1;
|
||||
min-width: 60px;
|
||||
padding: 8px 0;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--mobile-text-secondary);
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-tab.active {
|
||||
background: var(--mobile-primary);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.order-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.order-row.sub {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.order-actions {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.vital-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vital-item {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.vital-item:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.vital-value {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.vital-unit {
|
||||
font-size: 11px;
|
||||
color: var(--mobile-info);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.vital-label {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.entry-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.trend-section {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mini-trend {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
min-height: 80px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.trend-point {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.trend-bar {
|
||||
width: 16px;
|
||||
background: var(--mobile-primary);
|
||||
border-radius: 4px 4px 0 0;
|
||||
min-height: 10px;
|
||||
}
|
||||
|
||||
.trend-val {
|
||||
font-size: 10px;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.no-trend {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: var(--mobile-info);
|
||||
padding: 30px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.assess-card {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.assess-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.assess-row.sub {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.assess-tool {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.record-card {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-info);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.record-content {
|
||||
font-size: 14px;
|
||||
color: var(--mobile-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.record-author {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-top: 6px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -1,244 +0,0 @@
|
||||
<template>
|
||||
<MobileLayout title="患者列表" active-tab="patients" @switch-tab="onTabSwitch">
|
||||
<div class="patient-list">
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
placeholder="搜索姓名/床号"
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
@input="handleSearch"
|
||||
class="search-input"
|
||||
/>
|
||||
<el-select v-model="filterLevel" placeholder="护理等级" clearable class="filter-select" @change="handleSearch">
|
||||
<el-option label="特级" :value="1" />
|
||||
<el-option label="一级" :value="2" />
|
||||
<el-option label="二级" :value="3" />
|
||||
<el-option label="三级" :value="4" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="patient-cards">
|
||||
<div
|
||||
v-for="patient in patientList"
|
||||
:key="patient.encounterId"
|
||||
class="patient-card"
|
||||
@click="goDetail(patient)"
|
||||
>
|
||||
<div class="card-avatar">
|
||||
<span class="avatar-text">{{ (patient.patientName || '').slice(-1) }}</span>
|
||||
<el-tag
|
||||
:type="patient.genderEnum === 1 ? '' : 'danger'"
|
||||
size="small"
|
||||
class="gender-tag"
|
||||
>
|
||||
{{ patient.genderEnum === 1 ? '男' : '女' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-row main">
|
||||
<span class="patient-name">{{ patient.patientName }}</span>
|
||||
<span class="bed-name">{{ patient.bedName || '-' }}</span>
|
||||
</div>
|
||||
<div class="card-row">
|
||||
<el-tag
|
||||
:type="getNursingLevelType(patient.nursingLevel)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ patient.nursingLevelText || '普通' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="patient.priorityEnum"
|
||||
:type="getPriorityType(patient.priorityEnum)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ patient.priorityEnumText }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div v-if="patient.diagnosis" class="card-row diagnosis">
|
||||
{{ patient.diagnosis }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-arrow">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!loading && patientList.length === 0" description="暂无患者" />
|
||||
</div>
|
||||
</div>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search, ArrowRight } from '@element-plus/icons-vue'
|
||||
import MobileLayout from './MobileLayout.vue'
|
||||
import { getMobilePatientList } from './api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const searchKey = ref('')
|
||||
const filterLevel = ref(null)
|
||||
const patientList = ref([])
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { searchKey: searchKey.value }
|
||||
if (filterLevel.value) params.nursingLevel = filterLevel.value
|
||||
const res = await getMobilePatientList(params)
|
||||
patientList.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const goDetail = (patient) => {
|
||||
router.push({
|
||||
path: '/mobile/patient-detail',
|
||||
query: {
|
||||
patientId: patient.patientId,
|
||||
encounterId: patient.encounterId,
|
||||
patientName: patient.patientName,
|
||||
bedName: patient.bedName
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getNursingLevelType = (level) => {
|
||||
const map = { 1: 'danger', 2: 'warning', 3: '', 4: 'danger' }
|
||||
return map[level] || 'info'
|
||||
}
|
||||
|
||||
const getPriorityType = (priority) => {
|
||||
const map = { 1: 'danger', 2: 'warning', 3: '', 4: 'info' }
|
||||
return map[priority] || 'info'
|
||||
}
|
||||
|
||||
const onTabSwitch = () => {}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.patient-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.patient-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.patient-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.patient-card:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.card-avatar {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
min-width: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-text {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gender-tag {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
font-size: 10px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
height: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-row.main {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.patient-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.bed-name {
|
||||
font-size: 13px;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.card-row.diagnosis {
|
||||
font-size: 12px;
|
||||
color: var(--mobile-warning);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-arrow {
|
||||
color: var(--mobile-text-placeholder);
|
||||
}
|
||||
</style>
|
||||
@@ -1,228 +0,0 @@
|
||||
<template>
|
||||
<MobileLayout title="护理工作站" active-tab="tasks" @switch-tab="onTabSwitch">
|
||||
<div class="task-list">
|
||||
<div class="pull-tip" v-if="refreshing">刷新中...</div>
|
||||
|
||||
<div class="task-group" v-for="group in groupedTasks" :key="group.type">
|
||||
<div class="group-header">
|
||||
<span class="group-title">{{ group.label }}</span>
|
||||
<el-badge :value="group.items.length" :max="99" type="primary" />
|
||||
</div>
|
||||
<div
|
||||
v-for="task in group.items"
|
||||
:key="task.id"
|
||||
class="task-card"
|
||||
@touchstart="touchStart($event, task)"
|
||||
@touchend="touchEnd($event, task)"
|
||||
>
|
||||
<div class="task-info" :class="{ 'swiped': swipedTaskId === task.id }">
|
||||
<div class="task-top">
|
||||
<span class="task-name">{{ task.taskName }}</span>
|
||||
<el-tag :type="getTaskType(task.taskType)" size="small">
|
||||
{{ getTaskTypeText(task.taskType) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="task-meta">
|
||||
<span>{{ task.patientName }} ({{ task.bedName }})</span>
|
||||
<span class="task-time">{{ formatTime(task.scheduledTime) }}</span>
|
||||
</div>
|
||||
<div v-if="task.remark" class="task-remark">{{ task.remark }}</div>
|
||||
</div>
|
||||
<div class="task-action" v-if="swipedTaskId === task.id">
|
||||
<el-button type="success" size="small" @click.stop="completeTask(task)">
|
||||
完成
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!loading && allTasks.length === 0" description="暂无待办任务" />
|
||||
</div>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import MobileLayout from './MobileLayout.vue'
|
||||
import { getMobileTaskList, completeTask as apiCompleteTask } from './api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const allTasks = ref([])
|
||||
const swipedTaskId = ref(null)
|
||||
const touchStartX = ref(0)
|
||||
|
||||
const groupedTasks = computed(() => {
|
||||
const groups = [
|
||||
{ type: 'ORDER', label: '医嘱执行', items: [] },
|
||||
{ type: 'VITAL', label: '生命体征', items: [] },
|
||||
{ type: 'ASSESSMENT', label: '护理评估', items: [] }
|
||||
]
|
||||
for (const task of allTasks.value) {
|
||||
const g = groups.find(gr => gr.type === task.taskType)
|
||||
if (g) g.items.push(task)
|
||||
}
|
||||
return groups.filter(g => g.items.length > 0)
|
||||
})
|
||||
|
||||
const fetchTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMobileTaskList()
|
||||
allTasks.value = res.data || []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const touchStart = (e, task) => {
|
||||
touchStartX.value = e.touches[0].clientX
|
||||
swipedTaskId.value = null
|
||||
}
|
||||
|
||||
const touchEnd = (e, task) => {
|
||||
const dx = e.changedTouches[0].clientX - touchStartX.value
|
||||
if (dx < -50) {
|
||||
swipedTaskId.value = task.id
|
||||
} else {
|
||||
swipedTaskId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const completeTask = async (task) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认完成: ${task.taskName}?`,
|
||||
'确认操作',
|
||||
{ confirmButtonText: '确认', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
loading.value = true
|
||||
await apiCompleteTask({ taskId: task.id })
|
||||
ElMessage.success('任务已完成')
|
||||
swipedTaskId.value = null
|
||||
fetchTasks()
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') ElMessage.error('操作失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getTaskType = (type) => {
|
||||
const map = { ORDER: '', VITAL: 'success', ASSESSMENT: 'warning' }
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
const getTaskTypeText = (type) => {
|
||||
const map = { ORDER: '医嘱', VITAL: '体征', ASSESSMENT: '评估' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return ''
|
||||
const d = new Date(time)
|
||||
return `${d.getMonth() + 1}/${d.getDate()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const onTabSwitch = (key) => {}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTasks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pull-tip {
|
||||
text-align: center;
|
||||
color: var(--mobile-info);
|
||||
font-size: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.task-group {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.task-card {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.task-info {
|
||||
flex: 1;
|
||||
padding: 12px 14px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.task-info.swiped {
|
||||
transform: translateX(-70px);
|
||||
}
|
||||
|
||||
.task-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.task-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--mobile-text);
|
||||
}
|
||||
|
||||
.task-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.task-time {
|
||||
color: var(--mobile-info);
|
||||
}
|
||||
|
||||
.task-remark {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--mobile-info);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.task-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
background: var(--mobile-success);
|
||||
min-width: 70px;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,392 +0,0 @@
|
||||
<template>
|
||||
<MobileLayout :title="patientName + ' - 生命体征'">
|
||||
<div class="vital-entry">
|
||||
<div class="patient-bar">
|
||||
<span>{{ patientName }}</span>
|
||||
<span class="bed">{{ bedName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="vital-form">
|
||||
<div class="vital-group">
|
||||
<div class="group-title">体温</div>
|
||||
<div class="big-input-row">
|
||||
<el-input-number
|
||||
v-model="form.temperature"
|
||||
:min="35"
|
||||
:max="42"
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
size="large"
|
||||
controls-position="right"
|
||||
class="big-input"
|
||||
/>
|
||||
<span class="unit">°C</span>
|
||||
</div>
|
||||
<div class="quick-values">
|
||||
<span
|
||||
v-for="v in [36.0, 36.5, 36.8, 37.0, 37.3, 37.5]"
|
||||
:key="v"
|
||||
class="quick-btn"
|
||||
:class="{ active: form.temperature === v }"
|
||||
@click="form.temperature = v"
|
||||
>
|
||||
{{ v }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vital-group">
|
||||
<div class="group-title">脉搏</div>
|
||||
<div class="big-input-row">
|
||||
<el-input-number
|
||||
v-model="form.pulse"
|
||||
:min="40"
|
||||
:max="200"
|
||||
size="large"
|
||||
controls-position="right"
|
||||
class="big-input"
|
||||
/>
|
||||
<span class="unit">次/分</span>
|
||||
</div>
|
||||
<div class="quick-values">
|
||||
<span
|
||||
v-for="v in [60, 70, 80, 90, 100]"
|
||||
:key="v"
|
||||
class="quick-btn"
|
||||
:class="{ active: form.pulse === v }"
|
||||
@click="form.pulse = v"
|
||||
>
|
||||
{{ v }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vital-group">
|
||||
<div class="group-title">血压</div>
|
||||
<div class="big-input-row bp-row">
|
||||
<el-input-number
|
||||
v-model="form.systolicBp"
|
||||
:min="60"
|
||||
:max="300"
|
||||
size="large"
|
||||
controls-position="right"
|
||||
class="big-input"
|
||||
/>
|
||||
<span class="bp-sep">/</span>
|
||||
<el-input-number
|
||||
v-model="form.diastolicBp"
|
||||
:min="30"
|
||||
:max="200"
|
||||
size="large"
|
||||
controls-position="right"
|
||||
class="big-input"
|
||||
/>
|
||||
<span class="unit">mmHg</span>
|
||||
</div>
|
||||
<div class="quick-values">
|
||||
<span
|
||||
v-for="v in ['120/80', '130/85', '140/90', '100/60']"
|
||||
:key="v"
|
||||
class="quick-btn"
|
||||
@click="setBp(v)"
|
||||
>
|
||||
{{ v }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vital-group">
|
||||
<div class="group-title">血氧饱和度</div>
|
||||
<div class="big-input-row">
|
||||
<el-input-number
|
||||
v-model="form.spo2"
|
||||
:min="70"
|
||||
:max="100"
|
||||
size="large"
|
||||
controls-position="right"
|
||||
class="big-input"
|
||||
/>
|
||||
<span class="unit">%</span>
|
||||
</div>
|
||||
<div class="quick-values">
|
||||
<span
|
||||
v-for="v in [95, 96, 97, 98, 99, 100]"
|
||||
:key="v"
|
||||
class="quick-btn"
|
||||
:class="{ active: form.spo2 === v }"
|
||||
@click="form.spo2 = v"
|
||||
>
|
||||
{{ v }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vital-group">
|
||||
<div class="group-title">呼吸频率</div>
|
||||
<div class="big-input-row">
|
||||
<el-input-number
|
||||
v-model="form.respiration"
|
||||
:min="10"
|
||||
:max="60"
|
||||
size="large"
|
||||
controls-position="right"
|
||||
class="big-input"
|
||||
/>
|
||||
<span class="unit">次/分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vital-group">
|
||||
<div class="group-title">疼痛评分</div>
|
||||
<div class="pain-scale">
|
||||
<span
|
||||
v-for="n in 11"
|
||||
:key="n - 1"
|
||||
class="pain-dot"
|
||||
:class="{ active: form.painScore === n - 1, low: n - 1 <= 3, mid: n - 1 > 3 && n - 1 <= 6, high: n - 1 > 6 }"
|
||||
@click="form.painScore = n - 1"
|
||||
>
|
||||
{{ n - 1 }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pain-label">
|
||||
{{ form.painScore <= 3 ? '轻度疼痛' : form.painScore <= 6 ? '中度疼痛' : '重度疼痛' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
round
|
||||
size="large"
|
||||
:loading="submitting"
|
||||
class="submit-btn"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
一键提交
|
||||
</el-button>
|
||||
</div>
|
||||
</MobileLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import MobileLayout from './MobileLayout.vue'
|
||||
import { saveVitalSign, getVitalSignTrend } from './api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const submitting = ref(false)
|
||||
|
||||
const patientId = ref(route.query.patientId)
|
||||
const encounterId = ref(route.query.encounterId)
|
||||
const patientName = ref(route.query.patientName || '')
|
||||
const bedName = ref(route.query.bedName || '')
|
||||
|
||||
const now = new Date()
|
||||
const form = reactive({
|
||||
patientId: patientId.value ? Number(patientId.value) : null,
|
||||
encounterId: encounterId.value ? Number(encounterId.value) : null,
|
||||
patientName: patientName.value,
|
||||
recordDate: now.toISOString().split('T')[0],
|
||||
recordHour: now.getHours(),
|
||||
temperature: null,
|
||||
pulse: null,
|
||||
systolicBp: null,
|
||||
diastolicBp: null,
|
||||
spo2: null,
|
||||
respiration: null,
|
||||
painScore: 0
|
||||
})
|
||||
|
||||
const setBp = (val) => {
|
||||
const [sys, dia] = val.split('/').map(Number)
|
||||
form.systolicBp = sys
|
||||
form.diastolicBp = dia
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.temperature && !form.pulse && !form.systolicBp) {
|
||||
ElMessage.warning('请至少录入一项体征数据')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await saveVitalSign(form)
|
||||
ElMessage.success('提交成功')
|
||||
router.back()
|
||||
} catch (e) {
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.vital-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.patient-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bed {
|
||||
font-size: 13px;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.vital-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.vital-group {
|
||||
background: var(--mobile-card);
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.big-input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.big-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.big-input :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.big-input :deep(.el-input-number .el-input__inner) {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
height: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.big-input :deep(.el-input-number .el-input-number__decrease),
|
||||
.big-input :deep(.el-input-number .el-input-number__increase) {
|
||||
width: 40px;
|
||||
height: 48px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.bp-row {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bp-sep {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 13px;
|
||||
color: var(--mobile-info);
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.quick-values {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
background: #f0f2f5;
|
||||
color: var(--mobile-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.quick-btn.active {
|
||||
background: var(--mobile-primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pain-scale {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pain-dot {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
background: #f0f2f5;
|
||||
color: var(--mobile-text-secondary);
|
||||
}
|
||||
|
||||
.pain-dot.low {
|
||||
background: #e1f5e4;
|
||||
color: var(--mobile-success);
|
||||
}
|
||||
|
||||
.pain-dot.mid {
|
||||
background: #fdf3e5;
|
||||
color: var(--mobile-warning);
|
||||
}
|
||||
|
||||
.pain-dot.high {
|
||||
background: #fde2e2;
|
||||
color: var(--mobile-danger);
|
||||
}
|
||||
|
||||
.pain-dot.active {
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
|
||||
}
|
||||
|
||||
.pain-label {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--mobile-text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,45 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getMobileTaskList(params) {
|
||||
return request({ url: '/mp/nursing/task-list', method: 'get', params })
|
||||
}
|
||||
|
||||
export function completeTask(data) {
|
||||
return request({ url: '/mp/nursing/task-complete', method: 'post', data })
|
||||
}
|
||||
|
||||
export function getMobilePatientList(params) {
|
||||
return request({ url: '/mp/nursing/patient-list', method: 'get', params })
|
||||
}
|
||||
|
||||
export function getPatientDetail(patientId) {
|
||||
return request({ url: '/mp/nursing/patient-detail/' + patientId, method: 'get' })
|
||||
}
|
||||
|
||||
export function getPatientOrders(patientId, params) {
|
||||
return request({ url: '/mp/nursing/patient-orders/' + patientId, method: 'get', params })
|
||||
}
|
||||
|
||||
export function executeOrder(data) {
|
||||
return request({ url: '/mp/nursing/order-execute', method: 'post', data })
|
||||
}
|
||||
|
||||
export function saveVitalSign(data) {
|
||||
return request({ url: '/mp/nursing/vital-sign', method: 'post', data })
|
||||
}
|
||||
|
||||
export function getVitalSignTrend(patientId, params) {
|
||||
return request({ url: '/mp/nursing/vital-sign-trend/' + patientId, method: 'get', params })
|
||||
}
|
||||
|
||||
export function getAssessmentList(patientId) {
|
||||
return request({ url: '/mp/nursing/assessment-list/' + patientId, method: 'get' })
|
||||
}
|
||||
|
||||
export function submitAssessment(data) {
|
||||
return request({ url: '/mp/nursing/assessment-submit', method: 'post', data })
|
||||
}
|
||||
|
||||
export function getPatientRecords(patientId, params) {
|
||||
return request({ url: '/mp/nursing/patient-records/' + patientId, method: 'get', params })
|
||||
}
|
||||
Reference in New Issue
Block a user