feat(surgery): 完善手术管理功能模块

- 添加手术申请相关API接口,包括根据患者ID查询就诊列表功能
- 在医生工作站界面集成手术申请功能选项卡
- 实现手术管理页面的完整功能,包括手术申请的增删改查
- 添加手术排期、开始、完成等状态流转功能
- 优化手术管理页面表格展示,增加手术类型、等级、计划时间等字段
- 实现手术申请表单的完整编辑和查看模式
- 集成患者信息和就诊记录关联功能
- 添加手术室、医生、护士等资源选择功能
- 更新系统依赖配置,添加core-common模块
- 优化图标资源和manifest配置文件
- 调整患者档案和门诊记录相关状态枚举
This commit is contained in:
2026-01-06 16:23:15 +08:00
parent fa2884b320
commit b0850257c8
66 changed files with 7683 additions and 313 deletions

View File

@@ -0,0 +1,715 @@
<!--
* @Description: 门诊手术申请
-->
<template>
<div class="surgery-application-container">
<!-- 顶部操作栏 -->
<el-row :gutter="10" class="mb8 top-operation-bar">
<el-col :span="1.5">
<el-button type="primary" icon="Plus" @click="handleAdd" class="add-button">新增手术申请</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Refresh" @click="handleRefresh" class="refresh-button">刷新</el-button>
</el-col>
</el-row>
<!-- 手术申请记录表格 -->
<el-table
v-loading="loading"
:data="surgeryList"
border
row-key="id"
:row-class-name="getRowClassName"
height="calc(100vh - 250px)"
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<!-- 申请日期 -->
<el-table-column label="申请日期" align="center" prop="createTime" width="180">
<template #default="scope">
{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</template>
</el-table-column>
<!-- 手术单号 -->
<el-table-column label="手术单号" align="center" prop="surgeryNo" width="150" show-overflow-tooltip />
<!-- 患者姓名 -->
<el-table-column label="患者姓名" align="center" prop="patientName" width="100" />
<!-- 申请医生 -->
<el-table-column label="申请医生" align="center" prop="applyDoctorName" width="100" />
<!-- 申请科室 -->
<el-table-column label="申请科室" align="center" prop="applyDeptName" width="120" show-overflow-tooltip />
<!-- 手术名称 -->
<el-table-column label="手术名称" align="center" prop="surgeryName" min-width="150" show-overflow-tooltip />
<!-- 手术等级 -->
<el-table-column label="手术等级" align="center" prop="surgeryLevel_dictText" width="100" />
<!-- 状态 -->
<el-table-column label="状态" align="center" prop="statusEnum_dictText" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.statusEnum)">
{{ scope.row.statusEnum_dictText }}
</el-tag>
</template>
</el-table-column>
<!-- 操作 -->
<el-table-column label="操作" align="center" width="200" fixed="right">
<template #default="scope">
<!-- 查看显示手术申请详情只读模式 -->
<el-button link type="primary" @click="handleView(scope.row)">查看</el-button>
<!-- 编辑修改手术申请信息只有状态为新开的能修改 -->
<el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.statusEnum === 0">编辑</el-button>
<!-- 删除取消手术申请作废 -->
<el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.statusEnum === 0 || scope.row.statusEnum === 1">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增或修改手术申请对话框 -->
<el-dialog
:title="title"
v-model="open"
width="900px"
@close="cancel"
append-to-body
:close-on-click-modal="false"
>
<el-form ref="surgeryRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="id" prop="id" v-show="false">
<el-input v-model="form.id" />
</el-form-item>
<!-- 患者基本信息区 -->
<el-divider content-position="left">患者基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术单号" prop="surgeryNo">
<el-input v-model="form.surgeryNo" disabled placeholder="系统自动生成" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="form.patientName" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="就诊卡号" prop="encounterNo">
<el-input v-model="form.encounterNo" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="patientGender">
<el-select v-model="form.patientGender" disabled placeholder="系统自动获取" style="width: 100%">
<el-option label="男" value="1" />
<el-option label="女" value="2" />
<el-option label="其他" value="9" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="年龄" prop="patientAge">
<el-input-number v-model="form.patientAge" disabled placeholder="系统自动获取" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<!-- 手术信息区 -->
<el-divider content-position="left">手术信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术类型" prop="surgeryTypeEnum">
<el-select v-model="form.surgeryTypeEnum" placeholder="请选择手术类型" style="width: 100%">
<el-option label="门诊手术" :value="1" />
<el-option label="日间手术" :value="2" />
<el-option label="急诊手术" :value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术名称" prop="surgeryName">
<el-input v-model="form.surgeryName" placeholder="请选择手术名称">
<template #append>
<el-button icon="Search" @click="selectSurgeryName" />
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="拟实施手术日期" prop="plannedTime">
<el-date-picker
v-model="form.plannedTime"
type="datetime"
placeholder="选择日期时间"
value-format="YYYY-MM-DDTHH:mm:ss"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 医疗信息区 -->
<el-divider content-position="left">医疗信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术等级" prop="surgeryLevel">
<el-select v-model="form.surgeryLevel" placeholder="请选择手术等级" style="width: 100%">
<el-option label="一级手术" :value="1" />
<el-option label="二级手术" :value="2" />
<el-option label="三级手术" :value="3" />
<el-option label="四级手术" :value="4" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="切口类型" prop="incisionLevel">
<el-select v-model="form.incisionLevel" placeholder="请选择切口类型" style="width: 100%">
<el-option label="I类切口" :value="1" />
<el-option label="II类切口" :value="2" />
<el-option label="III类切口" :value="3" />
<el-option label="IV类切口" :value="4" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="麻醉方式" prop="anesthesiaTypeEnum">
<el-select v-model="form.anesthesiaTypeEnum" placeholder="请选择麻醉方式" style="width: 100%">
<el-option label="局麻" :value="1" />
<el-option label="全麻" :value="3" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 人员信息区 -->
<el-divider content-position="left">人员信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="申请医生" prop="applyDoctorName">
<el-input v-model="form.applyDoctorName" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主刀医生" prop="mainSurgeonId">
<el-select v-model="form.mainSurgeonId" filterable placeholder="请选择主刀医生" style="width: 100%">
<el-option v-for="item in doctorList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手术助手" prop="assistantId">
<el-select v-model="form.assistantId" filterable clearable placeholder="请选择手术助手" style="width: 100%">
<el-option v-for="item in doctorList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="申请科室" prop="applyDeptName">
<el-input v-model="form.applyDeptName" disabled placeholder="系统自动获取" />
</el-form-item>
</el-col>
</el-row>
<!-- 其他信息区 -->
<el-divider content-position="left">其他信息</el-divider>
<el-form-item label="术前诊断" prop="preoperativeDiagnosis">
<el-input v-model="form.preoperativeDiagnosis" disabled placeholder="自动获取门诊诊断的主要诊断名称" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="手术指征" prop="surgeryIndication">
<el-input v-model="form.surgeryIndication" placeholder="请输入手术指征" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="submitForm">提交申请</el-button>
</div>
</template>
</el-dialog>
<!-- 查看手术详情对话框 -->
<el-dialog title="手术申请详情" v-model="viewOpen" width="900px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="手术单号">{{ viewData.surgeryNo }}</el-descriptions-item>
<el-descriptions-item label="申请日期">{{ parseTime(viewData.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
<el-descriptions-item label="患者姓名">{{ viewData.patientName }}</el-descriptions-item>
<el-descriptions-item label="患者信息">{{ viewData.patientGender }} / {{ viewData.patientAge }}</el-descriptions-item>
<el-descriptions-item label="就诊流水号">{{ viewData.encounterNo }}</el-descriptions-item>
<el-descriptions-item label="手术状态">
<el-tag :type="getStatusType(viewData.statusEnum)">{{ viewData.statusEnum_dictText }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="手术名称">{{ viewData.surgeryName }}</el-descriptions-item>
<el-descriptions-item label="手术类型">{{ viewData.surgeryTypeEnum_dictText }}</el-descriptions-item>
<el-descriptions-item label="手术等级">{{ viewData.surgeryLevel_dictText }}</el-descriptions-item>
<el-descriptions-item label="麻醉方式">{{ viewData.anesthesiaTypeEnum_dictText }}</el-descriptions-item>
<el-descriptions-item label="计划时间">{{ viewData.plannedTime }}</el-descriptions-item>
<el-descriptions-item label="主刀医生">{{ viewData.mainSurgeonName }}</el-descriptions-item>
<el-descriptions-item label="申请医生">{{ viewData.applyDoctorName }}</el-descriptions-item>
<el-descriptions-item label="申请科室">{{ viewData.applyDeptName }}</el-descriptions-item>
<el-descriptions-item label="术前诊断" :span="2">{{ viewData.preoperativeDiagnosis }}</el-descriptions-item>
<el-descriptions-item label="手术指征" :span="2">{{ viewData.surgeryIndication }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup name="SurgeryApplication">
import { getCurrentInstance, ref, computed, watch } from 'vue'
import { getSurgeryPage, addSurgery, updateSurgery, deleteSurgery, getSurgeryDetail, updateSurgeryStatus } from '@/api/surgerymanage'
import { getEncounterDiagnosis } from '../api'
import { listUser } from '@/api/system/user'
import useUserStore from '@/store/modules/user'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const props = defineProps({
patientInfo: {
type: Object,
default: () => ({})
},
activeTab: {
type: String,
default: ''
}
})
const loading = ref(true)
const surgeryList = ref([])
const open = ref(false)
const viewOpen = ref(false)
const isEditMode = ref(false)
const form = ref({
id: undefined,
patientId: undefined,
encounterId: undefined,
encounterNo: undefined,
patientName: undefined,
patientGender: undefined,
patientAge: undefined,
applyDoctorName: undefined,
applyDeptName: undefined,
surgeryNo: undefined,
surgeryName: undefined,
surgeryTypeEnum: undefined,
surgeryLevel: undefined,
plannedTime: undefined,
mainSurgeonId: undefined,
assistantId: undefined,
anesthesiaTypeEnum: undefined,
incisionLevel: undefined,
preoperativeDiagnosis: undefined,
surgeryIndication: undefined
})
const surgeryRef = ref()
const viewData = ref({})
const title = ref('')
const doctorList = ref([])
// 字典选项
const surgeryStatusOptions = ref([
{ value: 0, label: '新开' },
{ value: 1, label: '已安排' },
{ value: 2, label: '手术中' },
{ value: 3, label: '已完成' },
{ value: 4, label: '已取消' },
{ value: 5, label: '暂停' }
])
const surgeryTypeOptions = ref([
{ value: 1, label: '门诊手术' },
{ value: 2, label: '日间手术' },
{ value: 3, label: '急诊手术' }
])
const rules = ref({
surgeryName: [{ required: true, message: '请输入手术名称', trigger: 'blur' }],
surgeryTypeEnum: [{ required: true, message: '请选择手术类型', trigger: 'change' }],
surgeryLevel: [{ required: true, message: '请选择手术等级', trigger: 'change' }],
plannedTime: [{ required: true, message: '请选择计划手术时间', trigger: 'change' }],
mainSurgeonId: [{ required: true, message: '请选择主刀医生', trigger: 'change' }],
anesthesiaTypeEnum: [{ required: true, message: '请选择麻醉方式', trigger: 'change' }]
})
// 监听患者信息变化
watch(() => props.patientInfo, (newVal) => {
if (newVal && newVal.encounterId) {
getList()
loadDiagnosisInfo()
loadDoctorList()
}
}, { immediate: true })
// 获取手术申请列表
function getList() {
if (!props.patientInfo?.encounterId) {
surgeryList.value = []
loading.value = false
return
}
loading.value = true
getSurgeryPage({
pageNo: 1,
pageSize: 100,
encounterId: props.patientInfo.encounterId
}).then((res) => {
surgeryList.value = res.data.records || []
}).catch(error => {
console.error('获取手术列表失败:', error)
proxy.$message.error('数据加载失败,请稍后重试')
surgeryList.value = []
}).finally(() => {
loading.value = false
})
}
// 加载诊断信息
function loadDiagnosisInfo() {
if (!props.patientInfo?.encounterId) return
getEncounterDiagnosis(props.patientInfo.encounterId).then((res) => {
if (res.code == 200) {
const datas = res.data || []
const mainDiagnosis = datas.find(item => item?.maindiseFlag == 1)
if (mainDiagnosis) {
form.value.preoperativeDiagnosis = mainDiagnosis.name
}
}
}).catch(error => {
console.error('获取诊断信息失败:', error)
})
}
// 加载医生列表
function loadDoctorList() {
listUser({ pageNo: 1, pageSize: 1000 }).then(res => {
if (res.code === 200) {
doctorList.value = res.data.records || []
console.log('加载医生列表成功,数量:', doctorList.value.length)
} else {
proxy.$modal.msgError('获取医生列表失败')
doctorList.value = []
}
}).catch(error => {
console.error('加载医生列表失败:', error)
proxy.$modal.msgError('获取医生列表失败')
doctorList.value = []
})
}
// 新增
function handleAdd() {
if (!props.patientInfo?.encounterId) {
proxy.$message.warning('请先选择患者')
return
}
title.value = '新增手术申请'
open.value = true
reset()
// 自动填充患者信息
form.value.patientId = props.patientInfo.patientId
form.value.encounterId = props.patientInfo.encounterId
form.value.encounterNo = props.patientInfo.busNo
form.value.patientName = props.patientInfo.patientName
form.value.patientGender = props.patientInfo.genderEnum_enumText
form.value.patientAge = props.patientInfo.age
form.value.applyDoctorName = userStore.nickName
form.value.applyDeptName = props.patientInfo.deptName || ''
// 加载诊断信息
loadDiagnosisInfo()
}
// 编辑
function handleEdit(row) {
if (row.statusEnum !== 0) {
proxy.$modal.msgWarning('当前状态不允许编辑手术,仅新开状态可编辑')
return
}
title.value = '编辑手术申请'
open.value = true
isEditMode.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
Object.assign(form.value, res.data)
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
// 查看
function handleView(row) {
viewOpen.value = true
getSurgeryDetail(row.id).then(res => {
if (res.code === 200) {
viewData.value = res.data
}
}).catch(error => {
console.error('获取手术信息失败:', error)
proxy.$modal.msgError('获取手术信息失败')
})
}
// 删除/取消
function handleDelete(row) {
if (row.statusEnum === 0) {
// 新开状态 - 直接删除
proxy.$modal.confirm('是否确认删除手术"' + row.surgeryName + '"?').then(() => {
return deleteSurgery(row.id)
}).then(() => {
getList()
proxy.$modal.msgSuccess('删除成功')
}).catch(error => {
console.error('删除手术失败:', error)
proxy.$modal.msgError('删除失败')
})
} else if (row.statusEnum === 1) {
// 已安排状态 - 更新为已取消
proxy.$modal.confirm('是否确认取消手术"' + row.surgeryName + '"?').then(() => {
return updateSurgeryStatus(row.id, 4) // 4 = 已取消
}).then(() => {
getList()
proxy.$modal.msgSuccess('手术已取消')
}).catch(error => {
console.error('取消手术失败:', error)
proxy.$modal.msgError('取消失败')
})
} else {
// 其他状态 - 不允许操作
proxy.$modal.msgWarning('当前状态不允许取消手术')
}
}
// 刷新
function handleRefresh() {
getList()
proxy.$modal.msgSuccess('刷新成功')
}
// 选择手术名称
function selectSurgeryName() {
proxy.$modal.msgInfo('请选择手术名称')
// TODO: 实现手术名称选择弹窗
}
// 提交表单
function submitForm() {
proxy.$refs['surgeryRef'].validate((valid) => {
if (valid) {
if (form.value.id == undefined) {
// 新增手术
addSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getList()
}).catch(error => {
console.error('新增手术失败:', error)
proxy.$message.error('新增手术失败,请检查表单信息')
})
} else {
// 修改手术
updateSurgery(form.value).then((res) => {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getList()
}).catch(error => {
console.error('更新手术失败:', error)
proxy.$message.error('更新手术失败,请检查表单信息')
})
}
} else {
proxy.$message.error('请检查表单信息,标红字段为必填项')
}
})
}
// 取消
function cancel() {
open.value = false
reset()
}
// 重置表单
function reset() {
form.value = {
id: undefined,
patientId: undefined,
encounterId: undefined,
encounterNo: undefined,
patientName: undefined,
patientGender: undefined,
patientAge: undefined,
applyDoctorName: undefined,
applyDeptName: undefined,
surgeryNo: undefined,
surgeryName: undefined,
surgeryTypeEnum: undefined,
surgeryLevel: undefined,
plannedTime: undefined,
mainSurgeonId: undefined,
assistantId: undefined,
anesthesiaTypeEnum: undefined,
incisionLevel: undefined,
preoperativeDiagnosis: undefined,
surgeryIndication: undefined
}
if (surgeryRef.value) {
surgeryRef.value.resetFields()
}
}
// 获取状态标签类型
function getStatusType(status) {
const typeMap = {
0: 'info',
1: 'warning',
2: 'primary',
3: 'success',
4: 'danger',
5: 'info'
}
return typeMap[status] || 'info'
}
// 获取表格行样式
function getRowClassName({ row }) {
return row.statusEnum === 4 ? 'cancelled-row' : ''
}
// 时间格式化函数
function parseTime(time, pattern) {
if (!time) return ''
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time)
} else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '')
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
return value.toString().padStart(2, '0')
})
return time_str
}
defineExpose({
getList
})
</script>
<style scoped lang="scss">
.surgery-application-container {
height: 100%;
width: 100%;
padding: 10px;
/* 顶部操作栏样式 */
.top-operation-bar {
height: 60px;
display: flex;
align-items: center;
margin-bottom: 16px;
.add-button {
background-color: #5b8fb9;
color: white;
border-radius: 8px;
padding: 0 20px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(91, 143, 185, 0.3);
}
}
.refresh-button {
background-color: transparent;
border: 1px solid #dcdfe6;
color: #606266;
border-radius: 8px;
padding: 0 20px;
&:hover {
background-color: #f5f7fa;
}
}
}
/* 表格样式 */
.el-table {
::v-deep(.cancelled-row) {
background-color: #f5f5f5;
color: #999;
text-decoration: line-through;
::v-deep(.cell) {
opacity: 0.6;
}
}
}
}
/* 对话框样式 */
.el-dialog {
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.dialog-footer {
text-align: center;
padding: 20px 0;
}
</style>