feat(mobile-h5): 创建独立移动端H5护理工作站项目
This commit is contained in:
14
healthlink-his-mobile/index.html
Normal file
14
healthlink-his-mobile/index.html
Normal 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>
|
||||
24
healthlink-his-mobile/package.json
Normal file
24
healthlink-his-mobile/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
3
healthlink-his-mobile/src/App.vue
Normal file
3
healthlink-his-mobile/src/App.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
29
healthlink-his-mobile/src/api/index.js
Normal file
29
healthlink-his-mobile/src/api/index.js
Normal 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
|
||||
13
healthlink-his-mobile/src/main.js
Normal file
13
healthlink-his-mobile/src/main.js
Normal 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')
|
||||
16
healthlink-his-mobile/src/router/index.js
Normal file
16
healthlink-his-mobile/src/router/index.js
Normal 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
|
||||
6
healthlink-his-mobile/src/styles/mobile.css
Normal file
6
healthlink-his-mobile/src/styles/mobile.css
Normal 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; }
|
||||
89
healthlink-his-mobile/src/views/AssessmentForm.vue
Normal file
89
healthlink-his-mobile/src/views/AssessmentForm.vue
Normal 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>
|
||||
33
healthlink-his-mobile/src/views/Mine.vue
Normal file
33
healthlink-his-mobile/src/views/Mine.vue
Normal 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>
|
||||
42
healthlink-his-mobile/src/views/MobileLayout.vue
Normal file
42
healthlink-his-mobile/src/views/MobileLayout.vue
Normal 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>
|
||||
92
healthlink-his-mobile/src/views/PatientDetail.vue
Normal file
92
healthlink-his-mobile/src/views/PatientDetail.vue
Normal 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>
|
||||
47
healthlink-his-mobile/src/views/PatientList.vue
Normal file
47
healthlink-his-mobile/src/views/PatientList.vue
Normal 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>
|
||||
62
healthlink-his-mobile/src/views/TaskList.vue
Normal file
62
healthlink-his-mobile/src/views/TaskList.vue
Normal 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>
|
||||
69
healthlink-his-mobile/src/views/VitalSignEntry.vue
Normal file
69
healthlink-his-mobile/src/views/VitalSignEntry.vue
Normal 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>
|
||||
19
healthlink-his-mobile/vite.config.js
Normal file
19
healthlink-his-mobile/vite.config.js
Normal 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'
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user