refactor: 移除UI项目中的mobile代码,准备独立移动端项目

This commit is contained in:
2026-06-19 12:15:18 +08:00
parent 7b4cfeb6d5
commit 5ab3865e04
7 changed files with 0 additions and 2054 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 })
}