feat(mobile-h5): 创建独立移动端H5护理工作站项目

This commit is contained in:
2026-06-19 12:16:58 +08:00
parent 5ab3865e04
commit 99812e1bf0
15 changed files with 558 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<title>HealthLink 移动护理</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,24 @@
{
"name": "healthlink-his-mobile",
"version": "1.0.0",
"description": "HealthLink-HIS 移动护理H5工作站",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"axios": "^1.7.0",
"element-plus": "^2.7.0",
"vxe-table": "^4.7.0",
"echarts": "^5.5.0",
"pinia": "^2.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.4.0",
"sass": "^1.77.0"
}
}

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -0,0 +1,29 @@
import axios from 'axios'
const request = axios.create({
baseURL: '/healthlink-his/api/v1',
timeout: 10000
})
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
request.interceptors.response.use(
res => res.data,
err => { console.error(err); return Promise.reject(err) }
)
export const nursingApi = {
getTasks: (params) => request.get('/mp/nursing/tasks', { params }),
completeTask: (id, data) => request.post(`/mp/nursing/tasks/${id}/complete`, data),
getPatientInfo: (id) => request.get(`/mp/nursing/patient/${id}`),
getVitalSigns: (patientId) => request.get(`/mp/nursing/vital-signs/${patientId}`),
submitVitalSign: (data) => request.post('/mp/nursing/vital-sign', data),
getAssessments: (patientId) => request.get(`/mp/nursing/assessments/${patientId}`),
submitAssessment: (data) => request.post('/mp/nursing/assessment', data)
}
export default request

View File

@@ -0,0 +1,13 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import './styles/mobile.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { size: 'large' })
app.mount('#app')

View File

@@ -0,0 +1,16 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', redirect: '/mobile/tasks' },
{ path: '/mobile', component: () => import('../views/MobileLayout.vue'), children: [
{ path: 'tasks', component: () => import('../views/TaskList.vue'), meta: { title: '任务' } },
{ path: 'patients', component: () => import('../views/PatientList.vue'), meta: { title: '患者' } },
{ path: 'patient-detail/:id', component: () => import('../views/PatientDetail.vue'), meta: { title: '患者详情' } },
{ path: 'vital-entry/:patientId', component: () => import('../views/VitalSignEntry.vue'), meta: { title: '生命体征' } },
{ path: 'assessment/:patientId', component: () => import('../views/AssessmentForm.vue'), meta: { title: '护理评估' } },
{ path: 'mine', component: () => import('../views/Mine.vue'), meta: { title: '我的' } }
]}
]
const router = createRouter({ history: createWebHistory(), routes })
export default router

View File

@@ -0,0 +1,6 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 16px; color: #333; background: #f5f5f5; -webkit-font-smoothing: antialiased; }
:root { --primary: #1890ff; --success: #52c41a; --warning: #fa8c16; --danger: #f5222d; --bg: #f5f5f5; --card: #fff; --border: #e8e8e8; }
input, button, textarea { font-family: inherit; font-size: inherit; }
button { cursor: pointer; -webkit-tap-highlight-color: transparent; }
::-webkit-scrollbar { display: none; }

View File

@@ -0,0 +1,89 @@
<template>
<div class="assessment-form">
<div class="type-select">
<div v-for="type in assessmentTypes" :key="type.key" class="type-card" :class="{ active: selectedType === type.key }" @click="selectedType = type.key">
<div class="type-icon">{{ type.icon }}</div>
<div class="type-name">{{ type.name }}</div>
</div>
</div>
<div v-if="selectedType" class="form-content">
<div v-for="(item, idx) in currentItems" :key="idx" class="form-item">
<div class="item-label">{{ item.label }}</div>
<div class="item-options">
<span v-for="opt in item.options" :key="opt.value" class="option" :class="{ selected: formData[item.key] === opt.value }" @click="formData[item.key] = opt.value">
{{ opt.label }} ({{ opt.score }})
</span>
</div>
</div>
<div class="score-result">
<div class="total-score">总分: {{ totalScore }}</div>
<div class="risk-level" :class="riskLevel">{{ riskLevelText }}</div>
</div>
<button class="submit-btn" @click="submit">提交评估</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { nursingApi } from '../api'
import { ElMessage } from 'element-plus'
const route = useRoute()
const selectedType = ref('')
const formData = ref({})
const assessmentTypes = [
{ key: 'Braden', name: '压疮评估', icon: '🩹', items: [
{ key: 'sensory', label: '感知能力', options: [{ label: '完全受限', value: 1, score: 1 }, { label: '严重受限', value: 2, score: 2 }, { label: '轻度受限', value: 3, score: 3 }, { label: '未受损', value: 4, score: 4 }] },
{ key: 'moisture', label: '皮肤潮湿', options: [{ label: '持续潮湿', value: 1, score: 1 }, { label: '经常潮湿', value: 2, score: 2 }, { label: '偶尔潮湿', value: 3, score: 3 }, { label: '很少潮湿', value: 4, score: 4 }] },
{ key: 'activity', label: '活动能力', options: [{ label: '卧床', value: 1, score: 1 }, { label: '轮椅', value: 2, score: 2 }, { label: '偶尔步行', value: 3, score: 3 }, { label: '经常步行', value: 4, score: 4 }] }
]},
{ key: 'Morse', name: '跌倒评估', icon: '⚠️', items: [
{ key: 'history', label: '跌倒史', options: [{ label: '无', value: 0, score: 0 }, { label: '有', value: 25, score: 25 }] },
{ key: 'diagnosis', label: '诊断', options: [{ label: '无', value: 0, score: 0 }, { label: '有', value: 15, score: 15 }] },
{ key: 'ambulation', label: '行走辅助', options: [{ label: '无需/卧床', value: 0, score: 0 }, { label: '拐杖/助行器', value: 15, score: 15 }, { label: '扶墙/家具', value: 30, score: 30 }] }
]},
{ key: 'NRS2002', name: '营养筛查', icon: '🍎', items: [
{ key: 'bmi', label: 'BMI', options: [{ label: '≥20.5', value: 0, score: 0 }, { label: '18.5-20.5', value: 1, score: 1 }, { label: '<18.5', value: 2, score: 2 }] },
{ key: 'weightLoss', label: '体重下降', options: [{ label: '无', value: 0, score: 0 }, { label: '<5%', value: 1, score: 1 }, { label: '>5%', value: 2, score: 2 }] },
{ key: 'intake', label: '饮食摄入', options: [{ label: '正常', value: 0, score: 0 }, { label: '减少', value: 1, score: 1 }, { label: '极少', value: 2, score: 2 }] }
]}
]
const currentItems = computed(() => assessmentTypes.find(t => t.key === selectedType.value)?.items || [])
const totalScore = computed(() => currentItems.value.reduce((sum, item) => sum + (formData.value[item.key] || 0), 0))
const riskLevel = computed(() => {
if (selectedType.value === 'Braden') return totalScore.value <= 12 ? 'HIGH' : totalScore.value <= 14 ? 'MEDIUM' : 'LOW'
if (selectedType.value === 'Morse') return totalScore.value >= 45 ? 'HIGH' : totalScore.value >= 25 ? 'MEDIUM' : 'LOW'
return totalScore.value >= 3 ? 'HIGH' : totalScore.value >= 2 ? 'MEDIUM' : 'LOW'
})
const riskLevelText = computed(() => ({ HIGH: '高风险', MEDIUM: '中风险', LOW: '低风险' }[riskLevel.value]))
const submit = async () => {
try {
await nursingApi.submitAssessment({ patientId: route.params.patientId, assessmentType: selectedType.value, totalScore: totalScore.value, riskLevel: riskLevel.value, detail: JSON.stringify(formData.value) })
ElMessage.success('评估提交成功')
} catch (e) { ElMessage.error('提交失败') }
}
</script>
<style scoped>
.type-select { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
.type-card { background: #fff; border-radius: 8px; padding: 16px; text-align: center; border: 2px solid transparent; }
.type-card.active { border-color: #1890ff; background: #e6f7ff; }
.type-icon { font-size: 28px; }
.type-name { font-size: 13px; margin-top: 4px; }
.form-content { background: #fff; border-radius: 8px; padding: 16px; }
.form-item { margin-bottom: 16px; }
.item-label { font-weight: 600; margin-bottom: 8px; }
.item-options { display: flex; flex-wrap: wrap; gap: 8px; }
.option { padding: 8px 12px; background: #f0f0f0; border-radius: 6px; font-size: 13px; cursor: pointer; }
.option.selected { background: #1890ff; color: #fff; }
.score-result { text-align: center; padding: 16px 0; border-top: 1px solid #eee; margin-top: 12px; }
.total-score { font-size: 24px; font-weight: 600; }
.risk-level { font-size: 16px; margin-top: 4px; }
.risk-HIGH { color: #f5222d; } .risk-MEDIUM { color: #fa8c16; } .risk-LOW { color: #52c41a; }
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 12px; }
</style>

View File

@@ -0,0 +1,33 @@
<template>
<div class="mine">
<div class="user-info">
<div class="avatar"></div>
<div class="info">
<div class="name">护士工作站</div>
<div class="role">移动H5版 v1.0</div>
</div>
</div>
<div class="menu-list">
<div class="menu-item"><span>今日工作量</span><span class="value">0</span></div>
<div class="menu-item"><span>待处理任务</span><span class="value">0</span></div>
<div class="menu-item"><span>高风险患者</span><span class="value">0</span></div>
<div class="menu-item" @click="logout"><span>退出登录</span><span class="arrow"></span></div>
</div>
</div>
</template>
<script setup>
const logout = () => { localStorage.removeItem('token'); window.location.href = '/login' }
</script>
<style scoped>
.user-info { background: #1890ff; color: #fff; padding: 24px 16px; display: flex; align-items: center; gap: 16px; }
.avatar { width: 56px; height: 56px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 600; }
.name { font-size: 18px; font-weight: 600; }
.role { font-size: 13px; opacity: 0.8; }
.menu-list { background: #fff; margin-top: 12px; border-radius: 8px; overflow: hidden; }
.menu-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; font-size: 15px; }
.menu-item:last-child { border-bottom: none; }
.value { color: #1890ff; font-weight: 600; }
.arrow { color: #999; font-size: 18px; }
</style>

View File

@@ -0,0 +1,42 @@
<template>
<div class="mobile-layout">
<div class="mobile-header">
<button v-if="canGoBack" class="back-btn" @click="$router.back()"></button>
<h1>{{ $route.meta.title || 'HealthLink' }}</h1>
</div>
<div class="mobile-content">
<router-view />
</div>
<div class="mobile-tabs">
<div v-for="tab in tabs" :key="tab.path" class="tab-item" :class="{ active: $route.path === tab.path }" @click="$router.push(tab.path)">
<span class="tab-icon">{{ tab.icon }}</span>
<span class="tab-label">{{ tab.label }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const canGoBack = computed(() => route.path !== '/mobile/tasks')
const tabs = [
{ path: '/mobile/tasks', icon: '📋', label: '任务' },
{ path: '/mobile/patients', icon: '👥', label: '患者' },
{ path: '/mobile/mine', icon: '👤', label: '我的' }
]
</script>
<style scoped>
.mobile-layout { display: flex; flex-direction: column; height: 100vh; background: #f5f5f5; }
.mobile-header { height: 48px; background: #1890ff; color: #fff; display: flex; align-items: center; padding: 0 16px; position: sticky; top: 0; z-index: 10; }
.mobile-header h1 { font-size: 18px; margin: 0; flex: 1; text-align: center; }
.back-btn { background: none; border: none; color: #fff; font-size: 20px; position: absolute; left: 16px; }
.mobile-content { flex: 1; overflow-y: auto; padding: 12px; padding-bottom: 60px; }
.mobile-tabs { position: fixed; bottom: 0; left: 0; right: 0; height: 56px; background: #fff; display: flex; border-top: 1px solid #e8e8e8; z-index: 10; padding-bottom: env(safe-area-inset-bottom); }
.tab-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: 12px; color: #999; }
.tab-item.active { color: #1890ff; }
.tab-icon { font-size: 20px; margin-bottom: 2px; }
</style>

View File

@@ -0,0 +1,92 @@
<template>
<div class="patient-detail">
<div class="patient-header">
<div class="avatar">{{ patient.name?.charAt(0) }}</div>
<div class="info">
<div class="name">{{ patient.name }} <span class="bed">{{ patient.bedNo }}</span></div>
<div class="diag">{{ patient.diagnosis }}</div>
</div>
</div>
<div class="tabs">
<div v-for="tab in tabs" :key="tab.key" class="tab" :class="{ active: activeTab === tab.key }" @click="activeTab = tab.key">{{ tab.label }}</div>
</div>
<div class="tab-content">
<div v-if="activeTab === 'orders'">
<div v-for="order in orders" :key="order.id" class="order-item">
<div class="order-name">{{ order.orderName }}</div>
<div class="order-dose">{{ order.dosage }} {{ order.frequency }}</div>
<button v-if="order.status === 'PENDING'" class="exec-btn" @click="executeOrder(order)">执行</button>
</div>
</div>
<div v-if="activeTab === 'vitals'">
<div class="vital-grid">
<div class="vital-item" v-for="v in latestVitals" :key="v.key">
<div class="vital-value" :class="v.status">{{ v.value }}</div>
<div class="vital-label">{{ v.label }}</div>
</div>
</div>
<button class="add-btn" @click="$router.push(`/mobile/vital-entry/${$route.params.id}`)">录入体征</button>
</div>
<div v-if="activeTab === 'assessments'">
<div v-for="a in assessments" :key="a.id" class="assess-item">
<div class="assess-type">{{ a.assessmentType }}</div>
<div class="assess-score">评分: {{ a.totalScore }} <span :class="'risk-' + a.riskLevel">{{ a.riskLevel }}</span></div>
</div>
<button class="add-btn" @click="$router.push(`/mobile/assessment/${$route.params.id}`)">新建评估</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { nursingApi } from '../api'
const route = useRoute()
const patient = ref({})
const orders = ref([])
const latestVitals = ref([])
const assessments = ref([])
const activeTab = ref('orders')
const tabs = [{ key: 'orders', label: '医嘱' }, { key: 'vitals', label: '体征' }, { key: 'assessments', label: '评估' }]
onMounted(async () => {
const id = route.params.id
try {
const res = await nursingApi.getPatientInfo(id)
patient.value = res.data || {}
} catch (e) { console.error(e) }
})
const executeOrder = async (order) => {
try { await nursingApi.completeTask(order.id, { result: '执行完成' }); order.status = 'COMPLETED' } catch (e) { console.error(e) }
}
</script>
<style scoped>
.patient-header { background: #1890ff; color: #fff; padding: 16px; display: flex; align-items: center; gap: 12px; }
.avatar { width: 48px; height: 48px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; font-size: 20px; }
.name { font-size: 18px; font-weight: 600; }
.bed { font-size: 14px; opacity: 0.8; }
.diag { font-size: 13px; opacity: 0.8; }
.tabs { display: flex; background: #fff; border-bottom: 1px solid #eee; }
.tab { flex: 1; text-align: center; padding: 12px; font-size: 14px; color: #666; }
.tab.active { color: #1890ff; border-bottom: 2px solid #1890ff; }
.tab-content { padding: 12px; }
.order-item { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; }
.order-name { font-weight: 600; }
.order-dose { color: #666; font-size: 13px; }
.exec-btn { background: #1890ff; color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 14px; }
.vital-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
.vital-item { background: #fff; border-radius: 8px; padding: 12px; text-align: center; }
.vital-value { font-size: 20px; font-weight: 600; }
.vital-value.normal { color: #52c41a; }
.vital-value.warning { color: #fa8c16; }
.vital-value.danger { color: #f5222d; }
.vital-label { font-size: 12px; color: #999; margin-top: 4px; }
.add-btn { width: 100%; padding: 12px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 12px; }
.assess-item { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; }
.assess-type { font-weight: 600; }
.risk-HIGH { color: #f5222d; } .risk-MEDIUM { color: #fa8c16; } .risk-LOW { color: #52c41a; }
</style>

View File

@@ -0,0 +1,47 @@
<template>
<div class="patient-list">
<div class="search-bar">
<input v-model="searchText" placeholder="搜索患者姓名/床号..." class="search-input" />
</div>
<div v-for="p in filteredPatients" :key="p.id" class="patient-card" @click="$router.push(`/mobile/patient-detail/${p.id}`)">
<div class="patient-avatar">{{ p.name?.charAt(0) }}</div>
<div class="patient-info">
<div class="patient-name">{{ p.name }} <span class="bed-no">{{ p.bedNo }}</span></div>
<div class="patient-diag">{{ p.diagnosis || '暂无诊断' }}</div>
<div class="patient-level">
<span :class="'level-' + p.nursingLevel">{{ p.nursingLevel }}级护理</span>
</div>
</div>
</div>
<div v-if="filteredPatients.length === 0" class="empty">暂无患者</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { nursingApi } from '../api'
const patients = ref([])
const searchText = ref('')
const filteredPatients = computed(() => {
if (!searchText.value) return patients.value
return patients.value.filter(p => p.name?.includes(searchText.value) || p.bedNo?.includes(searchText.value))
})
onMounted(async () => {
try { const res = await nursingApi.getPatientInfo('list'); patients.value = res.data || [] } catch (e) { console.error(e) }
})
</script>
<style scoped>
.search-bar { padding: 8px 0; }
.search-input { width: 100%; padding: 10px 16px; border: 1px solid #ddd; border-radius: 20px; font-size: 16px; outline: none; }
.patient-card { background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.patient-avatar { width: 44px; height: 44px; border-radius: 50%; background: #1890ff; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; }
.patient-name { font-weight: 600; font-size: 16px; }
.bed-no { color: #999; font-size: 14px; }
.patient-diag { color: #666; font-size: 13px; margin: 2px 0; }
.patient-level { font-size: 12px; }
.level-1 { color: #f5222d; } .level-2 { color: #fa8c16; } .level-3 { color: #52c41a; }
.empty { text-align: center; padding: 40px; color: #999; }
</style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="task-list">
<div v-for="(group, type) in groupedTasks" :key="type" class="task-group">
<div class="group-header">{{ type }}</div>
<div v-for="task in group" :key="task.id" class="task-card" @touchstart="swipeStart" @touchend="swipeEnd($event, task)">
<div class="task-info">
<div class="task-patient">{{ task.patientName }} - {{ task.bedNo }}</div>
<div class="task-content">{{ task.taskContent }}</div>
<div class="task-time">{{ task.dueTime }}</div>
</div>
<div class="task-status" :class="task.taskStatus">{{ statusText(task.taskStatus) }}</div>
</div>
</div>
<div v-if="tasks.length === 0" class="empty">暂无任务</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { nursingApi } from '../api'
const tasks = ref([])
const groupedTasks = computed(() => {
const groups = {}
tasks.value.forEach(t => {
const type = t.taskType || '其他'
if (!groups[type]) groups[type] = []
groups[type].push(t)
})
return groups
})
const statusText = (s) => ({ PENDING: '待完成', IN_PROGRESS: '进行中', COMPLETED: '已完成' }[s] || s)
const loadTasks = async () => {
try { const res = await nursingApi.getTasks({ status: 'PENDING' }); tasks.value = res.data || [] } catch (e) { console.error(e) }
}
let startX = 0
const swipeStart = (e) => { startX = e.touches[0].clientX }
const swipeEnd = async (e, task) => {
const diff = startX - e.changedTouches[0].clientX
if (diff > 80 && task.taskStatus === 'PENDING') {
try { await nursingApi.completeTask(task.id, { result: '完成' }); task.taskStatus = 'COMPLETED' } catch (e) { console.error(e) }
}
}
onMounted(loadTasks)
</script>
<style scoped>
.task-list { padding: 0; }
.group-header { padding: 8px 16px; font-size: 14px; color: #666; background: #f0f0f0; margin: 8px 0; border-radius: 4px; }
.task-card { background: #fff; border-radius: 8px; padding: 12px 16px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.task-patient { font-weight: 600; font-size: 16px; }
.task-content { color: #666; font-size: 14px; margin: 4px 0; }
.task-time { color: #999; font-size: 12px; }
.task-status { font-size: 12px; padding: 4px 8px; border-radius: 12px; }
.task-status.PENDING { background: #fff7e6; color: #fa8c16; }
.task-status.COMPLETED { background: #f6ffed; color: #52c41a; }
.empty { text-align: center; padding: 40px; color: #999; }
</style>

View File

@@ -0,0 +1,69 @@
<template>
<div class="vital-entry">
<div class="entry-grid">
<div v-for="item in vitalItems" :key="item.key" class="entry-item">
<div class="entry-label">{{ item.label }}</div>
<input v-model="formData[item.key]" type="number" :placeholder="item.placeholder" class="entry-input" />
<div class="quick-values">
<span v-for="v in item.quickValues" :key="v" class="quick-val" @click="formData[item.key] = v">{{ v }}</span>
</div>
</div>
</div>
<div class="pain-section">
<div class="entry-label">疼痛评分</div>
<div class="pain-scale">
<span v-for="n in 11" :key="n" class="pain-num" :class="{ active: formData.painScore === n-1 }" @click="formData.painScore = n-1">{{ n-1 }}</span>
</div>
<div class="pain-label">{{ painLabel }}</div>
</div>
<button class="submit-btn" @click="submit">一键提交</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { nursingApi } from '../api'
import { ElMessage } from 'element-plus'
const route = useRoute()
const formData = ref({ temperature: '', pulse: '', bloodPressureHigh: '', bloodPressureLow: '', spo2: '', respiration: '', painScore: 0 })
const vitalItems = [
{ key: 'temperature', label: '体温(°C)', placeholder: '36.5', quickValues: [36.0, 36.5, 37.0, 37.5, 38.0] },
{ key: 'pulse', label: '脉搏(次/分)', placeholder: '72', quickValues: [60, 72, 80, 90, 100] },
{ key: 'bloodPressureHigh', label: '收缩压(mmHg)', placeholder: '120', quickValues: [90, 110, 120, 130, 140] },
{ key: 'bloodPressureLow', label: '舒张压(mmHg)', placeholder: '80', quickValues: [60, 70, 80, 90, 100] },
{ key: 'spo2', label: '血氧(%)', placeholder: '98', quickValues: [95, 96, 97, 98, 99] },
{ key: 'respiration', label: '呼吸(次/分)', placeholder: '18', quickValues: [14, 16, 18, 20, 22] }
]
const painLabel = computed(() => {
const s = formData.value.painScore
if (s <= 3) return '轻度疼痛'
if (s <= 6) return '中度疼痛'
return '重度疼痛'
})
const submit = async () => {
try {
await nursingApi.submitVitalSign({ ...formData.value, patientId: route.params.patientId })
ElMessage.success('提交成功')
} catch (e) { ElMessage.error('提交失败') }
}
</script>
<style scoped>
.entry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.entry-item { background: #fff; border-radius: 8px; padding: 12px; }
.entry-label { font-size: 13px; color: #666; margin-bottom: 8px; }
.entry-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 18px; text-align: center; }
.quick-values { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
.quick-val { padding: 4px 8px; background: #f0f0f0; border-radius: 4px; font-size: 12px; cursor: pointer; }
.pain-section { background: #fff; border-radius: 8px; padding: 12px; margin-top: 12px; }
.pain-scale { display: flex; gap: 4px; margin-top: 8px; }
.pain-num { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: #f0f0f0; font-size: 14px; cursor: pointer; }
.pain-num.active { background: #1890ff; color: #fff; }
.pain-label { text-align: center; margin-top: 8px; color: #666; font-size: 14px; }
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 18px; margin-top: 16px; font-weight: 600; }
</style>

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 82,
proxy: {
'/healthlink-his': {
target: 'http://localhost:18082',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets'
}
})