feat(mobile): 重构移动端护士工作站 - 完整功能版本
This commit is contained in:
11
healthlink-his-mobile/.env.development
Normal file
11
healthlink-his-mobile/.env.development
Normal file
@@ -0,0 +1,11 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = HealthLink移动护理
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
# API地址
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
||||
# 后端代理地址
|
||||
VITE_API_PROXY = 'http://localhost:18080/healthlink-his'
|
||||
8
healthlink-his-mobile/.env.production
Normal file
8
healthlink-his-mobile/.env.production
Normal file
@@ -0,0 +1,8 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = HealthLink移动护理
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
# API地址
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "healthlink-his-mobile",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "HealthLink-HIS 移动护理H5工作站",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -12,11 +13,13 @@
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0",
|
||||
"pinia": "^2.1.0",
|
||||
"axios": "^1.7.0",
|
||||
"element-plus": "^2.7.0",
|
||||
"vxe-table": "^4.7.0",
|
||||
"echarts": "^5.5.0",
|
||||
"pinia": "^2.1.0"
|
||||
"js-cookie": "^3.0.5",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-to-regexp": "^6.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/dev-api',
|
||||
timeout: 10000
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_APP_BASE_API || '/dev-api',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
request.interceptors.request.use(config => {
|
||||
service.interceptors.request.use(config => {
|
||||
const token = localStorage.getItem('Admin-Token')
|
||||
if (token && !(config.headers && config.headers.isToken === false)) {
|
||||
config.headers.Authorization = 'Bearer ' + token
|
||||
@@ -14,39 +14,41 @@ request.interceptors.request.use(config => {
|
||||
return config
|
||||
})
|
||||
|
||||
request.interceptors.response.use(
|
||||
res => {
|
||||
if (res.data?.code === 401) {
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
if (res.code === 401) {
|
||||
localStorage.removeItem('Admin-Token')
|
||||
window.location.href = '/login'
|
||||
return Promise.reject(new Error('登录已过期'))
|
||||
}
|
||||
return res.data
|
||||
return res
|
||||
},
|
||||
err => {
|
||||
if (err.response?.status === 401) {
|
||||
error => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('Admin-Token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(err)
|
||||
ElMessage.error(error.response?.data?.msg || '请求失败')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export const authApi = {
|
||||
login: (data) => request.post('/login', data, { headers: { isToken: false } }),
|
||||
getTenants: (username) => request.get('/system/tenant/user-bind/' + username, { headers: { isToken: false } })
|
||||
login: (data) => service.post('/login', data, { headers: { isToken: false } }),
|
||||
getTenants: (username) => service.get('/system/tenant/user-bind/' + username, { headers: { isToken: false } })
|
||||
}
|
||||
|
||||
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}`),
|
||||
getPatientList: (params) => request.get('/mp/nursing/patient/list', { params }),
|
||||
getOrders: (patientId) => request.get(`/mp/nursing/orders/${patientId}`),
|
||||
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)
|
||||
getTasks: (params) => service.get('/mp/nursing/tasks', { params }),
|
||||
completeTask: (id, data) => service.post(`/mp/nursing/tasks/${id}/complete`, data),
|
||||
getPatientInfo: (id) => service.get(`/mp/nursing/patient/${id}`),
|
||||
getPatientList: (params) => service.get('/mp/nursing/patient/list', { params }),
|
||||
getOrders: (patientId) => service.get(`/mp/nursing/orders/${patientId}`),
|
||||
getVitalSigns: (patientId) => service.get(`/mp/nursing/vital-signs/${patientId}`),
|
||||
submitVitalSign: (data) => service.post('/mp/nursing/vital-sign', data),
|
||||
getAssessments: (patientId) => service.get(`/mp/nursing/assessments/${patientId}`),
|
||||
submitAssessment: (data) => service.post('/mp/nursing/assessment', data)
|
||||
}
|
||||
|
||||
export default request
|
||||
export default service
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import 'element-plus/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
@@ -9,5 +10,5 @@ import './styles/mobile.css'
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
app.use(ElementPlus, { size: 'large' })
|
||||
app.use(ElementPlus, { size: 'large', locale: zhCn })
|
||||
app.mount('#app')
|
||||
|
||||
@@ -2,25 +2,21 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
const routes = [
|
||||
{ path: '/login', component: () => import('../views/Login.vue'), meta: { title: '登录' } },
|
||||
{ path: '/', redirect: '/mobile/tasks' },
|
||||
{ path: '/', redirect: '/mobile/home' },
|
||||
{ path: '/mobile', component: () => import('../views/MobileLayout.vue'), meta: { requiresAuth: true }, children: [
|
||||
{ path: 'tasks', component: () => import('../views/TaskList.vue'), meta: { title: '任务' } },
|
||||
{ path: 'patients', component: () => import('../views/PatientList.vue'), meta: { title: '患者' } },
|
||||
{ path: 'home', component: () => import('../views/Home.vue'), meta: { title: '首页' } },
|
||||
{ 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: '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 })
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth) {
|
||||
const token = localStorage.getItem('Admin-Token')
|
||||
if (!token) { next('/login'); return }
|
||||
}
|
||||
if (to.meta.requiresAuth && !localStorage.getItem('Admin-Token')) { next('/login'); return }
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -2,24 +2,18 @@
|
||||
<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 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>
|
||||
<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 class="score-result"><div class="total-score">总分: {{ totalScore }}</div><div class="risk-level" :class="riskLevel">{{ riskLevelText }}</div></div>
|
||||
<button class="submit-btn" @click="submit" :disabled="submitting">{{ submitting ? '提交中...' : '提交评估' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,11 +21,12 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { nursingApi } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
const selectedType = ref('')
|
||||
const submitting = ref(false)
|
||||
const formData = ref({})
|
||||
|
||||
const assessmentTypes = [
|
||||
@@ -43,7 +38,7 @@ const assessmentTypes = [
|
||||
{ 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: '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 }] },
|
||||
@@ -61,37 +56,31 @@ const riskLevel = computed(() => {
|
||||
})
|
||||
const riskLevelText = computed(() => ({ HIGH: '高风险', MEDIUM: '中风险', LOW: '低风险' }[riskLevel.value]))
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true
|
||||
submitting.value = true
|
||||
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) {
|
||||
console.error(e)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch (e) { ElMessage.error('提交失败') } finally { submitting.value = false }
|
||||
}
|
||||
</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 { background: #fff; border-radius: 8px; padding: 14px; text-align: center; border: 2px solid transparent; cursor: pointer; }
|
||||
.type-card.active { border-color: #1890ff; background: #e6f7ff; }
|
||||
.type-icon { font-size: 28px; }
|
||||
.type-icon { font-size: 26px; }
|
||||
.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; }
|
||||
.form-content { background: #fff; border-radius: 8px; padding: 14px; }
|
||||
.form-item { margin-bottom: 14px; }
|
||||
.item-label { font-weight: 600; margin-bottom: 8px; font-size: 14px; }
|
||||
.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; }
|
||||
.score-result { text-align: center; padding: 14px 0; border-top: 1px solid #eee; margin-top: 10px; }
|
||||
.total-score { font-size: 22px; font-weight: 600; }
|
||||
.risk-level { font-size: 15px; 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; }
|
||||
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 10px; }
|
||||
.submit-btn:disabled { background: #91d5ff; }
|
||||
</style>
|
||||
|
||||
89
healthlink-his-mobile/src/views/Home.vue
Normal file
89
healthlink-his-mobile/src/views/Home.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="home">
|
||||
<div class="welcome">
|
||||
<div class="user-info">
|
||||
<div class="avatar">{{ userInfo?.userName?.charAt(0) || '护' }}</div>
|
||||
<div><div class="name">{{ userInfo?.userName || '护士' }}</div><div class="dept">{{ userInfo?.deptName || '护理部' }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-for="s in stats" :key="s.label">
|
||||
<div class="stat-value">{{ s.value }}</div>
|
||||
<div class="stat-label">{{ s.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="quick-actions">
|
||||
<div class="action-title">快捷操作</div>
|
||||
<div class="action-grid">
|
||||
<div class="action-item" v-for="a in actions" :key="a.label" @click="$router.push(a.path)">
|
||||
<div class="action-icon" :style="{ background: a.color }">{{ a.icon }}</div>
|
||||
<div class="action-label">{{ a.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="recent-tasks">
|
||||
<div class="section-header"><span>待办任务</span><span class="more" @click="$router.push('/mobile/tasks')">查看全部</span></div>
|
||||
<div v-for="task in recentTasks" :key="task.id" class="task-item">
|
||||
<div class="task-dot" :class="task.taskStatus"></div>
|
||||
<div class="task-info"><div class="task-name">{{ task.patientName }} - {{ task.taskContent }}</div><div class="task-time">{{ task.dueTime }}</div></div>
|
||||
</div>
|
||||
<div v-if="recentTasks.length === 0" class="empty">暂无待办任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const userInfo = ref({})
|
||||
const stats = ref([{ label: '待执行医嘱', value: 0 }, { label: '待测体征', value: 0 }, { label: '待评估', value: 0 }, { label: '高风险患者', value: 0 }])
|
||||
const recentTasks = ref([])
|
||||
const actions = [
|
||||
{ icon: '📋', label: '任务列表', path: '/mobile/tasks', color: '#1890ff' },
|
||||
{ icon: '👥', label: '患者列表', path: '/mobile/patients', color: '#52c41a' },
|
||||
{ icon: '💊', label: '药品发放', path: '/mobile/drug', color: '#fa8c16' },
|
||||
{ icon: '📊', label: '生命体征', path: '/mobile/vital-entry', color: '#722ed1' },
|
||||
{ icon: '📝', label: '护理记录', path: '/mobile/record', color: '#13c2c2' },
|
||||
{ icon: '💧', label: '入出量', path: '/mobile/inout', color: '#eb2f96' }
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try { const info = localStorage.getItem('userInfo'); if (info) userInfo.value = JSON.parse(info) } catch {}
|
||||
try {
|
||||
const res = await nursingApi.getTasks({ status: 'PENDING' })
|
||||
if (res.code === 200) {
|
||||
recentTasks.value = (res.data?.tasks || []).slice(0, 5)
|
||||
stats.value[0].value = res.data?.summary?.pending || 0
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home { padding: 12px; padding-bottom: 70px; }
|
||||
.welcome { background: linear-gradient(135deg, #1890ff, #096dd9); border-radius: 12px; padding: 20px; color: #fff; margin-bottom: 12px; }
|
||||
.user-info { 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; }
|
||||
.dept { font-size: 13px; opacity: 0.8; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; }
|
||||
.stat-card { background: #fff; border-radius: 8px; padding: 12px 8px; text-align: center; }
|
||||
.stat-value { font-size: 22px; font-weight: 600; color: #1890ff; }
|
||||
.stat-label { font-size: 11px; color: #999; margin-top: 4px; }
|
||||
.quick-actions { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 12px; }
|
||||
.action-title { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
|
||||
.action-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
||||
.action-item { text-align: center; cursor: pointer; }
|
||||
.action-icon { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 22px; margin: 0 auto 6px; }
|
||||
.action-label { font-size: 12px; color: #666; }
|
||||
.recent-tasks { background: #fff; border-radius: 12px; padding: 16px; }
|
||||
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 15px; font-weight: 600; }
|
||||
.more { color: #1890ff; font-size: 13px; }
|
||||
.task-item { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }
|
||||
.task-dot { width: 8px; height: 8px; border-radius: 50%; background: #fa8c16; }
|
||||
.task-dot.COMPLETED { background: #52c41a; }
|
||||
.task-name { font-size: 14px; }
|
||||
.task-time { font-size: 12px; color: #999; }
|
||||
.empty { text-align: center; padding: 20px; color: #999; }
|
||||
</style>
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="login-header">
|
||||
<div class="logo">🏥</div>
|
||||
<h1>{{ currentTenantName || 'HealthLink 移动护理' }}</h1>
|
||||
<p>医院信息管理系统</p>
|
||||
<p>护士工作站</p>
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<div class="form-item">
|
||||
@@ -21,14 +21,9 @@
|
||||
<label>密码</label>
|
||||
<input v-model="form.password" type="password" placeholder="请输入密码" class="input" @keyup.enter="handleLogin" />
|
||||
</div>
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
</button>
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">{{ loading ? '登录中...' : '登 录' }}</button>
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
</div>
|
||||
<div class="login-footer">
|
||||
<span>{{ currentTenantName || 'HealthLink' }} v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,14 +45,8 @@ const loadTenants = async () => {
|
||||
try {
|
||||
const res = await authApi.getTenants(form.value.username)
|
||||
if (res.code === 200 && res.data) {
|
||||
tenantOptions.value = res.data.map(item => ({
|
||||
label: item.tenantName,
|
||||
value: item.id
|
||||
}))
|
||||
if (tenantOptions.value.length === 1) {
|
||||
form.value.tenantId = tenantOptions.value[0].value
|
||||
currentTenantName.value = tenantOptions.value[0].label
|
||||
}
|
||||
tenantOptions.value = res.data.map(item => ({ label: item.tenantName, value: item.id }))
|
||||
if (tenantOptions.value.length === 1) { form.value.tenantId = tenantOptions.value[0].value; currentTenantName.value = tenantOptions.value[0].label }
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
@@ -70,29 +59,12 @@ const onTenantChange = () => {
|
||||
const handleLogin = async () => {
|
||||
if (!form.value.username) { errorMsg.value = '请输入用户名'; return }
|
||||
if (!form.value.password) { errorMsg.value = '请输入密码'; return }
|
||||
|
||||
loading.value = true
|
||||
errorMsg.value = ''
|
||||
loading.value = true; errorMsg.value = ''
|
||||
try {
|
||||
const res = await authApi.login({
|
||||
username: form.value.username,
|
||||
password: form.value.password,
|
||||
tenantId: form.value.tenantId,
|
||||
code: '',
|
||||
uuid: ''
|
||||
})
|
||||
if (res.code === 200 && res.token) {
|
||||
localStorage.setItem('Admin-Token', res.token)
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/mobile/tasks')
|
||||
} else {
|
||||
errorMsg.value = res.msg || '登录失败'
|
||||
}
|
||||
} catch (e) {
|
||||
errorMsg.value = e.response?.data?.msg || '登录失败,请检查网络'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
const res = await authApi.login({ username: form.value.username, password: form.value.password, tenantId: form.value.tenantId, code: '', uuid: '' })
|
||||
if (res.code === 200 && res.token) { localStorage.setItem('Admin-Token', res.token); ElMessage.success('登录成功'); router.push('/mobile/home') }
|
||||
else { errorMsg.value = res.msg || '登录失败' }
|
||||
} catch (e) { errorMsg.value = e.response?.data?.msg || '登录失败' } finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -111,5 +83,4 @@ select.input { appearance: none; background: #fff url("data:image/svg+xml,%3Csvg
|
||||
.login-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 18px; font-weight: 600; cursor: pointer; }
|
||||
.login-btn:disabled { background: #91d5ff; }
|
||||
.error-msg { color: #f5222d; text-align: center; margin-top: 12px; font-size: 14px; }
|
||||
.login-footer { margin-top: 20px; color: rgba(255,255,255,0.6); font-size: 12px; }
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
<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 class="avatar">{{ userInfo?.userName?.charAt(0) || '护' }}</div>
|
||||
<div class="info"><div class="name">{{ userInfo?.userName || '护士' }}</div><div class="role">{{ userInfo?.deptName || '护理部' }} | 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"><span>今日工作量</span><span class="value">{{ taskCount }}</span></div>
|
||||
<div class="menu-item"><span>待处理任务</span><span class="value">{{ pendingCount }}</span></div>
|
||||
<div class="menu-item" @click="logout"><span>退出登录</span><span class="arrow">›</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const userInfo = ref({})
|
||||
const taskCount = ref(0)
|
||||
const pendingCount = ref(0)
|
||||
|
||||
onMounted(async () => {
|
||||
try { const info = localStorage.getItem('userInfo'); if (info) userInfo.value = JSON.parse(info) } catch {}
|
||||
try { const res = await nursingApi.getTasks({}); if (res.code === 200) { taskCount.value = res.data?.summary?.total || 0; pendingCount.value = res.data?.summary?.pending || 0 } } catch {}
|
||||
})
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认退出登录?', '提示', { confirmButtonText: '确认', cancelButtonText: '取消' })
|
||||
localStorage.removeItem('Admin-Token')
|
||||
window.location.href = '/login'
|
||||
} catch {}
|
||||
try { await ElMessageBox.confirm('确认退出登录?', '提示'); localStorage.removeItem('Admin-Token'); localStorage.removeItem('userInfo'); window.location.href = '/login' } catch {}
|
||||
}
|
||||
</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; }
|
||||
.user-info { background: linear-gradient(135deg, #1890ff, #096dd9); 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; }
|
||||
.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-list { background: #fff; margin: 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; }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="mobile-layout">
|
||||
<div class="mobile-header">
|
||||
<div class="mobile-header" v-if="!hideHeader">
|
||||
<button v-if="canGoBack" class="back-btn" @click="$router.back()">←</button>
|
||||
<h1>{{ $route.meta.title || 'HealthLink' }}</h1>
|
||||
</div>
|
||||
<div class="mobile-content">
|
||||
<div class="mobile-content" :class="{ 'no-header': hideHeader }">
|
||||
<router-view />
|
||||
</div>
|
||||
<div class="mobile-tabs">
|
||||
<div class="mobile-tabs" v-if="showTabs">
|
||||
<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>
|
||||
@@ -19,10 +19,12 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const canGoBack = computed(() => route.path !== '/mobile/tasks')
|
||||
const canGoBack = computed(() => route.path !== '/mobile/home')
|
||||
const hideHeader = computed(() => ['/mobile/login'].includes(route.path))
|
||||
const showTabs = computed(() => route.path.startsWith('/mobile/'))
|
||||
const tabs = [
|
||||
{ path: '/mobile/home', icon: '🏠', label: '首页' },
|
||||
{ path: '/mobile/tasks', icon: '📋', label: '任务' },
|
||||
{ path: '/mobile/patients', icon: '👥', label: '患者' },
|
||||
{ path: '/mobile/mine', icon: '👤', label: '我的' }
|
||||
@@ -34,7 +36,8 @@ const tabs = [
|
||||
.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-content { flex: 1; overflow-y: auto; }
|
||||
.mobile-content.no-header { padding-bottom: 56px; }
|
||||
.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; }
|
||||
|
||||
@@ -1,52 +1,42 @@
|
||||
<template>
|
||||
<div class="patient-detail">
|
||||
<div v-if="loading" class="empty">加载中...</div>
|
||||
<template v-else>
|
||||
<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 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-main"><div class="order-name">{{ order.orderName || order.adviceName }}</div><div class="order-dose">{{ order.dosage }} {{ order.frequency }}</div></div>
|
||||
<button v-if="order.status === 'PENDING' || order.executeStatus === '待执行'" class="exec-btn" @click="executeOrder(order)">执行</button>
|
||||
<span v-else class="done-tag">已执行</span>
|
||||
</div>
|
||||
<div v-if="orders.length === 0" class="empty">暂无医嘱</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 v-if="activeTab === 'vitals'">
|
||||
<div class="vital-grid"><div class="vital-item" v-for="v in latestVitals" :key="v.key"><div class="vital-value">{{ v.value || '--' }}</div><div class="vital-label">{{ v.label }}</div></div></div>
|
||||
<button class="action-btn" @click="$router.push(`/mobile/vital-entry/${$route.params.id}`)">录入体征</button>
|
||||
</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 v-if="orders.length === 0" class="empty">暂无医嘱</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 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="action-btn" @click="$router.push(`/mobile/assessment/${$route.params.id}`)">新建评估</button>
|
||||
<div v-if="assessments.length === 0" class="empty">暂无评估记录</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { nursingApi } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
const patient = ref({})
|
||||
@@ -54,66 +44,46 @@ const orders = ref([])
|
||||
const latestVitals = ref([])
|
||||
const assessments = ref([])
|
||||
const activeTab = ref('orders')
|
||||
const loading = ref(false)
|
||||
const tabs = [{ key: 'orders', label: '医嘱' }, { key: 'vitals', label: '体征' }, { key: 'assessments', label: '评估' }]
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.params.id
|
||||
loading.value = true
|
||||
try {
|
||||
const [patientRes, ordersRes, vitalsRes, assessRes] = await Promise.all([
|
||||
nursingApi.getPatientInfo(id),
|
||||
nursingApi.getOrders(id),
|
||||
nursingApi.getVitalSigns(id),
|
||||
nursingApi.getAssessments(id)
|
||||
const [pRes, oRes, vRes, aRes] = await Promise.all([
|
||||
nursingApi.getPatientInfo(id), nursingApi.getOrders(id),
|
||||
nursingApi.getVitalSigns(id), nursingApi.getAssessments(id)
|
||||
])
|
||||
patient.value = patientRes.data || {}
|
||||
orders.value = ordersRes.data || []
|
||||
latestVitals.value = vitalsRes.data || []
|
||||
assessments.value = assessRes.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('加载患者数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
patient.value = pRes.data || {}; orders.value = oRes.data || []; latestVitals.value = vRes.data || []; assessments.value = aRes.data || []
|
||||
} catch (e) { ElMessage.error('加载失败') }
|
||||
})
|
||||
|
||||
const executeOrder = async (order) => {
|
||||
try {
|
||||
await nursingApi.completeTask(order.id, { result: '执行完成' })
|
||||
order.status = 'COMPLETED'
|
||||
ElMessage.success('医嘱执行成功')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('医嘱执行失败')
|
||||
}
|
||||
try { await nursingApi.completeTask(order.id, { result: '执行完成' }); order.status = 'COMPLETED'; ElMessage.success('医嘱已执行') } catch (e) { ElMessage.error('执行失败') }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.patient-header { background: #1890ff; color: #fff; padding: 16px; display: flex; align-items: center; gap: 12px; }
|
||||
.patient-header { background: linear-gradient(135deg, #1890ff, #096dd9); 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; }
|
||||
.tabs { display: flex; background: #fff; border-bottom: 1px solid #eee; position: sticky; top: 48px; z-index: 5; }
|
||||
.tab { flex: 1; text-align: center; padding: 12px; font-size: 14px; color: #666; }
|
||||
.tab.active { color: #1890ff; border-bottom: 2px solid #1890ff; }
|
||||
.tab.active { color: #1890ff; border-bottom: 2px solid #1890ff; font-weight: 600; }
|
||||
.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; }
|
||||
.order-name { font-weight: 600; font-size: 14px; }
|
||||
.order-dose { color: #666; font-size: 12px; margin-top: 2px; }
|
||||
.exec-btn { background: #1890ff; color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 13px; }
|
||||
.done-tag { color: #52c41a; font-size: 12px; }
|
||||
.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; }
|
||||
.vital-value { font-size: 18px; font-weight: 600; color: #1890ff; }
|
||||
.vital-label { font-size: 11px; color: #999; margin-top: 4px; }
|
||||
.action-btn { width: 100%; padding: 12px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 15px; 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; }
|
||||
.empty { text-align: center; padding: 20px; color: #999; }
|
||||
</style>
|
||||
|
||||
@@ -1,61 +1,49 @@
|
||||
<template>
|
||||
<div class="patient-list">
|
||||
<div class="search-bar">
|
||||
<input v-model="searchText" placeholder="搜索患者姓名/床号..." class="search-input" />
|
||||
</div>
|
||||
<div v-if="loading" class="empty">加载中...</div>
|
||||
<template v-else>
|
||||
<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 class="search-bar"><input v-model="searchText" placeholder="搜索患者姓名/床号..." class="search-input" @input="filterPatients" /></div>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-for="p in displayPatients" :key="p.id" class="patient-card" @click="$router.push(`/mobile/patient-detail/${p.id}`)">
|
||||
<div class="patient-avatar" :class="'level-' + p.nursingLevel">{{ p.name?.charAt(0) }}</div>
|
||||
<div class="patient-info">
|
||||
<div class="patient-name">{{ p.name }} <span class="bed">{{ p.bedNo }}床</span></div>
|
||||
<div class="patient-diag">{{ p.diagnosis || '暂无诊断' }}</div>
|
||||
<div class="patient-tags"><span class="tag" :class="'level-' + p.nursingLevel">{{ p.nursingLevel }}级护理</span><span v-if="p.gender" class="tag">{{ p.gender }}</span></div>
|
||||
</div>
|
||||
<div v-if="filteredPatients.length === 0" class="empty">暂无患者</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="!loading && displayPatients.length === 0" class="empty">暂无患者</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { nursingApi } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const patients = ref([])
|
||||
const searchText = ref('')
|
||||
const loading = ref(false)
|
||||
const filteredPatients = computed(() => {
|
||||
if (!searchText.value) return patients.value
|
||||
return patients.value.filter(p => p.name?.includes(searchText.value) || p.bedNo?.includes(searchText.value))
|
||||
})
|
||||
const searchText = ref('')
|
||||
const displayPatients = computed(() => searchText.value ? patients.value.filter(p => p.name?.includes(searchText.value) || p.bedNo?.includes(searchText.value)) : patients.value)
|
||||
|
||||
onMounted(async () => {
|
||||
const loadPatients = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await nursingApi.getPatientList()
|
||||
patients.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('加载患者列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
try { const res = await nursingApi.getPatientList({}); patients.value = res.data || [] } catch (e) { ElMessage.error('加载失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
onMounted(loadPatients)
|
||||
</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; }
|
||||
.search-input { width: 100%; padding: 10px 16px; border: 1px solid #ddd; border-radius: 20px; font-size: 15px; outline: none; background: #fff; }
|
||||
.search-input:focus { border-color: #1890ff; }
|
||||
.loading { text-align: center; padding: 20px; color: #999; }
|
||||
.patient-card { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; align-items: center; gap: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.patient-avatar { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 600; color: #fff; }
|
||||
.level-1 { background: #f5222d; } .level-2 { background: #fa8c16; } .level-3 { background: #52c41a; }
|
||||
.patient-name { font-weight: 600; font-size: 15px; }
|
||||
.bed { color: #999; font-size: 13px; }
|
||||
.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; }
|
||||
.patient-tags { display: flex; gap: 6px; }
|
||||
.tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; }
|
||||
.empty { text-align: center; padding: 40px; color: #999; }
|
||||
</style>
|
||||
|
||||
@@ -1,57 +1,36 @@
|
||||
<template>
|
||||
<div class="task-list">
|
||||
<div class="task-header">
|
||||
<h3>护理任务</h3>
|
||||
<button class="refresh-btn" @click="loadTasks" :disabled="loading">{{ loading ? '刷新中...' : '刷新' }}</button>
|
||||
<div class="filter-bar">
|
||||
<select v-model="filterType" class="filter-select" @change="loadTasks"><option value="">全部</option><option value="医嘱执行">医嘱执行</option><option value="生命体征">生命体征</option><option value="护理评估">护理评估</option></select>
|
||||
<button class="refresh-btn" @click="loadTasks">刷新</button>
|
||||
</div>
|
||||
<div v-if="loading" class="empty">加载中...</div>
|
||||
<template v-else>
|
||||
<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 v-if="loading" class="loading">加载中...</div>
|
||||
<div v-for="task in filteredTasks" :key="task.id" class="task-card" @touchstart="swipeStart" @touchend="swipeEnd($event, task)">
|
||||
<div class="task-info">
|
||||
<div class="task-header"><span class="task-patient">{{ task.patientName }}</span><span class="bed">{{ task.bedNo }}床</span></div>
|
||||
<div class="task-content">{{ task.taskContent }}</div>
|
||||
<div class="task-meta"><span class="task-type">{{ task.taskType }}</span><span class="task-time">{{ task.dueTime }}</span></div>
|
||||
</div>
|
||||
<div v-if="tasks.length === 0" class="empty">暂无任务</div>
|
||||
</template>
|
||||
<div class="task-status" :class="task.taskStatus">{{ statusText(task.taskStatus) }}</div>
|
||||
</div>
|
||||
<div v-if="!loading && filteredTasks.length === 0" class="empty">暂无任务</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const tasks = ref([])
|
||||
const loading = ref(false)
|
||||
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 filterType = ref('')
|
||||
const filteredTasks = computed(() => filterType.value ? tasks.value.filter(t => t.taskType === filterType.value) : tasks.value)
|
||||
const statusText = (s) => ({ PENDING: '待完成', IN_PROGRESS: '进行中', COMPLETED: '已完成' }[s] || s)
|
||||
|
||||
const loadTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await nursingApi.getTasks({ status: 'PENDING' })
|
||||
tasks.value = res.data || []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('加载任务失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
try { const res = await nursingApi.getTasks({ status: 'PENDING' }); tasks.value = res.data?.tasks || [] } catch (e) { ElMessage.error('加载失败') } finally { loading.value = false }
|
||||
}
|
||||
|
||||
let startX = 0
|
||||
@@ -60,16 +39,10 @@ const swipeEnd = async (e, task) => {
|
||||
const diff = startX - e.changedTouches[0].clientX
|
||||
if (diff > 80 && task.taskStatus === 'PENDING') {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认完成此任务?', '提示', { confirmButtonText: '确认', cancelButtonText: '取消' })
|
||||
await ElMessageBox.confirm('确认完成此任务?', '提示')
|
||||
await nursingApi.completeTask(task.id, { result: '完成' })
|
||||
task.taskStatus = 'COMPLETED'
|
||||
ElMessage.success('任务已完成')
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
console.error(e)
|
||||
ElMessage.error('任务完成失败')
|
||||
}
|
||||
}
|
||||
task.taskStatus = 'COMPLETED'; ElMessage.success('任务已完成')
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,17 +50,18 @@ onMounted(loadTasks)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-list { padding: 0; }
|
||||
.task-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; }
|
||||
.task-header h3 { margin: 0; font-size: 16px; }
|
||||
.refresh-btn { background: #1890ff; color: #fff; border: none; padding: 6px 16px; border-radius: 4px; font-size: 14px; }
|
||||
.refresh-btn:disabled { background: #ccc; }
|
||||
.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; }
|
||||
.filter-bar { display: flex; gap: 8px; padding: 8px 0; }
|
||||
.filter-select { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: #fff; }
|
||||
.refresh-btn { padding: 8px 16px; background: #1890ff; color: #fff; border: none; border-radius: 6px; }
|
||||
.loading { text-align: center; padding: 20px; color: #999; }
|
||||
.task-card { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
.task-header { display: flex; align-items: center; gap: 8px; }
|
||||
.task-patient { font-weight: 600; font-size: 15px; }
|
||||
.bed { color: #1890ff; font-size: 13px; }
|
||||
.task-content { color: #666; font-size: 13px; margin: 4px 0; }
|
||||
.task-meta { display: flex; gap: 12px; font-size: 12px; color: #999; }
|
||||
.task-type { background: #e6f7ff; color: #1890ff; padding: 2px 8px; border-radius: 4px; }
|
||||
.task-status { font-size: 12px; padding: 4px 10px; border-radius: 12px; white-space: nowrap; }
|
||||
.task-status.PENDING { background: #fff7e6; color: #fa8c16; }
|
||||
.task-status.COMPLETED { background: #f6ffed; color: #52c41a; }
|
||||
.empty { text-align: center; padding: 40px; color: #999; }
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
<template>
|
||||
<div class="vital-entry">
|
||||
<div class="patient-bar" v-if="patientName"><span class="label">患者:</span> {{ patientName }}</div>
|
||||
<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 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="entry-label">疼痛评分 (0-10)</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>
|
||||
<button class="submit-btn" @click="submit" :disabled="submitting">{{ submitting ? '提交中...' : '一键提交' }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { nursingApi } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { nursingApi } from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
const submitting = ref(false)
|
||||
const patientName = ref('')
|
||||
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] },
|
||||
@@ -37,41 +35,33 @@ const vitalItems = [
|
||||
{ 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 loading = ref(false)
|
||||
const painLabel = computed(() => { const s = formData.value.painScore; return s <= 3 ? '轻度疼痛' : s <= 6 ? '中度疼痛' : '重度疼痛' })
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true
|
||||
submitting.value = true
|
||||
try {
|
||||
await nursingApi.submitVitalSign({ ...formData.value, patientId: route.params.patientId })
|
||||
ElMessage.success('提交成功')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
ElMessage.success('体征录入成功')
|
||||
} catch (e) { ElMessage.error('提交失败') } finally { submitting.value = false }
|
||||
}
|
||||
</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; }
|
||||
.patient-bar { background: #e6f7ff; padding: 10px 16px; font-size: 14px; margin-bottom: 12px; border-radius: 8px; }
|
||||
.patient-bar .label { color: #666; }
|
||||
.entry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.entry-item { background: #fff; border-radius: 8px; padding: 10px; }
|
||||
.entry-label { font-size: 12px; color: #666; margin-bottom: 6px; }
|
||||
.entry-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 18px; text-align: center; }
|
||||
.entry-input:focus { border-color: #1890ff; }
|
||||
.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; }
|
||||
.quick-val { padding: 3px 8px; background: #f0f0f0; border-radius: 4px; font-size: 12px; cursor: pointer; }
|
||||
.quick-val:active { background: #1890ff; color: #fff; }
|
||||
.pain-section { background: #fff; border-radius: 8px; padding: 12px; margin-top: 10px; }
|
||||
.pain-scale { display: flex; gap: 3px; margin-top: 8px; flex-wrap: wrap; }
|
||||
.pain-num { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: #f0f0f0; font-size: 13px; 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; }
|
||||
.pain-label { text-align: center; margin-top: 8px; color: #666; font-size: 13px; }
|
||||
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 16px; font-weight: 600; }
|
||||
.submit-btn:disabled { background: #91d5ff; }
|
||||
</style>
|
||||
|
||||
@@ -1,20 +1,42 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import path from 'path'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 82,
|
||||
proxy: {
|
||||
'/dev-api': {
|
||||
target: 'http://localhost:18080/healthlink-his',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
return {
|
||||
base: '/',
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': path.resolve(__dirname, './'),
|
||||
'@': path.resolve(__dirname, './src')
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||
},
|
||||
server: {
|
||||
port: 82,
|
||||
host: true,
|
||||
proxy: {
|
||||
'/dev-api': {
|
||||
target: env.VITE_API_PROXY || 'http://localhost:18080/healthlink-his',
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/dev-api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
cssMinify: 'esbuild'
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'legacy-js-api']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets'
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user