Files
his/openhis-ui-vue3/src/views/operatingroom/index.vue
zhangfei 9c3e603b94 Fix Bug #443: 手术计费:点击签发耗材时异常报错
当手术计费弹窗中点击"签发"耗材时,因耗材的locationId(发放库房)为空导致后端异常。
在DoctorStationAdviceAppServiceImpl.handDevice方法中,当locationId为null时,使用登录用户的科室ID作为默认值,
与NurseBillingAppService中的处理方式保持一致。
2026-05-08 09:14:18 +08:00

560 lines
17 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<!-- 查询表单 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" class="query-form">
<el-form-item label="手术室名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入手术室名称"
clearable
@keyup.enter="handleQuery"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="状态" prop="statusEnum">
<el-select v-model="queryParams.statusEnum" placeholder="请选择状态" clearable style="width: 150px">
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作栏 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增手术室</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="VideoPlay" :disabled="multiple" @click="handleEnable(ids)">
批量启用
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="VideoPause" :disabled="multiple" @click="handleDisable(ids)">
批量停用
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 数据表格 -->
<el-table
v-loading="loading"
:data="operatingRoomList"
@selection-change="handleSelectionChange"
row-key="id"
:row-class-name="getRowClassName"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" align="center" width="60" />
<el-table-column label="房间号" align="center" prop="busNo" width="120" show-overflow-tooltip />
<el-table-column label="手术室名称" align="center" prop="name" min-width="150" show-overflow-tooltip />
<el-table-column label="类型" align="center" prop="roomTypeEnum_dictText" width="100" />
<el-table-column label="所属科室" align="center" prop="organizationName" width="120" show-overflow-tooltip />
<el-table-column label="位置描述" align="center" prop="locationDescription" min-width="150" show-overflow-tooltip />
<el-table-column label="设备配置" align="center" prop="equipmentConfig" min-width="200" show-overflow-tooltip />
<el-table-column label="容纳人数" align="center" prop="capacity" width="100" />
<el-table-column label="状态" align="center" prop="statusEnum" width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.statusEnum)">
{{ scope.row.statusEnum === 1 ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="显示顺序" align="center" prop="displayOrder" width="100" />
<el-table-column label="操作" align="center" width="220" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
<el-button link type="primary" icon="View" @click="handleView(scope.row)">查看</el-button>
<el-button
v-if="scope.row.statusEnum === 1"
link
type="warning"
icon="VideoPause"
@click="handleDisable([scope.row.id])"
>停用</el-button>
<el-button
v-else
link
type="success"
icon="VideoPlay"
@click="handleEnable([scope.row.id])"
>启用</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 新增或修改手术室对话框 -->
<el-dialog :title="title" v-model="open" width="700px" append-to-body>
<el-form ref="operatingRoomRef" :model="form" :rules="rules" label-width="120px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="房间号" prop="busNo">
<el-input v-model="form.busNo" placeholder="请输入房间号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手术室名称" prop="name">
<el-input v-model="form.name" placeholder="请输入手术室名称" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="容纳人数" prop="capacity">
<el-input-number
v-model="form.capacity"
:min="1"
:max="20"
style="width: 100%"
placeholder="请输入容纳人数"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="类型" prop="roomTypeEnum">
<el-select v-model="form.roomTypeEnum" placeholder="请选择类型" style="width: 100%">
<el-option
v-for="item in roomTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属科室" prop="organizationId">
<el-select
v-model="form.organizationId"
filterable
clearable
placeholder="请选择所属科室"
:loading="deptLoading"
style="width: 100%"
>
<el-option
v-for="item in flatDeptOptions"
: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="statusEnum">
<el-select v-model="form.statusEnum" placeholder="请选择状态" style="width: 100%">
<el-option
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="显示顺序" prop="displayOrder">
<el-input-number
v-model="form.displayOrder"
:min="0"
:max="999"
style="width: 100%"
placeholder="请输入显示顺序"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="位置描述" prop="locationDescription">
<el-input
v-model="form.locationDescription"
type="textarea"
placeholder="请输入位置描述"
:rows="2"
/>
</el-form-item>
<el-form-item label="设备配置" prop="equipmentConfig">
<el-input
v-model="form.equipmentConfig"
type="textarea"
placeholder="请输入设备配置,如:麻醉机、监护仪、手术台等"
:rows="3"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
placeholder="请输入备注"
:rows="2"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
<!-- 查看手术室详情对话框 -->
<el-dialog title="手术室详情" v-model="viewOpen" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="手术室编码">{{ viewData.busNo }}</el-descriptions-item>
<el-descriptions-item label="手术室名称">{{ viewData.name }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ viewData.roomTypeEnum_dictText }}</el-descriptions-item>
<el-descriptions-item label="位置描述">{{ viewData.locationDescription }}</el-descriptions-item>
<el-descriptions-item label="容纳人数">{{ viewData.capacity }}</el-descriptions-item>
<el-descriptions-item label="所属科室">{{ viewData.organizationName }}</el-descriptions-item>
<el-descriptions-item label="显示顺序">{{ viewData.displayOrder }}</el-descriptions-item>
<el-descriptions-item label="设备配置" :span="2">{{ viewData.equipmentConfig }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="getStatusType(viewData.statusEnum)">
{{ viewData.statusEnum === 1 ? '启用' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ viewData.remark }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup name="OperatingRoomManage">
import { listOperatingRoom, getOperatingRoom, addOperatingRoom, updateOperatingRoom, deleteOperatingRoom, enableOperatingRoom, disableOperatingRoom } from '@/api/operatingroom'
import { deptTreeSelect } from '@/api/system/user'
import { computed } from 'vue'
const { proxy } = getCurrentInstance()
const { operating_room_type } = proxy.useDict('operating_room_type')
const loading = ref(true)
const showSearch = ref(true)
const operatingRoomList = ref([])
const ids = ref([])
const single = ref(true)
const multiple = ref(true)
const total = ref(0)
const title = ref('')
const open = ref(false)
const viewOpen = ref(false)
const deptOptions = ref([]) // 科室选项,始终是数组
const deptLoading = ref(false) // 科室数据加载状态
// 将树形科室数据扁平化为一维数组,用于可搜索的下拉框
const flatDeptOptions = computed(() => {
const flatten = (nodes) => {
const result = []
nodes.forEach(node => {
result.push({
id: node.id,
name: node.name
})
if (node.children && node.children.length > 0) {
result.push(...flatten(node.children))
}
})
return result
}
return flatten(deptOptions.value)
})
const queryParams = ref({
pageNo: 1,
pageSize: 10,
name: undefined,
statusEnum: undefined
})
const form = ref({
id: undefined,
busNo: undefined,
name: undefined,
roomTypeEnum: undefined,
organizationId: undefined,
locationDescription: undefined,
equipmentConfig: undefined,
capacity: 1, // 默认值为1而不是undefined
statusEnum: 1,
displayOrder: 0, // 默认值为0
remark: undefined
})
const operatingRoomRef = ref()
const viewData = ref({})
const roomTypeOptions = operating_room_type
const statusOptions = ref([
{ value: 1, label: '启用' },
{ value: 0, label: '停用' }
])
const rules = ref({
busNo: [{ required: true, message: '房间号不能为空', trigger: 'blur' }],
name: [{ required: true, message: '手术室名称不能为空', trigger: 'blur' }],
capacity: [
{ required: true, message: '容纳人数不能为空', trigger: 'blur' },
{ type: 'number', message: '容纳人数必须为数字', trigger: 'blur' }
],
organizationId: [{ required: true, message: '请选择所属科室', trigger: 'change' }]
})
/** 查询手术室列表 */
function getList() {
loading.value = true
const params = { ...queryParams.value }
listOperatingRoom(params).then(res => {
if (res.code === 200) {
operatingRoomList.value = res.data.records || []
total.value = res.data.total || 0
} else {
proxy.$modal.msgError(res.msg || '获取手术室列表失败')
}
}).catch(error => {
console.error('获取手术室列表失败:', error)
proxy.$modal.msgError('获取手术室列表失败,请稍后重试')
}).finally(() => {
loading.value = false
})
}
/** 查询科室树 */
function getDeptTree() {
deptLoading.value = true
deptTreeSelect().then(res => {
if (res.code === 200) {
// 后端返回的是分页对象,需要从 records 中获取实际数据
const data = res.data
if (data && Array.isArray(data.records)) {
deptOptions.value = data.records
} else if (Array.isArray(data)) {
deptOptions.value = data
} else {
deptOptions.value = []
console.warn('科室数据格式不正确:', data)
}
} else {
deptOptions.value = []
console.warn('获取科室树失败:', res.msg)
}
}).catch(error => {
console.error('获取科室树失败:', error)
deptOptions.value = []
}).finally(() => {
deptLoading.value = false
})
}
/** 取消按钮 */
function cancel() {
open.value = false
reset()
}
/** 表单重置 */
function reset() {
form.value = {
id: undefined,
busNo: undefined,
name: undefined,
roomTypeEnum: undefined,
organizationId: undefined,
locationDescription: undefined,
equipmentConfig: undefined,
capacity: 1, // 默认值为1
statusEnum: 1,
displayOrder: 0, // 默认值为0
remark: undefined
}
proxy.resetForm('operatingRoomRef')
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNo = 1
getList()
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm('queryRef')
queryParams.value = {
pageNo: 1,
pageSize: 10,
name: undefined,
statusEnum: undefined
}
handleQuery()
}
/** 多选框选中数据 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
/** 新增按钮操作 */
function handleAdd() {
reset()
open.value = true
title.value = '新增手术室'
}
/** 修改按钮操作 */
function handleEdit(row) {
reset()
const id = row.id
getOperatingRoom(id).then(res => {
if (res.code === 200) {
form.value = res.data
open.value = true
title.value = '编辑手术室'
}
})
}
/** 查看按钮操作 */
function handleView(row) {
const id = row.id
listOperatingRoom({ id }).then(res => {
if (res.code === 200 && res.data.records.length > 0) {
viewData.value = res.data.records[0]
viewOpen.value = true
}
})
}
/** 提交按钮 */
function submitForm() {
proxy.$refs['operatingRoomRef'].validate(valid => {
if (valid) {
// 确保所有必填字段都有值
const submitData = {
...form.value,
name: form.value.name?.trim() || '',
organizationId: form.value.organizationId || null,
capacity: form.value.capacity || 1,
displayOrder: form.value.displayOrder || 0
}
if (form.value.id) {
updateOperatingRoom(submitData).then(res => {
if (res.code === 200) {
proxy.$modal.msgSuccess('修改成功')
open.value = false
getList()
} else {
proxy.$modal.msgError(res.msg || '修改失败')
}
}).catch(error => {
console.error('修改手术室失败:', error)
proxy.$modal.msgError('修改失败,请稍后重试')
})
} else {
addOperatingRoom(submitData).then(res => {
if (res.code === 200) {
proxy.$modal.msgSuccess('新增成功')
open.value = false
getList()
} else {
proxy.$modal.msgError(res.msg || '新增失败')
}
}).catch(error => {
console.error('新增手术室失败:', error)
proxy.$modal.msgError('新增失败,请稍后重试')
})
}
} else {
proxy.$modal.msgError('请完善表单信息')
}
})
}
/** 启用按钮操作 */
function handleEnable(ids) {
proxy.$modal
.confirm('是否确认启用选中的手术室?')
.then(function () {
return enableOperatingRoom(ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('启用成功')
})
.catch(() => {})
}
/** 停用按钮操作 */
function handleDisable(ids) {
proxy.$modal
.confirm('是否确认停用选中的手术室?')
.then(function () {
return disableOperatingRoom(ids)
})
.then(() => {
getList()
proxy.$modal.msgSuccess('停用成功')
})
.catch(() => {})
}
/** 获取状态标签类型 */
function getStatusType(status) {
const typeMap = {
1: 'success',
0: 'info'
}
return typeMap[status] || 'info'
}
/** 获取行样式 */
function getRowClassName({ row }) {
if (row.statusEnum === 0) {
return 'disabled-row'
}
return ''
}
getDeptTree()
getList()
</script>
<style scoped>
.disabled-row {
background-color: #f5f5f5;
color: #999;
}
</style>