feat(mobile): 重构移动端护士工作站 - 完整功能版本

This commit is contained in:
2026-06-19 22:04:41 +08:00
parent 3bc8a85426
commit 6dc9aaba6c
16 changed files with 366 additions and 346 deletions

View 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'

View File

@@ -0,0 +1,8 @@
# 页面标题
VITE_APP_TITLE = HealthLink移动护理
# 生产环境配置
VITE_APP_ENV = 'production'
# API地址
VITE_APP_BASE_API = '/dev-api'

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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