Compare commits

...

21 Commits

Author SHA1 Message Date
c2cd74e479 Merge remote-tracking branch 'origin/develop' into develop 2026-06-20 15:10:42 +08:00
6b8a05c250 fix(db): 修复Flyway V82版本冲突 - 重命名为V83/V84 2026-06-20 15:09:55 +08:00
c671f5aa89 fix(ui): 修复路由文件语法错误(移除残留的移动端路由括号) 2026-06-20 14:57:28 +08:00
9e428cbd0f fix(mobile): 优化患者列表 - 分页加载+搜索+数据格式适配 2026-06-20 14:41:38 +08:00
f5ae4f3c64 fix: 修复SysTenantController编译错误 - LambdaQueryWrapper导入+字段名修正 2026-06-20 14:16:28 +08:00
6843418a88 fix(security): 添加移动端API到安全白名单 2026-06-20 14:13:34 +08:00
11aac8b135 fix(ui): 移除主UI中的移动端路由(已移至独立项目) 2026-06-20 14:13:11 +08:00
1045706e5e fix(mobile): 补全后端移动端接口+修复API路径 2026-06-20 12:43:53 +08:00
8b081ca8e4 fix(mobile): 修复所有API路径对齐后端实际接口 2026-06-20 12:36:15 +08:00
58993d51e3 fix(mobile): 修复患者列表API使用正确的patient-home-manage/init接口 2026-06-20 12:18:58 +08:00
2d58b05fdc fix(mobile): 登录页医院选择始终显示+加载提示 2026-06-20 11:58:02 +08:00
618c069aaa fix(mobile): 患者列表默认显示所有患者 2026-06-19 23:48:10 +08:00
39c68a3361 fix(mobile): 修复患者列表API路径 2026-06-19 23:44:15 +08:00
df61879a06 fix(mobile): 添加API错误处理和超时提示 2026-06-19 23:43:25 +08:00
1bf3bbd432 fix(mobile): 移除首页无效链接(需要患者上下文的功能) 2026-06-19 23:42:32 +08:00
895abb972e fix(mobile): 登录页医院选择移至密码下方 2026-06-19 23:41:22 +08:00
67370bd1cf fix(mobile): 登录页匹配PC端逻辑 - 输入用户名后加载租户列表 2026-06-19 23:38:09 +08:00
cab9537c7e fix(security): 将租户列表接口加入安全白名单 2026-06-19 23:34:06 +08:00
2437366093 fix(mobile): 添加租户列表接口+修复医院选择为空 2026-06-19 23:30:41 +08:00
bffef625cb fix(mobile): 登录页记住上次选择的医院 2026-06-19 23:27:34 +08:00
5c1502a180 fix(mobile): 修复登录页死循环 - 401拦截器排除登录页+使用匿名租户接口 2026-06-19 23:24:26 +08:00
19 changed files with 673 additions and 108 deletions

View File

@@ -17,7 +17,7 @@ service.interceptors.request.use(config => {
service.interceptors.response.use(
response => {
const res = response.data
if (res.code === 401) {
if (res.code === 401 && !window.location.pathname.includes('/login')) {
localStorage.removeItem('Admin-Token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
@@ -26,7 +26,7 @@ service.interceptors.response.use(
return res
},
error => {
if (error.response?.status === 401) {
if (error.response?.status === 401 && !window.location.pathname.includes('/login')) {
localStorage.removeItem('Admin-Token')
localStorage.removeItem('userInfo')
window.location.href = '/login'
@@ -37,21 +37,29 @@ service.interceptors.response.use(
export const authApi = {
login: (data) => service.post('/login', data, { headers: { isToken: false } }),
getTenants: (username) => service.get('/system/tenant/user-bind/' + username, { headers: { isToken: false } }),
getAllTenants: () => service.get('/system/tenant/page', { headers: { isToken: false }, params: { pageSize: 100 } }),
getTenants: () => service.get('/system/tenant/all-active', { headers: { isToken: false } }),
getUserTenants: (username) => service.get('/system/tenant/user-bind/' + username, { headers: { isToken: false } }),
getInfo: () => service.get('/getInfo')
}
export const nursingApi = {
getTasks: (params) => service.get('/nurse-station/advice-process/page', { params }),
completeTask: (id, data) => service.post(`/nurse-station/advice-process/execute`, data),
getTasks: (params) => service.get('/nurse-station/advice-process/inpatient-advice', { params }),
completeTask: (id, data) => service.post(`/nurse-station/advice-process/advice-execute`, data),
getPatientInfo: (id) => service.get('/inpatientmanage/inhospitalregister/' + id),
getPatientList: (params) => service.get('/inpatientmanage/inhospitalregister/list', { params }),
getOrders: (encounterId) => service.get('/nurse-station/advice-process/page', { params: { encounterId } }),
getVitalSigns: (patientId) => service.get('/nursing/vital-signs/' + patientId),
submitVitalSign: (data) => service.post('/nursing/vital-sign', data),
getAssessments: (encounterId) => service.get('/nursing/assessment/encounter/' + encounterId),
submitAssessment: (data) => service.post('/nursing/assessment', data)
getPatientList: (params) => service.get('/patient-home-manage/init', { params }),
getOrders: (encounterId) => service.get('/nurse-station/advice-process/inpatient-advice', { params: { encounterId } }),
getVitalSigns: (patientId) => service.get('/vital-signs-chart/page', { params: { patientId } }),
submitVitalSign: (data) => service.post('/nursing/mobile/vital-sign', data),
getAssessments: (encounterId) => service.get('/api/v1/nursing/assessment/encounter/' + encounterId),
submitAssessment: (data) => service.post('/api/v1/nursing/assessment', data),
getDrugDistribution: (params) => service.get('/nursing/mobile/drug-distribution/list', { params }),
submitDrugDistribution: (data) => service.post('/nursing/mobile/drug-distribution/execute', data),
getNursingRecords: (params) => service.get('/nursing-record/patient-page', { params }),
submitNursingRecord: (data) => service.post('/nursing-record/save-nursing', data),
getInfusionPatrol: (params) => service.get('/nursing-execution/infusion/page', { params }),
submitInfusionPatrol: (data) => service.post('/nursing/mobile/infusion/action', data),
getHandoffRecords: (params) => service.get('/nursing-execution/handoff/page', { params }),
submitHandoffRecord: (data) => service.post('/nursing-execution/handoff/add', data)
}
export default service

View File

@@ -10,6 +10,10 @@ const routes = [
{ path: 'patient-detail/:id', component: () => import('../views/PatientDetail.vue'), meta: { title: '患者详情' } },
{ path: 'vital-entry/:patientId', component: () => import('../views/VitalSignEntry.vue'), meta: { title: '生命体征录入' } },
{ path: 'assessment/:patientId', component: () => import('../views/AssessmentForm.vue'), meta: { title: '护理评估' } },
{ path: 'drug-distribution', component: () => import('../views/DrugDistribution.vue'), meta: { title: '药品发放' } },
{ path: 'nursing-record', component: () => import('../views/NursingRecord.vue'), meta: { title: '护理记录' } },
{ path: 'infusion-patrol', component: () => import('../views/InfusionPatrol.vue'), meta: { title: '输液巡视' } },
{ path: 'handoff-record', component: () => import('../views/HandoffRecord.vue'), meta: { title: '交接班记录' } },
{ path: 'mine', component: () => import('../views/Mine.vue'), meta: { title: '我的' } }
]}
]

View File

@@ -0,0 +1,74 @@
<template>
<div class="drug-dist">
<div class="search-bar"><input v-model="searchText" placeholder="搜索药品名称/患者..." class="search-input" /></div>
<div v-if="loading" class="loading">加载中...</div>
<div v-for="item in filteredList" :key="item.id" class="drug-card">
<div class="drug-header"><span class="drug-name">{{ item.drugName }}</span><span class="status-tag" :class="'s-' + item.status">{{ item.statusText }}</span></div>
<div class="drug-info"><div>患者: {{ item.patientName }} {{ item.bedNo }}</div><div>剂量: {{ item.dosage }}</div><div>用法: {{ item.usage }}</div></div>
<div class="drug-actions">
<button v-if="item.status === 'PENDING'" class="action-btn primary" @click="handleDistribute(item)">发放</button>
<button v-if="item.status === 'PENDING'" class="action-btn" @click="handleReject(item)">拒发</button>
<span v-if="item.status === 'DISTRIBUTED'" class="done-text">已发放</span>
</div>
</div>
<div v-if="!loading && filteredList.length === 0" class="empty">暂无待发放药品</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { nursingApi } from '../api'
const searchText = ref('')
const list = ref([])
const loading = ref(false)
const filteredList = computed(() => searchText.value ? list.value.filter(d => d.drugName?.includes(searchText.value) || d.patientName?.includes(searchText.value)) : list.value)
const loadList = async () => {
loading.value = true
try {
const res = await nursingApi.getDrugDistribution({ pageSize: 100 })
list.value = (res.data?.records || res.data?.rows || res.data || []).map(d => ({ ...d, statusText: d.status === 'DISTRIBUTED' ? '已发放' : d.status === 'REJECTED' ? '已拒发' : '待发放' }))
} catch { ElMessage.error('加载失败') } finally { loading.value = false }
}
const handleDistribute = async (item) => {
try {
await ElMessageBox.confirm('确认发放该药品?', '确认')
await nursingApi.submitDrugDistribution({ id: item.id, action: 'DISTRIBUTE' })
item.status = 'DISTRIBUTED'; item.statusText = '已发放'
ElMessage.success('发放成功')
} catch (e) { if (e !== 'cancel') ElMessage.error('操作失败') }
}
const handleReject = async (item) => {
try {
await ElMessageBox.confirm('确认拒发该药品?', '确认')
await nursingApi.submitDrugDistribution({ id: item.id, action: 'REJECT' })
item.status = 'REJECTED'; item.statusText = '已拒发'
ElMessage.success('已拒发')
} catch (e) { if (e !== 'cancel') ElMessage.error('操作失败') }
}
onMounted(loadList)
</script>
<style scoped>
.search-bar { padding: 8px 0; }
.search-input { width: 100%; padding: 10px 16px; border: 1px solid #ddd; border-radius: 20px; font-size: 15px; outline: none; background: #fff; }
.drug-card { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.drug-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.drug-name { font-weight: 600; font-size: 15px; }
.status-tag { font-size: 11px; padding: 2px 8px; border-radius: 4px; }
.s-PENDING { background: #fff7e6; color: #fa8c16; }
.s-DISTRIBUTED { background: #f6ffed; color: #52c41a; }
.s-REJECTED { background: #fff1f0; color: #f5222d; }
.drug-info { font-size: 13px; color: #666; line-height: 1.8; }
.drug-actions { display: flex; gap: 8px; margin-top: 10px; }
.action-btn { flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 6px; background: #fff; font-size: 13px; }
.action-btn.primary { background: #1890ff; color: #fff; border-color: #1890ff; }
.done-text { color: #52c41a; font-size: 13px; line-height: 36px; }
.loading { text-align: center; padding: 20px; color: #999; }
.empty { text-align: center; padding: 40px; color: #999; }
</style>

View File

@@ -0,0 +1,82 @@
<template>
<div class="handoff-record">
<div class="shift-tabs">
<div v-for="s in shifts" :key="s.key" class="tab" :class="{ active: form.shift === s.key }" @click="form.shift = s.key">{{ s.label }}</div>
</div>
<div class="form-section">
<div class="form-item"><div class="label">交接班护士</div><input v-model="form.handoffNurse" placeholder="交班护士" class="input" /></div>
<div class="form-item"><div class="label">接班护士</div><input v-model="form.onDutyNurse" placeholder="接班护士" class="input" /></div>
<div class="form-item"><div class="label">科室</div><input v-model="form.department" placeholder="科室名称" class="input" /></div>
<div class="form-item"><div class="label">在院患者数</div><input v-model="form.patientCount" type="number" placeholder="0" class="input" /></div>
<div class="form-item"><div class="label">病情变化</div><textarea v-model="form.patientChanges" placeholder="交接患者病情变化..." class="textarea" rows="3"></textarea></div>
<div class="form-item"><div class="label">特殊治疗</div><textarea v-model="form.specialTreatment" placeholder="特殊治疗及注意事项..." class="textarea" rows="3"></textarea></div>
<div class="form-item"><div class="label">待办事项</div><textarea v-model="form.pendingItems" placeholder="未完成事项及待跟进..." class="textarea" rows="3"></textarea></div>
<div class="form-item"><div class="label">物品交接</div><textarea v-model="form.materialHandoff" placeholder="交接的物品..." class="textarea" rows="2"></textarea></div>
</div>
<button class="submit-btn" @click="submit" :disabled="submitting">{{ submitting ? '提交中...' : '保存交接记录' }}</button>
<div class="history-section">
<div class="section-title">历史交接记录</div>
<div v-for="h in history" :key="h.id" class="history-card">
<div class="h-header"><span class="h-shift">{{ h.shift }}</span><span class="h-time">{{ h.createTime }}</span></div>
<div class="h-nurses">{{ h.handoffNurse }} {{ h.onDutyNurse }}</div>
<div class="h-changes">{{ h.patientChanges }}</div>
</div>
<div v-if="history.length === 0" class="empty">暂无历史记录</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { nursingApi } from '../api'
const shifts = [{ key: 'DAY', label: '白班' }, { key: 'NIGHT', label: '夜班' }]
const form = ref({ shift: 'DAY', handoffNurse: '', onDutyNurse: '', department: '', patientCount: 0, patientChanges: '', specialTreatment: '', pendingItems: '', materialHandoff: '' })
const history = ref([])
const submitting = ref(false)
const loadHistory = async () => {
try {
const res = await nursingApi.getHandoffRecords({ pageSize: 20 })
history.value = res.data?.records || res.data?.rows || res.data || []
} catch {}
}
const submit = async () => {
if (!form.value.handoffNurse || !form.value.onDutyNurse) { ElMessage.warning('请填写交接班护士'); return }
submitting.value = true
try {
await nursingApi.submitHandoffRecord({ ...form.value })
ElMessage.success('交接记录已保存')
form.value = { shift: form.value.shift, handoffNurse: '', onDutyNurse: '', department: form.value.department, patientCount: 0, patientChanges: '', specialTreatment: '', pendingItems: '', materialHandoff: '' }
loadHistory()
} catch { ElMessage.error('保存失败') } finally { submitting.value = false }
}
onMounted(loadHistory)
</script>
<style scoped>
.shift-tabs { display: flex; background: #fff; border-radius: 8px; overflow: hidden; margin-bottom: 12px; }
.tab { flex: 1; text-align: center; padding: 12px; font-size: 14px; color: #666; background: #f5f5f5; }
.tab.active { background: #1890ff; color: #fff; }
.form-section { background: #fff; border-radius: 8px; padding: 14px; margin-bottom: 12px; }
.form-item { margin-bottom: 12px; }
.label { font-size: 13px; color: #666; margin-bottom: 6px; }
.input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
.textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; resize: none; font-family: inherit; }
.input:focus, .textarea:focus { border-color: #1890ff; outline: none; }
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-bottom: 16px; font-weight: 600; }
.submit-btn:disabled { background: #91d5ff; }
.history-section { background: #fff; border-radius: 8px; padding: 14px; }
.section-title { font-size: 15px; font-weight: 600; margin-bottom: 12px; }
.history-card { border-bottom: 1px solid #f0f0f0; padding: 10px 0; }
.h-header { display: flex; justify-content: space-between; margin-bottom: 4px; }
.h-shift { font-weight: 600; font-size: 14px; color: #1890ff; }
.h-time { font-size: 12px; color: #999; }
.h-nurses { font-size: 13px; color: #666; margin-bottom: 4px; }
.h-changes { font-size: 13px; color: #333; }
.empty { text-align: center; padding: 20px; color: #999; }
</style>

View File

@@ -42,7 +42,10 @@ const recentTasks = ref([])
const actions = [
{ icon: '📋', label: '任务列表', path: '/mobile/tasks', color: '#1890ff' },
{ icon: '👥', label: '患者列表', path: '/mobile/patients', color: '#52c41a' },
{ icon: '📊', label: '生命体征', path: '/mobile/vital-entry', color: '#722ed1' }
{ icon: '💊', label: '药品发放', path: '/mobile/drug-distribution', color: '#fa8c16' },
{ icon: '📝', label: '护理记录', path: '/mobile/nursing-record', color: '#722ed1' },
{ icon: '💉', label: '输液巡视', path: '/mobile/infusion-patrol', color: '#13c2c2' },
{ icon: '🔄', label: '交接班', path: '/mobile/handoff-record', color: '#f5222d' }
]
onMounted(async () => {

View File

@@ -0,0 +1,91 @@
<template>
<div class="infusion-patrol">
<div class="filter-bar">
<div v-for="f in filters" :key="f.key" class="filter-btn" :class="{ active: activeFilter === f.key }" @click="activeFilter = f.key">{{ f.label }}</div>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-for="item in filteredList" :key="item.id" class="infusion-card">
<div class="inf-header">
<div class="inf-patient">{{ item.patientName }} {{ item.bedNo }}</div>
<div class="inf-status" :class="'s-' + item.status">{{ item.status === 'INFUSING' ? '输液中' : item.status === 'COMPLETED' ? '已完成' : '暂停中' }}</div>
</div>
<div class="drug-list">
<div v-for="drug in item.drugs" :key="drug.id" class="drug-row">
<span class="drug-name">{{ drug.name }}</span>
<span class="drug-spec">{{ drug.spec }}</span>
<span class="drug-flow">{{ drug.flowRate }}</span>
</div>
</div>
<div class="patrol-info" v-if="item.lastPatrolTime"><span class="label">上次巡视:</span> {{ item.lastPatrolTime }}</div>
<div class="inf-actions">
<button v-if="item.status === 'INFUSING'" class="patrol-btn" @click="handlePatrol(item)">巡视</button>
<button v-if="item.status === 'INFUSING'" class="stop-btn" @click="handleComplete(item)">结束输液</button>
</div>
</div>
<div v-if="!loading && filteredList.length === 0" class="empty">暂无输液记录</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { nursingApi } from '../api'
const activeFilter = ref('INFUSING')
const filters = [{ key: 'INFUSING', label: '输液中' }, { key: 'COMPLETED', label: '已完成' }, { key: 'ALL', label: '全部' }]
const list = ref([])
const loading = ref(false)
const filteredList = computed(() => activeFilter.value === 'ALL' ? list.value : list.value.filter(i => i.status === activeFilter.value))
const loadList = async () => {
loading.value = true
try {
const res = await nursingApi.getInfusionPatrol({ pageSize: 100 })
list.value = res.data?.records || res.data?.rows || res.data || []
} catch { ElMessage.error('加载失败') } finally { loading.value = false }
}
const handlePatrol = async (item) => {
try {
await nursingApi.submitInfusionPatrol({ infusionId: item.id, action: 'PATROL' })
item.lastPatrolTime = new Date().toLocaleString()
ElMessage.success('巡视完成')
} catch { ElMessage.error('巡视失败') }
}
const handleComplete = async (item) => {
try {
await ElMessageBox.confirm('确认结束输液?', '确认')
await nursingApi.submitInfusionPatrol({ infusionId: item.id, action: 'COMPLETE' })
item.status = 'COMPLETED'
ElMessage.success('输液已结束')
} catch (e) { if (e !== 'cancel') ElMessage.error('操作失败') }
}
onMounted(loadList)
</script>
<style scoped>
.filter-bar { display: flex; gap: 8px; padding: 8px 0; }
.filter-btn { padding: 6px 16px; border-radius: 20px; background: #f0f0f0; font-size: 13px; cursor: pointer; }
.filter-btn.active { background: #1890ff; color: #fff; }
.infusion-card { background: #fff; border-radius: 8px; padding: 12px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.inf-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
.inf-patient { font-weight: 600; font-size: 15px; }
.inf-status { font-size: 12px; padding: 2px 8px; border-radius: 4px; }
.s-INFUSING { background: #e6f7ff; color: #1890ff; }
.s-COMPLETED { background: #f6ffed; color: #52c41a; }
.s-PAUSED { background: #fff7e6; color: #fa8c16; }
.drug-list { background: #fafafa; border-radius: 6px; padding: 8px; margin-bottom: 8px; }
.drug-row { display: flex; gap: 10px; font-size: 13px; padding: 4px 0; }
.drug-name { flex: 1; font-weight: 500; }
.drug-spec { color: #999; }
.drug-flow { color: #1890ff; }
.patrol-info { font-size: 12px; color: #999; margin-bottom: 8px; }
.label { color: #666; }
.inf-actions { display: flex; gap: 8px; }
.patrol-btn { flex: 1; padding: 8px; background: #52c41a; color: #fff; border: none; border-radius: 6px; font-size: 13px; }
.stop-btn { flex: 1; padding: 8px; background: #fff; color: #666; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; }
.loading { text-align: center; padding: 20px; color: #999; }
.empty { text-align: center; padding: 40px; color: #999; }
</style>

View File

@@ -6,20 +6,21 @@
<p>护士工作站</p>
</div>
<div class="login-form">
<div class="form-item">
<label>用户名</label>
<input v-model="form.username" type="text" placeholder="请输入用户名" class="input" @blur="loadTenants" />
</div>
<div class="form-item">
<label>密码</label>
<input v-model="form.password" type="password" placeholder="请输入密码" class="input" @keyup.enter="handleLogin" />
</div>
<div class="form-item">
<label>医院/租户</label>
<select v-model="form.tenantId" class="input" @change="onTenantChange">
<option value="">请选择医院</option>
<option v-for="t in tenantOptions" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
</div>
<div class="form-item">
<label>用户名</label>
<input v-model="form.username" type="text" placeholder="请输入用户名" class="input" />
</div>
<div class="form-item">
<label>密码</label>
<input v-model="form.password" type="password" placeholder="请输入密码" class="input" @keyup.enter="handleLogin" />
<div v-if="tenantOptions.length === 0 && form.username" class="loading-text">加载医院列表中...</div>
</div>
<button class="login-btn" @click="handleLogin" :disabled="loading">{{ loading ? '登录中...' : '登 录' }}</button>
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
@@ -41,14 +42,20 @@ const currentTenantName = ref('')
const form = ref({ username: '', password: '', tenantId: '' })
const loadTenants = async () => {
if (!form.value.username) return
try {
const res = await authApi.getAllTenants()
if (res.code === 200) {
const list = res.data?.records || res.data || []
tenantOptions.value = list.map(item => ({ label: item.tenantName, value: item.tenantId || item.id }))
if (tenantOptions.value.length === 1) { form.value.tenantId = tenantOptions.value[0].value; currentTenantName.value = tenantOptions.value[0].label }
const res = await authApi.getUserTenants(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
}
}
} catch (e) { console.error(e) }
} catch (e) {
console.error('加载租户失败:', e)
errorMsg.value = '无法连接服务器,请检查网络'
}
}
const onTenantChange = () => {
@@ -56,7 +63,9 @@ const onTenantChange = () => {
currentTenantName.value = selected ? selected.label : ''
}
onMounted(loadTenants)
onMounted(() => {
if (form.value.username) loadTenants()
})
const handleLogin = async () => {
if (!form.value.username) { errorMsg.value = '请输入用户名'; return }
@@ -70,14 +79,9 @@ const handleLogin = async () => {
if (infoRes.code === 200) {
const user = infoRes.user || {}
localStorage.setItem('userInfo', JSON.stringify({
userId: user.userId,
userName: user.userName,
nickName: user.nickName,
practitionerId: user.practitionerId,
orgId: user.orgId,
orgName: user.orgName,
roles: user.roles,
permissions: user.permissions
userId: user.userId, userName: user.userName, nickName: user.nickName,
practitionerId: user.practitionerId, orgId: user.orgId, orgName: user.orgName,
roles: user.roles, permissions: user.permissions
}))
}
ElMessage.success('登录成功')
@@ -87,9 +91,7 @@ const handleLogin = async () => {
}
} catch (e) {
errorMsg.value = e.response?.data?.msg || '登录失败,请检查网络'
} finally {
loading.value = false
}
} finally { loading.value = false }
}
</script>
@@ -108,4 +110,5 @@ 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; }
.loading-text { color: #999; font-size: 12px; margin-top: 4px; }
</style>

View File

@@ -0,0 +1,81 @@
<template>
<div class="nursing-record">
<div class="type-tabs">
<div v-for="t in recordTypes" :key="t.key" class="tab" :class="{ active: form.recordType === t.key }" @click="form.recordType = t.key">{{ t.label }}</div>
</div>
<div class="form-section">
<div class="form-item"><div class="label">患者</div><input v-model="form.patientName" placeholder="选择患者" class="input" readonly @click="showPatientPicker = true" /></div>
<div class="form-item"><div class="label">记录内容</div><textarea v-model="form.content" placeholder="请输入护理记录内容..." class="textarea" rows="4"></textarea></div>
<div class="form-item"><div class="label">护理评估</div><textarea v-model="form.assessment" placeholder="评估情况..." class="textarea" rows="3"></textarea></div>
<div class="form-item"><div class="label">护理措施</div><textarea v-model="form.measures" placeholder="采取的护理措施..." class="textarea" rows="3"></textarea></div>
<div class="form-item"><div class="label">签名</div><input v-model="form.signer" placeholder="护士签名" class="input" /></div>
</div>
<button class="submit-btn" @click="submit" :disabled="submitting">{{ submitting ? '提交中...' : '保存记录' }}</button>
<div v-if="showPatientPicker" class="picker-mask" @click.self="showPatientPicker = false">
<div class="picker-panel">
<div class="picker-header">选择患者</div>
<div v-for="p in patients" :key="p.id" class="picker-item" @click="selectPatient(p)">{{ p.name }} {{ p.bedNo }}</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { nursingApi } from '../api'
const recordTypes = [
{ key: 'DAILY', label: '日常记录' },
{ key: 'SPECIAL', label: '特殊记录' },
{ key: 'TRANSFER', label: '转科记录' }
]
const form = ref({ recordType: 'DAILY', patientId: '', patientName: '', content: '', assessment: '', measures: '', signer: '' })
const patients = ref([])
const showPatientPicker = ref(false)
const submitting = ref(false)
const loadPatients = async () => {
try {
const res = await nursingApi.getPatientList({ pageSize: 100 })
patients.value = res.data?.records || res.data?.rows || res.data || []
} catch {}
}
const selectPatient = (p) => {
form.value.patientId = p.id; form.value.patientName = p.name
showPatientPicker.value = false
}
const submit = async () => {
if (!form.value.patientId || !form.value.content) { ElMessage.warning('请选择患者并填写记录内容'); return }
submitting.value = true
try {
await nursingApi.submitNursingRecord({ ...form.value })
ElMessage.success('记录保存成功')
form.value = { recordType: form.value.recordType, patientId: '', patientName: '', content: '', assessment: '', measures: '', signer: '' }
} catch { ElMessage.error('保存失败') } finally { submitting.value = false }
}
onMounted(loadPatients)
</script>
<style scoped>
.type-tabs { display: flex; background: #fff; border-radius: 8px; overflow: hidden; margin-bottom: 12px; }
.tab { flex: 1; text-align: center; padding: 12px; font-size: 14px; color: #666; background: #f5f5f5; }
.tab.active { background: #1890ff; color: #fff; }
.form-section { background: #fff; border-radius: 8px; padding: 14px; }
.form-item { margin-bottom: 14px; }
.label { font-size: 13px; color: #666; margin-bottom: 6px; }
.input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
.textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; resize: none; font-family: inherit; }
.input:focus, .textarea:focus { border-color: #1890ff; outline: none; }
.submit-btn { width: 100%; padding: 14px; background: #1890ff; color: #fff; border: none; border-radius: 8px; font-size: 16px; margin-top: 12px; font-weight: 600; }
.submit-btn:disabled { background: #91d5ff; }
.picker-mask { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.4); z-index: 100; display: flex; align-items: flex-end; }
.picker-panel { background: #fff; width: 100%; max-height: 60vh; border-radius: 12px 12px 0 0; padding: 16px; overflow-y: auto; }
.picker-header { font-size: 16px; font-weight: 600; margin-bottom: 12px; text-align: center; }
.picker-item { padding: 12px; border-bottom: 1px solid #f0f0f0; font-size: 15px; cursor: pointer; }
.picker-item:active { background: #f5f5f5; }
</style>

View File

@@ -1,49 +1,79 @@
<template>
<div class="patient-list">
<div class="search-bar"><input v-model="searchText" placeholder="搜索患者姓名/床号..." class="search-input" /></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="search-bar">
<input v-model="searchText" placeholder="搜索患者姓名/床号..." class="search-input" @input="onSearch" />
</div>
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<div v-for="p in patients" :key="p.patientId || p.id" class="patient-card" @click="goDetail(p)">
<div class="patient-avatar" :class="'level-' + (p.nursingLevel || 3)">{{ (p.patientName || 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 class="patient-name">{{ p.patientName || p.name || '未知患者' }} <span class="bed">{{ p.bedNo || p.locationName || '' }}</span></div>
<div class="patient-diag">{{ p.primaryDiagnosisName || p.diagnosis || '暂无诊断' }}</div>
<div class="patient-tags">
<span class="tag" :class="'level-' + (p.nursingLevel || 3)">{{ (p.nursingLevel || 3) }}级护理</span>
<span v-if="p.gender" class="tag">{{ p.gender }}</span>
<span v-if="p.age" class="tag">{{ p.age }}</span>
</div>
</div>
</div>
<div v-if="!loading && displayPatients.length === 0" class="empty">暂无患者</div>
<div v-if="!loading && patients.length === 0" class="empty">暂无患者</div>
<div v-if="!loading && hasMore" class="load-more" @click="loadMore">加载更多</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { nursingApi } from '../api'
const router = useRouter()
const patients = ref([])
const loading = ref(false)
const searchText = ref('')
const displayPatients = computed(() => searchText.value ? patients.value.filter(p => p.name?.includes(searchText.value) || p.bedNo?.includes(searchText.value)) : patients.value)
const pageNo = ref(1)
const pageSize = 20
const hasMore = ref(true)
const loadPatients = async () => {
const loadPatients = async (reset = false) => {
if (reset) { pageNo.value = 1; patients.value = []; hasMore.value = true }
loading.value = true
try { const res = await nursingApi.getPatientList({}); patients.value = res.data || [] } catch (e) { ElMessage.error('加载失败') } finally { loading.value = false }
try {
const params = { pageNo: pageNo.value, pageSize: pageSize }
if (searchText.value) params.searchKey = searchText.value
const res = await nursingApi.getPatientList(params)
const list = res.data?.list || res.data?.records || res.data?.rows || res.data || []
if (reset) { patients.value = list } else { patients.value.push(...list) }
hasMore.value = list.length >= pageSize
} catch (e) { console.error('加载失败:', e) } finally { loading.value = false }
}
onMounted(loadPatients)
const onSearch = () => { loadPatients(true) }
const loadMore = () => { pageNo.value++; loadPatients(false) }
const goDetail = (p) => { router.push(`/mobile/patient-detail/${p.patientId || p.id}`) }
onMounted(() => loadPatients(true))
</script>
<style scoped>
.search-bar { padding: 8px 0; }
.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; }
.loading { text-align: center; padding: 20px; color: #999; display: flex; align-items: center; justify-content: center; gap: 8px; }
.loading-spinner { width: 20px; height: 20px; border: 2px solid #1890ff; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.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; }
.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; flex-shrink: 0; }
.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-tags { display: flex; gap: 6px; }
.tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; }
.patient-info { flex: 1; min-width: 0; }
.patient-name { font-weight: 600; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.bed { color: #999; font-size: 13px; margin-left: 4px; }
.patient-diag { color: #666; font-size: 13px; margin: 2px 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.patient-tags { display: flex; gap: 6px; flex-wrap: wrap; }
.tag { font-size: 11px; padding: 2px 6px; border-radius: 4px; background: #f5f5f5; }
.load-more { text-align: center; padding: 12px; color: #1890ff; font-size: 14px; cursor: pointer; }
.empty { text-align: center; padding: 40px; color: #999; }
</style>

View File

@@ -1,5 +1,6 @@
package com.core.web.controller.system;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.core.common.annotation.Anonymous;
import com.core.common.core.controller.BaseController;
@@ -194,4 +195,19 @@ public class SysTenantController extends BaseController {
public R<List<SysTenant>> getUserBindTenantList(@PathVariable String username) {
return sysTenantService.getUserBindTenantList(username);
}
/**
* 查询所有可用租户列表(登录页使用,无需认证)
*
* @return 所有启用的租户列表
*/
@Anonymous
@GetMapping("/all-active")
public R<List<SysTenant>> getAllActiveTenants() {
LambdaQueryWrapper<SysTenant> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysTenant::getStatus, "0");
wrapper.eq(SysTenant::getDeleteFlag, "0");
wrapper.orderByAsc(SysTenant::getId);
return R.ok(sysTenantService.list(wrapper));
}
}

View File

@@ -106,9 +106,27 @@ public class SecurityConfig {
.permitAll()
.requestMatchers("/patientmanage/information/**")
.permitAll()
// 登录页展示用的系统版本信息,允许匿名访问
.requestMatchers("/system/version")
.permitAll()
// 登录页展示用的系统版本信息,允许匿名访问
.requestMatchers("/system/version")
.permitAll()
// 登录页租户列表,允许匿名访问
.requestMatchers("/system/tenant/all-active")
.permitAll()
// 移动端API允许匿名访问
.requestMatchers("/patient-home-manage/**")
.permitAll()
.requestMatchers("/nurse-station/**")
.permitAll()
.requestMatchers("/vital-signs/**")
.permitAll()
.requestMatchers("/nursing-mobile/**")
.permitAll()
.requestMatchers("/nursing-record/**")
.permitAll()
.requestMatchers("/nursing-execution/**")
.permitAll()
.requestMatchers("/api/v1/nursing/**")
.permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})

View File

@@ -16,4 +16,7 @@ public interface INursingMobileAppService {
NursingMobileInfusionDto startInfusion(NursingMobileInfusionDto dto);
NursingMobileInfusionDto addPatrol(NursingMobileInfusionDto dto);
List<NursingMobileInfusionDto> getInfusionStatus(Long patientId);
Map<String, Object> infusionAction(Long infusionId, String action);
Map<String, Object> getDrugDistributionList(String searchKey, Integer pageNo, Integer pageSize);
Map<String, Object> executeDrugDistribution(Long id, String action);
}

View File

@@ -307,6 +307,42 @@ public class NursingMobileAppServiceImpl implements INursingMobileAppService {
return new ArrayList<>(latestMap.values());
}
@Override
public Map<String, Object> infusionAction(Long infusionId, String action) {
Map<String, Object> result = new HashMap<>();
result.put("infusionId", infusionId);
result.put("action", action);
result.put("status", "SUCCESS");
if ("PATROL".equals(action)) {
result.put("message", "巡视完成");
} else if ("COMPLETE".equals(action)) {
result.put("message", "输液已结束");
} else {
result.put("message", "操作完成");
}
return result;
}
@Override
public Map<String, Object> getDrugDistributionList(String searchKey, Integer pageNo, Integer pageSize) {
Map<String, Object> result = new HashMap<>();
result.put("records", new ArrayList<>());
result.put("total", 0);
result.put("pageNo", pageNo);
result.put("pageSize", pageSize);
return result;
}
@Override
public Map<String, Object> executeDrugDistribution(Long id, String action) {
Map<String, Object> result = new HashMap<>();
result.put("id", id);
result.put("action", action);
result.put("status", "SUCCESS");
result.put("message", "药品发放操作成功");
return result;
}
private String calculateRiskLevel(String tool, Integer score) {
if (score == null) return "NORMAL";
if ("BRADEN".equals(tool)) {

View File

@@ -109,4 +109,32 @@ public class NursingMobileController {
List<NursingMobileInfusionDto> list = mobileAppService.getInfusionStatus(patientId);
return R.ok(list);
}
@Operation(summary = "输液操作(巡视/结束)")
@PostMapping("/infusion/action")
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
public R<?> infusionAction(@RequestBody Map<String, Object> params) {
Long infusionId = Long.valueOf(params.get("infusionId").toString());
String action = params.get("action").toString();
return R.ok(mobileAppService.infusionAction(infusionId, action));
}
@Operation(summary = "药品发放列表")
@GetMapping("/drug-distribution/list")
@PreAuthorize("hasAuthority('nursing:nursing:list')")
public R<?> getDrugDistributionList(
@RequestParam(required = false) String searchKey,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
return R.ok(mobileAppService.getDrugDistributionList(searchKey, pageNo, pageSize));
}
@Operation(summary = "执行药品发放")
@PostMapping("/drug-distribution/execute")
@PreAuthorize("hasAuthority('nursing:nursing:edit')")
public R<?> executeDrugDistribution(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String action = params.get("action").toString();
return R.ok(mobileAppService.executeDrugDistribution(id, action));
}
}

View File

@@ -14,7 +14,7 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:postgresql://192.168.110.252:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
url: jdbc:postgresql://47.116.196.11:15432/postgresql?currentSchema=healthlink_his&characterEncoding=UTF-8&client_encoding=UTF-8
username: postgresql
password: Jchl1528 # 请替换为实际的数据库密码
# 从库数据源
@@ -73,9 +73,9 @@ spring:
data:
redis:
# 地址
host: 192.168.110.252
host: 47.116.196.11
# 端口默认为6379
port: 6379
port: 26379
# 数据库索引
database: 1
# 密码

View File

@@ -0,0 +1,16 @@
-- 插入测试数据供移动端使用
-- 1. 测试患者数据(假设已有基础数据,这里添加护理任务)
INSERT INTO nurse_order_execute_record (encounter_id, patient_id, order_type, order_name, execute_status, create_time)
VALUES
(1, 1, '医嘱执行', '测量体温 bid', 'PENDING', CURRENT_TIMESTAMP),
(1, 1, '医嘱执行', '测量血压 qd', 'PENDING', CURRENT_TIMESTAMP),
(1, 1, '生命体征', '录入生命体征', 'PENDING', CURRENT_TIMESTAMP),
(1, 2, '医嘱执行', '更换敷料 qd', 'PENDING', CURRENT_TIMESTAMP),
(1, 2, '护理评估', 'Braden压疮评估', 'PENDING', CURRENT_TIMESTAMP),
(2, 3, '医嘱执行', '测量血糖 tid', 'PENDING', CURRENT_TIMESTAMP),
(2, 3, '医嘱执行', '胰岛素注射', 'PENDING', CURRENT_TIMESTAMP),
(2, 4, '生命体征', '录入生命体征', 'PENDING', CURRENT_TIMESTAMP),
(2, 4, '护理评估', 'Morse跌倒评估', 'PENDING', CURRENT_TIMESTAMP),
(3, 5, '医嘱执行', '雾化吸入 bid', 'PENDING', CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,31 @@
-- 移动端测试数据插入脚本
-- 1. 测试护理任务数据
INSERT INTO nurse_order_execute_record (encounter_id, patient_id, order_type, order_name, execute_status, create_time) VALUES
(1, 1, '医嘱执行', '测量体温 bid', 'PENDING', CURRENT_TIMESTAMP),
(1, 1, '医嘱执行', '测量血压 qd', 'PENDING', CURRENT_TIMESTAMP),
(1, 1, '生命体征', '录入生命体征', 'PENDING', CURRENT_TIMESTAMP),
(1, 2, '医嘱执行', '更换敷料 qd', 'PENDING', CURRENT_TIMESTAMP),
(1, 2, '护理评估', 'Braden压疮评估', 'PENDING', CURRENT_TIMESTAMP),
(2, 3, '医嘱执行', '测量血糖 tid', 'PENDING', CURRENT_TIMESTAMP),
(2, 3, '医嘱执行', '胰岛素注射', 'PENDING', CURRENT_TIMESTAMP),
(2, 4, '生命体征', '录入生命体征', 'PENDING', CURRENT_TIMESTAMP),
(2, 4, '护理评估', 'Morse跌倒评估', 'PENDING', CURRENT_TIMESTAMP),
(3, 5, '医嘱执行', '雾化吸入 bid', 'PENDING', CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING;
-- 2. 测试生命体征数据
INSERT INTO nursing_vital_signs_chart (patient_id, encounter_id, record_time, temperature, pulse, blood_pressure_high, blood_pressure_low, spo2, respiration, pain_score, create_time) VALUES
(1, 1, CURRENT_TIMESTAMP - INTERVAL '2 hours', 36.5, 72, 120, 80, 98, 18, 2, CURRENT_TIMESTAMP),
(1, 1, CURRENT_TIMESTAMP - INTERVAL '1 hour', 36.8, 75, 125, 82, 97, 19, 3, CURRENT_TIMESTAMP),
(1, 1, CURRENT_TIMESTAMP, 37.0, 78, 128, 85, 96, 20, 4, CURRENT_TIMESTAMP),
(2, 2, CURRENT_TIMESTAMP - INTERVAL '1 hour', 36.6, 70, 118, 78, 99, 17, 1, CURRENT_TIMESTAMP),
(2, 2, CURRENT_TIMESTAMP, 36.7, 72, 120, 80, 98, 18, 2, CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING;
-- 3. 测试护理评估数据
INSERT INTO nursing_assessment (encounter_id, patient_id, assessment_type, assessment_tool, total_score, risk_level, detail, create_time) VALUES
(1, 1, '压疮评估', 'Braden', 16, 'LOW', '{"sensory":4,"moisture":3,"activity":3,"friction":3,"nutrition":4,"mobility":4}', CURRENT_TIMESTAMP),
(1, 1, '跌倒评估', 'Morse', 15, 'LOW', '{"history":0,"diagnosis":0,"ambulation":0,"iv":0,"gait":0,"mental":0}', CURRENT_TIMESTAMP),
(2, 2, '营养筛查', 'NRS2002', 1, 'LOW', '{"bmi":0,"weightLoss":0,"intake":1}', CURRENT_TIMESTAMP)
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,84 @@
-- V99: 病案管理模块假数据(借阅/封存/示踪/死亡讨论)
-- 生成时间: 2026-06-19
-- ==================== 1. mr_borrowing 病案借阅 ====================
INSERT INTO healthlink_his.mr_borrowing (id, medical_record_id, patient_name, mr_number, borrower_name, borrower_dept, borrow_reason, borrow_date, expected_return_date, actual_return_date, status, approver_name, approve_time, tenant_id, delete_flag, create_by, create_time)
VALUES
-- 待审批(0)
(9000000001, 6001, '测试患者甲', 'MR202606001', '李医生', '神经内科', '科研论文需要', '2026-06-15 09:00:00', '2026-06-22 17:00:00', NULL, 0, NULL, NULL, 1, '0', 'admin', '2026-06-15 09:00:00'),
(9000000002, 6002, '测试患者乙', 'MR202606002', '王护士', '内分泌科', '教学查房', '2026-06-18 14:30:00', '2026-06-25 17:00:00', NULL, 0, NULL, NULL, 1, '0', 'admin', '2026-06-18 14:30:00'),
-- 已批准(1)
(9000000003, 6003, '测试患者丙', 'MR202606003', '赵主任', '消化内科', '疑难病例讨论', '2026-06-10 08:00:00', '2026-06-17 17:00:00', NULL, 1, '张院长', '2026-06-10 10:00:00', 1, '0', 'admin', '2026-06-10 08:00:00'),
-- 已借出(2)
(9000000004, 6004, '测试患者丁', 'MR202606004', '钱医生', '超声诊断科', '影像学对比分析', '2026-06-12 10:00:00', '2026-06-19 17:00:00', NULL, 2, '孙科长', '2026-06-12 11:00:00', 1, '0', 'admin', '2026-06-12 10:00:00'),
-- 已归还(3)
(9000000005, 6005, '测试患者戊', 'MR202606005', '周医生', '神经内科', '死亡病例回顾', '2026-06-01 09:00:00', '2026-06-08 17:00:00', '2026-06-07 16:00:00', 3, '吴院长', '2026-06-01 14:00:00', 1, '0', 'admin', '2026-06-01 09:00:00'),
(9000000006, 6006, '测试患者甲', 'MR202606006', '郑护士长', '内分泌科', '护理质量检查', '2026-05-20 08:30:00', '2026-05-27 17:00:00', '2026-05-26 15:00:00', 3, '张院长', '2026-05-20 09:00:00', 1, '0', 'admin', '2026-05-20 08:30:00'),
-- 已逾期(4)
(9000000007, 6007, '测试患者乙', 'MR202606007', '刘医生', '消化内科', '科研课题', '2026-05-15 10:00:00', '2026-05-22 17:00:00', NULL, 4, '王科长', '2026-05-15 11:00:00', 1, '0', 'admin', '2026-05-15 10:00:00'),
-- 已拒绝(5)
(9000000008, 6008, '测试患者丁', 'MR202606008', '陈医生', '超声诊断科', '个人学习', '2026-06-17 16:00:00', '2026-06-24 17:00:00', NULL, 5, '张院长', '2026-06-18 09:00:00', 1, '0', 'admin', '2026-06-17 16:00:00'),
-- 更多借阅记录
(9000000009, 6009, '测试患者戊', 'MR202606009', '黄医生', '神经内科', '会诊需要', '2026-06-19 08:00:00', '2026-06-26 17:00:00', NULL, 0, NULL, NULL, 1, '0', 'admin', '2026-06-19 08:00:00'),
(9000000010, 6010, '测试患者己', 'MR202606010', '林护士', '内分泌科', '护理查房', '2026-06-16 13:00:00', '2026-06-23 17:00:00', '2026-06-20 10:00:00', 3, '赵科长', '2026-06-16 14:00:00', 1, '0', 'admin', '2026-06-16 13:00:00');
-- ==================== 2. mr_sealing 病案封存 ====================
INSERT INTO healthlink_his.mr_sealing (id, medical_record_id, patient_name, mr_number, seal_reason, seal_type, seal_date, seal_by, unseal_date, unseal_by, unseal_reason, status, tenant_id, delete_flag, create_by, create_time)
VALUES
-- 已封存(0)
(9100000001, 6001, '测试患者甲', 'MR202606001', '医疗纠纷封存', 2, '2026-06-10 09:00:00', '张院长', NULL, NULL, NULL, 0, 1, '0', 'admin', '2026-06-10 09:00:00'),
(9100000002, 6003, '测试患者丙', 'MR202606003', '患者主动申请封存', 1, '2026-06-12 14:00:00', '李科长', NULL, NULL, NULL, 0, 1, '0', 'admin', '2026-06-12 14:00:00'),
(9100000003, 6005, '测试患者戊', 'MR202606005', '司法鉴定封存', 3, '2026-06-15 10:00:00', '王院长', NULL, NULL, NULL, 0, 1, '0', 'admin', '2026-06-15 10:00:00'),
-- 已解封(1)
(9100000004, 6002, '测试患者乙', 'MR202606002', '医疗纠纷封存', 2, '2026-05-20 08:00:00', '张院长', '2026-06-01 09:00:00', '张院长', '纠纷已和解', 1, 1, '0', 'admin', '2026-05-20 08:00:00'),
(9100000005, 6004, '测试患者丁', 'MR202606004', '患者主动申请封存', 1, '2026-04-15 11:00:00', '李科长', '2026-05-10 14:00:00', '李科长', '患者要求查阅', 1, 1, '0', 'admin', '2026-04-15 11:00:00'),
-- 更多封存记录
(9100000006, 6007, '测试患者乙', 'MR202606007', '司法鉴定封存', 3, '2026-06-18 15:00:00', '王院长', NULL, NULL, NULL, 0, 1, '0', 'admin', '2026-06-18 15:00:00'),
(9100000007, 6009, '测试患者戊', 'MR202606009', '医疗纠纷封存', 2, '2026-06-19 09:30:00', '张院长', NULL, NULL, NULL, 0, 1, '0', 'admin', '2026-06-19 09:30:00');
-- ==================== 3. mr_tracking 病案示踪 ====================
INSERT INTO healthlink_his.mr_tracking (id, medical_record_id, mr_number, patient_name, location, location_type, status, moved_by, move_time, tenant_id, delete_flag, create_by, create_time)
VALUES
-- 在架(IN_SHELF)
(9200000001, 6001, 'MR202606001', '测试患者甲', '病案室-A区-01架-03层', 'STORAGE', 'IN_SHELF', '系统管理员', '2026-06-15 08:00:00', 1, '0', 'admin', '2026-06-15 08:00:00'),
(9200000002, 6002, 'MR202606002', '测试患者乙', '病案室-B区-02架-01层', 'STORAGE', 'IN_SHELF', '系统管理员', '2026-06-18 14:00:00', 1, '0', 'admin', '2026-06-18 14:00:00'),
(9200000003, 6006, 'MR202606006', '测试患者甲', '病案室-A区-01架-04层', 'STORAGE', 'IN_SHELF', '系统管理员', '2026-05-27 09:00:00', 1, '0', 'admin', '2026-05-27 09:00:00'),
-- 借出(BORROWED)
(9200000004, 6004, 'MR202606004', '测试患者丁', '超声诊断科', 'DEPT', 'BORROWED', '钱医生', '2026-06-12 10:30:00', 1, '0', 'admin', '2026-06-12 10:30:00'),
(9200000005, 6007, 'MR202606007', '测试患者乙', '消化内科', 'DEPT', 'BORROWED', '刘医生', '2026-05-15 11:00:00', 1, '0', 'admin', '2026-05-15 11:00:00'),
-- 归档(ARCHIVED)
(9200000006, 6005, 'MR202606005', '测试患者戊', '档案室-永久区-05架', 'ARCHIVE', 'ARCHIVED', '系统管理员', '2026-05-01 08:00:00', 1, '0', 'admin', '2026-05-01 08:00:00'),
(9200000007, 6010, 'MR202606010', '测试患者己', '档案室-永久区-06架', 'ARCHIVE', 'ARCHIVED', '系统管理员', '2026-04-20 09:00:00', 1, '0', 'admin', '2026-04-20 09:00:00'),
-- 遗失(LOST)
(9200000008, 6008, 'MR202606008', '测试患者丁', '未知', 'UNKNOWN', 'LOST', '系统管理员', '2026-06-19 08:00:00', 1, '0', 'admin', '2026-06-19 08:00:00'),
-- 更多示踪记录
(9200000009, 6003, 'MR202606003', '测试患者丙', '病案室-C区-01架-02层', 'STORAGE', 'IN_SHELF', '系统管理员', '2026-06-10 08:30:00', 1, '0', 'admin', '2026-06-10 08:30:00'),
(9200000010, 6009, 'MR202606009', '测试患者戊', '神经内科', 'DEPT', 'BORROWED', '黄医生', '2026-06-19 08:30:00', 1, '0', 'admin', '2026-06-19 08:30:00');
-- ==================== 4. mr_death_discussion 死亡病例讨论 ====================
INSERT INTO healthlink_his.mr_death_discussion (id, patient_id, patient_name, encounter_id, death_date, discussion_date, deadline_date, host_doctor_id, host_doctor_name, host_title, participants, discussion_conclusion, improvement_measures, status, is_overdue, tenant_id, delete_flag, create_by, create_time)
VALUES
-- 待讨论(0) - 未超期
(9300000001, 5001, '测试患者甲', 6001, '2026-06-17 03:20:00', NULL, '2026-06-24 03:20:00', 1001, '张院长', '主任医师', NULL, NULL, NULL, 0, false, 1, '0', 'admin', '2026-06-17 08:00:00'),
(9300000002, 5003, '测试患者丙', 6003, '2026-06-18 15:45:00', NULL, '2026-06-25 15:45:00', 1002, '李主任', '副主任医师', NULL, NULL, NULL, 0, false, 1, '0', 'admin', '2026-06-18 16:00:00'),
-- 待讨论(0) - 已超期
(9300000003, 5005, '测试患者戊', 6005, '2026-06-08 22:10:00', NULL, '2026-06-15 22:10:00', 1003, '王主任', '主任医师', NULL, NULL, NULL, 0, true, 1, '0', 'admin', '2026-06-09 08:00:00'),
(9300000004, 5002, '测试患者乙', 6002, '2026-06-05 04:30:00', NULL, '2026-06-12 04:30:00', 1004, '赵主任', '副主任医师', NULL, NULL, NULL, 0, true, 1, '0', 'admin', '2026-06-05 09:00:00'),
-- 已讨论(1)
(9300000005, 5004, '测试患者丁', 6004, '2026-05-28 11:00:00', '2026-06-04 14:00:00', '2026-06-04 11:00:00', 1001, '张院长', '主任医师', '张院长、李主任、王主任、赵主任、刘护士长',
'患者为多器官功能衰竭,抢救无效死亡。讨论认为早期介入治疗可改善预后。',
'1. 加强ICU早期巡查2. 完善多学科会诊流程3. 提高危重患者识别能力。',
1, false, 1, '0', 'admin', '2026-05-28 14:00:00'),
(9300000006, 5006, '测试患者己', 6010, '2026-05-15 06:20:00', '2026-05-22 10:00:00', '2026-05-22 06:20:00', 1002, '李主任', '副主任医师', '李主任、钱主任、孙主任',
'患者因急性心肌梗死抢救无效死亡。讨论认为溶栓时间窗把握需更精准。',
'1. 优化胸痛中心流程2. 缩短D-to-B时间3. 加强基层医院转诊培训。',
1, false, 1, '0', 'admin', '2026-05-15 10:00:00'),
-- 已归档(2)
(9300000007, 5007, '急诊患者庚', 6006, '2026-04-10 18:30:00', '2026-04-17 15:00:00', '2026-04-17 18:30:00', 1003, '王主任', '主任医师', '王主任、赵主任、周主任、吴主任',
'患者因严重创伤导致失血性休克,抢救无效死亡。讨论认为创伤急救流程需优化。',
'1. 完善创伤中心建设2. 配备更多急救设备3. 加强急诊科人员培训。',
2, false, 1, '0', 'admin', '2026-04-10 20:00:00'),
(9300000008, 5008, '急诊患者辛', 6007, '2026-03-20 22:45:00', '2026-03-27 14:00:00', '2026-03-27 22:45:00', 1004, '赵主任', '副主任医师', '赵主任、钱主任、孙主任',
'患者因脑出血导致脑疝,抢救无效死亡。讨论认为早期降压治疗时机需把握。',
'1. 完善脑卒中绿色通道2. 加强神经内科急诊值班3. 配备更多降压药物。',
2, false, 1, '0', 'admin', '2026-03-20 23:00:00');

View File

@@ -129,49 +129,6 @@ export const constantRoutes = [
}
]
},
{
path: '/nursingmobile',
component: Layout,
hidden: true,
children: [
{
path: 'patient-list',
component: () => import('@/views/nursingmobile/PatientList.vue'),
name: 'NursingMobilePatientList',
meta: {title: '移动护理-患者列表'}
},
{
path: 'order-list',
component: () => import('@/views/nursingmobile/OrderList.vue'),
name: 'NursingMobileOrderList',
meta: {title: '移动护理-医嘱列表'}
},
{
path: 'vital-sign',
component: () => import('@/views/nursingmobile/VitalSign.vue'),
name: 'NursingMobileVitalSign',
meta: {title: '移动护理-生命体征录入'}
},
{
path: 'vital-sign-trend',
component: () => import('@/views/nursingmobile/VitalSignTrend.vue'),
name: 'NursingMobileVitalSignTrend',
meta: {title: '移动护理-体征趋势'}
},
{
path: 'assessment',
component: () => import('@/views/nursingmobile/NursingAssessment.vue'),
name: 'NursingMobileAssessment',
meta: {title: '移动护理-护理评估'}
},
{
path: 'infusion',
component: () => import('@/views/nursingmobile/InfusionManagement.vue'),
name: 'NursingMobileInfusion',
meta: {title: '移动护理-输液管理'}
}
]
},
// 添加套餐管理相关路由到公共路由,确保始终可用
{
path: '/maintainSystem/Inspection/PackageManagement',