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

1051 lines
26 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="dept-appthours-container">
<!-- 页头区域 -->
<div class="page-header">
<h1 class="page-title">科室预约工作时间维护</h1>
</div>
<!-- 筛选控制区 -->
<div class="filter-section">
<el-form :model="queryParams" ref="queryRef" :inline="true" class="filter-row">
<el-form-item label="所属机构" prop="institution" class="filter-item">
<el-select
v-model="queryParams.institution"
placeholder="请选择"
clearable
style="width: 150px"
:popper-append-to-body="false"
>
<el-option
v-for="item in tenantOptions"
:key="item.id"
:label="item.tenantName"
:value="item.tenantName"
/>
</el-select>
</el-form-item>
<el-form-item label="科室名称" prop="department" class="filter-item">
<el-select
v-model="queryParams.department"
placeholder="全部科室"
clearable
style="width: 150px"
:popper-append-to-body="false"
filterable
>
<el-option
v-for="item in departmentOptions"
:key="item.deptId || item.id"
:label="item.deptName || item.name"
:value="item.deptName || item.name"
/>
</el-select>
</el-form-item>
<div class="filter-buttons">
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
</div>
</el-form>
</div>
<!-- 数据表格区 -->
<div class="table-section" v-loading="loading">
<el-table
:data="deptAppthoursList"
border
style="width: 100%"
class="dept-appthours-table"
:height="tableHeight"
:max-height="tableMaxHeight"
stripe
:row-class-name="tableRowClassName"
>
<el-table-column prop="institution" label="所属机构" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="department" label="科室名称" min-width="150" align="center" show-overflow-tooltip />
<el-table-column prop="morningStart" label="上午开始时间" min-width="120" align="center">
<template #default="scope">
{{ formatTime(scope.row.morningStart) }}
</template>
</el-table-column>
<el-table-column prop="morningEnd" label="上午结束时间" min-width="120" align="center">
<template #default="scope">
{{ formatTime(scope.row.morningEnd) }}
</template>
</el-table-column>
<el-table-column prop="afternoonStart" label="下午开始时间" min-width="120" align="center">
<template #default="scope">
{{ formatTime(scope.row.afternoonStart) }}
</template>
</el-table-column>
<el-table-column prop="afternoonEnd" label="下午结束时间" min-width="120" align="center">
<template #default="scope">
{{ formatTime(scope.row.afternoonEnd) }}
</template>
</el-table-column>
<el-table-column prop="quota" label="限号数量" min-width="100" align="center" />
<el-table-column prop="operator" label="操作人" min-width="100" align="center" show-overflow-tooltip />
<el-table-column width="120" align="center" fixed="right">
<template #default="scope">
<el-tooltip content="编辑" placement="top">
<el-button type="primary" link icon="EditPen" @click="handleEdit(scope.row)" />
</el-tooltip>
<el-tooltip content="查看" placement="top">
<el-button type="info" link icon="View" @click="handleView(scope.row)" />
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button type="danger" link icon="Delete" @click="handleDelete(scope.row)" />
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-section">
<span class="pagination-total">总数{{ total }}</span>
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
:small="true"
:background="true"
layout="prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="600px"
append-to-body
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
:disabled="dialogType === 'view'"
>
<el-form-item label="所属机构" prop="institution">
<el-select
v-model="form.institution"
placeholder="请选择"
style="width: 100%"
:disabled="dialogType === 'view'"
>
<el-option
v-for="item in tenantOptions"
:key="item.id"
:label="item.tenantName"
:value="item.tenantName"
/>
</el-select>
</el-form-item>
<el-form-item label="科室名称" prop="department">
<el-select
v-model="form.department"
placeholder="请选择"
style="width: 100%"
filterable
:disabled="dialogType === 'view'"
>
<el-option
v-for="item in departmentOptions"
:key="item.deptId || item.id"
:label="item.deptName || item.name"
:value="item.deptName || item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="上午时段" prop="morningStart">
<el-col :span="11">
<el-time-picker
v-model="form.morningStart"
placeholder="开始时间"
format="HH:mm"
value-format="HH:mm"
style="width: 100%"
/>
</el-col>
<el-col :span="2" style="text-align: center">-</el-col>
<el-col :span="11">
<el-time-picker
v-model="form.morningEnd"
placeholder="结束时间"
format="HH:mm"
value-format="HH:mm"
style="width: 100%"
/>
</el-col>
</el-form-item>
<el-form-item label="下午时段" prop="afternoonStart">
<el-col :span="11">
<el-time-picker
v-model="form.afternoonStart"
placeholder="开始时间"
format="HH:mm"
value-format="HH:mm"
style="width: 100%"
/>
</el-col>
<el-col :span="2" style="text-align: center">-</el-col>
<el-col :span="11">
<el-time-picker
v-model="form.afternoonEnd"
placeholder="结束时间"
format="HH:mm"
value-format="HH:mm"
style="width: 100%"
/>
</el-col>
</el-form-item>
<el-form-item label="限号数量" prop="quota">
<el-input-number
v-model="form.quota"
:min="0"
:max="999"
placeholder="请输入"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button v-if="dialogType !== 'view'" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="DeptAppthoursManage">
import {getCurrentInstance, nextTick, onMounted, onUnmounted, reactive, ref, computed} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {
addDeptAppthours,
deleteDeptAppthours,
getDeptAppthoursDetail,
getDeptAppthoursList,
getDepartmentList,
getTenantList,
updateDeptAppthours
} from '@/api/appoinmentmanage/deptappthoursManage'
import {addLog} from '@/api/monitor/operlog'
import useUserStore from '@/store/modules/user'
const { proxy } = getCurrentInstance()
const userStore = useUserStore()
const loading = ref(false)
const deptAppthoursList = ref([])
const total = ref(0)
const tableHeight = ref(400)
const tableMaxHeight = ref(600)
const isMobile = ref(false)
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
institution: null,
department: null
})
const departmentOptions = ref([])
const tenantOptions = ref([])
const dialogVisible = ref(false)
const dialogType = ref('')
const dialogTitle = ref('')
const formRef = ref()
const form = reactive({
id: null,
institution: null,
department: null,
morningStart: null,
morningEnd: null,
afternoonStart: null,
afternoonEnd: null,
quota: 0,
operator: null
})
const currentEditRow = ref(null)
const rules = {
institution: [
{ required: true, message: '所属机构不能为空', trigger: 'change' }
],
department: [
{ required: true, message: '科室名称不能为空', trigger: 'change' }
],
morningStart: [
{
validator: (rule, value, callback) => {
if (!form.morningStart && !form.morningEnd) {
callback()
} else if (!form.morningStart) {
callback(new Error('请选择上午开始时间'))
} else if (!form.morningEnd) {
callback(new Error('请选择上午结束时间'))
} else if (form.morningStart >= form.morningEnd) {
callback(new Error('上午结束时间必须大于开始时间'))
} else {
callback()
}
},
trigger: 'change'
}
],
afternoonStart: [
{
validator: (rule, value, callback) => {
if (!form.afternoonStart && !form.afternoonEnd) {
callback()
} else if (!form.afternoonStart) {
callback(new Error('请选择下午开始时间'))
} else if (!form.afternoonEnd) {
callback(new Error('请选择下午结束时间'))
} else if (form.afternoonStart >= form.afternoonEnd) {
callback(new Error('下午结束时间必须大于开始时间'))
} else {
callback()
}
},
trigger: 'change'
}
],
quota: [
{ required: true, message: '限号数量不能为空', trigger: 'blur' }
]
}
function getList() {
loading.value = true
getDeptAppthoursList(queryParams)
.then(response => {
if (response.code === 200) {
deptAppthoursList.value = response.data?.records || response.data || []
total.value = response.data?.total || deptAppthoursList.value.length || 0
} else {
ElMessage.error(response.msg || '获取科室预约工作时间列表失败')
}
})
.catch(error => {
console.error('获取科室预约工作时间列表失败:', error)
ElMessage.error('获取科室预约工作时间列表失败')
})
.finally(() => {
loading.value = false
})
}
function handleQuery() {
queryParams.pageNum = 1
getList()
}
function handleSizeChange(val) {
queryParams.pageSize = val
getList()
}
function handleCurrentChange(val) {
queryParams.pageNum = val
getList()
}
function resetQuery() {
proxy.resetForm('queryRef')
queryParams.institution = null
queryParams.department = null
queryParams.pageNum = 1
getList()
}
function handleAdd() {
resetForm()
dialogType.value = 'add'
dialogTitle.value = '新增科室预约时间'
dialogVisible.value = true
}
function handleEdit(row) {
currentEditRow.value = { ...row }
resetForm()
const id = row.id
getDeptAppthoursDetail(id)
.then(response => {
if (response.code === 200) {
const data = response.data || row
form.id = data.id
form.institution = data.institution
form.department = data.department
form.morningStart = data.morningStart
form.morningEnd = data.morningEnd
form.afternoonStart = data.afternoonStart
form.afternoonEnd = data.afternoonEnd
form.quota = data.quota
form.operator = data.operator
dialogType.value = 'edit'
dialogTitle.value = '编辑科室预约时间'
dialogVisible.value = true
} else {
ElMessage.error(response.msg || '获取详情失败')
}
})
.catch(error => {
console.error('获取详情失败:', error)
ElMessage.error('获取详情失败')
})
}
function handleView(row) {
resetForm()
const id = row.id
getDeptAppthoursDetail(id)
.then(response => {
if (response.code === 200) {
const data = response.data || row
form.id = data.id
form.institution = data.institution
form.department = data.department
form.morningStart = data.morningStart
form.morningEnd = data.morningEnd
form.afternoonStart = data.afternoonStart
form.afternoonEnd = data.afternoonEnd
form.quota = data.quota
form.operator = data.operator
dialogType.value = 'view'
dialogTitle.value = '查看科室预约时间详情'
dialogVisible.value = true
} else {
ElMessage.error(response.msg || '获取详情失败')
}
})
.catch(error => {
console.error('获取详情失败:', error)
ElMessage.error('获取详情失败')
})
}
function handleDelete(row) {
const id = row.id || row.ids
ElMessageBox.confirm('是否确认删除该科室预约工作时间数据项?')
.then(async () => {
const deletePromise = deleteDeptAppthours(id)
const response = await deletePromise
if (response.code === 200) {
const currentUser = getCurrentUser()
try {
await addLog({
type: '删除',
operName: currentUser.nickName || currentUser.userName || currentUser.name || '未知用户',
operMethod: 'DELETE',
operParam: JSON.stringify({ id, deptName: row.deptName || row.department }),
operStatus: 1
})
} catch (logError) {
console.warn('操作日志记录失败:', logError)
}
ElMessage.success('删除成功')
getList()
} else {
ElMessage.error(response.msg || '删除失败')
}
})
.catch(() => {})
}
function submitForm() {
formRef.value.validate(valid => {
if (valid) {
const submitData = { ...form }
if (dialogType.value === 'edit') {
const currentUser = getCurrentUser()
submitData.operator = currentUser.nickName || currentUser.userName || currentUser.name || '未知用户'
updateDeptAppthours(submitData)
.then(response => {
if (response.code === 200) {
ElMessage.success('修改成功')
dialogVisible.value = false
getList()
} else {
ElMessage.error(response.msg || '修改失败')
}
})
.catch(error => {
console.error('修改失败:', error)
ElMessage.error('修改失败')
})
} else {
const currentUser = getCurrentUser()
submitData.operator = currentUser.nickName || currentUser.userName || currentUser.name || '未知用户'
addDeptAppthours(submitData)
.then(response => {
if (response.code === 200) {
ElMessage.success('新增成功')
dialogVisible.value = false
getList()
} else {
ElMessage.error(response.msg || '新增失败')
}
})
.catch(error => {
console.error('新增失败:', error)
ElMessage.error('新增失败')
})
}
}
})
}
function cancel() {
dialogVisible.value = false
resetForm()
}
function resetForm() {
form.id = null
const currentUser = getCurrentUser()
form.institution = currentUser.tenantName || null
form.department = null
form.morningStart = null
form.morningEnd = null
form.afternoonStart = null
form.afternoonEnd = null
form.quota = 0
form.operator = null
proxy.resetForm('formRef')
}
function formatTime(time) {
if (!time) return '-'
if (time.length === 5) {
return time + ':00'
}
return time
}
function tableRowClassName({row, rowIndex}) {
if (rowIndex % 2 === 1) {
return 'warning-row'
}
return ''
}
function getCurrentUser() {
return {
nickName: userStore.nickName,
userName: userStore.name,
name: userStore.nickName,
tenantName: userStore.tenantName,
orgName: userStore.orgName,
tenantId: userStore.tenantId,
orgId: userStore.orgId
}
}
function getDepartmentOptions() {
getDepartmentList()
.then(response => {
if (response.code === 200) {
if (Array.isArray(response.data)) {
departmentOptions.value = response.data
} else if (response.data?.data && Array.isArray(response.data.data)) {
departmentOptions.value = response.data.data
} else if (response.data?.records && Array.isArray(response.data.records)) {
departmentOptions.value = response.data.records
} else {
departmentOptions.value = []
}
} else {
console.warn('获取科室列表失败:', response.msg)
}
})
.catch(error => {
console.error('获取科室列表失败:', error)
})
}
function getTenantOptions() {
getTenantList()
.then(response => {
if (response.code === 200) {
if (response.data?.records && Array.isArray(response.data.records)) {
tenantOptions.value = response.data.records
} else if (Array.isArray(response.data)) {
tenantOptions.value = response.data
} else {
tenantOptions.value = []
}
} else {
console.warn('获取租户列表失败:', response.msg)
}
})
.catch(error => {
console.error('获取租户列表失败:', error)
})
}
onMounted(() => {
resetQuery()
getDepartmentOptions()
getTenantOptions()
updateTableHeight()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
function handleResize() {
updateTableHeight()
checkMobile()
}
function updateTableHeight() {
const container = document.querySelector('.dept-appthours-container')
if (container) {
const containerHeight = container.clientHeight
const headerHeight = 60
const filterHeight = 48
const paginationHeight = 48
const padding = 32
const availableHeight = containerHeight - headerHeight - filterHeight - paginationHeight - padding
tableHeight.value = Math.max(200, Math.min(availableHeight, 600))
tableMaxHeight.value = tableHeight.value
}
}
function checkMobile() {
isMobile.value = window.innerWidth < 768
}
</script>
<style scoped lang="scss">
$primary-color: #1890FF;
$border-color: #D9D9D9;
$text-primary: #606266;
$bg-color: #f5f7fa;
$module-spacing: 24px;
$form-element-spacing: 16px;
.dept-appthours-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: $bg-color;
}
.page-header {
height: 60px;
display: flex;
align-items: center;
padding: 0 20px;
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
.page-title {
font-size: 20px;
font-weight: 500;
color: #333;
margin: 0;
}
}
.filter-section {
height: auto;
min-height: 48px;
display: flex;
align-items: center;
padding: 12px 20px;
background-color: #fff;
border-bottom: 1px solid #e8e8e8;
.filter-row {
display: flex;
align-items: center;
width: 100%;
gap: $form-element-spacing;
flex-wrap: wrap;
}
:deep(.el-form-item) {
margin-bottom: 0;
margin-right: 0;
}
:deep(.el-form-item__label) {
font-size: 14px;
color: $text-primary;
padding-right: 8px;
line-height: 1.5;
}
:deep(.el-select) {
.el-input__wrapper {
width: 150px;
height: 32px;
border: 1px solid $border-color;
border-radius: 4px;
box-shadow: none;
&:hover {
border-color: $primary-color;
}
}
}
.filter-buttons {
display: flex;
gap: $form-element-spacing;
margin-left: auto;
.el-button {
height: 32px;
padding: 0 16px;
font-size: 14px;
border-radius: 2px;
line-height: 1.5;
}
.el-button--primary {
background-color: $primary-color;
border-color: $primary-color;
&:hover {
background-color: lighten($primary-color, 10%);
border-color: lighten($primary-color, 10%);
}
}
.el-button:not(.el-button--primary) {
background-color: #fff;
border-color: $border-color;
color: $text-primary;
&:hover {
color: $primary-color;
border-color: $primary-color;
}
}
}
@media screen and (max-width: 768px) {
padding: 12px 16px;
.filter-row {
flex-direction: column;
align-items: flex-start;
}
:deep(.el-form-item) {
margin-bottom: 12px;
width: 100%;
}
:deep(.el-select) {
.el-input__wrapper {
width: 100%;
}
}
.filter-buttons {
margin-left: 0;
width: 100%;
justify-content: flex-start;
.el-button {
flex: 1;
min-width: 80px;
}
}
}
}
.table-section {
flex: 1;
overflow: auto;
padding: $module-spacing 20px;
.dept-appthours-table {
width: 100%;
background-color: #fff;
border-radius: 4px;
}
}
.pagination-section {
height: auto;
min-height: 48px;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 12px 20px;
background-color: #fff;
border-top: 1px solid #e8e8e8;
gap: $form-element-spacing;
.pagination-total {
font-size: 14px;
color: $text-primary;
line-height: 1.5;
}
:deep(.el-pagination) {
font-weight: normal;
}
:deep(.el-pagination.is-background .el-pager li) {
background-color: #fff;
border: 1px solid $border-color;
border-radius: 2px;
margin: 0 4px;
min-width: 28px;
height: 28px;
line-height: 26px;
font-size: 14px;
color: $text-primary;
&:hover {
color: $primary-color;
border-color: $primary-color;
}
&.is-active {
background-color: $primary-color;
border-color: $primary-color;
color: #fff;
&:hover {
background-color: darken($primary-color, 5%);
border-color: darken($primary-color, 5%);
}
}
}
:deep(.el-pagination.is-background .btn-prev),
:deep(.el-pagination.is-background .btn-next) {
background-color: #fff;
border: 1px solid $border-color;
border-radius: 2px;
min-width: 28px;
height: 28px;
color: $text-primary;
line-height: 26px;
&:hover {
color: $primary-color;
border-color: $primary-color;
}
&:disabled {
color: #c0c4cc;
border-color: #e8e8e8;
}
}
@media screen and (max-width: 768px) {
padding: 12px 16px;
flex-wrap: wrap;
justify-content: center;
.pagination-total {
width: 100%;
text-align: center;
margin-bottom: 8px;
font-size: 13px;
}
:deep(.el-pagination) {
.el-pager li {
min-width: 24px;
height: 24px;
line-height: 22px;
font-size: 12px;
margin: 0 2px;
}
.btn-prev,
.btn-next {
min-width: 24px;
height: 24px;
line-height: 22px;
}
}
}
}
:deep(.el-table) {
.el-table__header-wrapper th {
background-color: #fafafa;
font-weight: 500;
font-size: 14px;
}
.el-table__body tr {
&:hover > td {
background-color: #f5f7fa;
}
&.warning-row {
background-color: #fafafa;
&:hover > td {
background-color: #f0f0f0;
}
}
}
td {
font-size: 14px;
line-height: 1.5;
}
}
:deep(.el-button--link) {
padding: 0;
margin: 0 8px;
font-size: 14px;
height: auto;
line-height: 1.5;
&.el-button--primary {
color: $primary-color;
&:hover {
color: lighten($primary-color, 10%);
}
}
&.el-button--danger {
color: #f56c6c;
&:hover {
color: lighten(#f56c6c, 10%);
}
}
&.el-button--info {
color: #909399;
&:hover {
color: lighten(#909399, 10%);
}
}
}
:deep(.el-dialog) {
border-radius: 4px;
.el-dialog__header {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
}
.el-dialog__body {
padding: 20px;
}
.el-dialog__footer {
padding: 16px 20px;
border-top: 1px solid #e8e8e8;
}
}
:deep(.el-form-item) {
margin-bottom: $form-element-spacing;
&:last-child {
margin-bottom: 0;
}
}
:deep(.el-input__wrapper),
:deep(.el-select .el-input__wrapper),
:deep(.el-textarea__inner) {
border-radius: 4px;
border: 1px solid $border-color;
box-shadow: none;
&:hover {
border-color: $primary-color;
}
&.is-focus {
border-color: $primary-color;
box-shadow: 0 0 0 1px $primary-color;
}
}
:deep(.el-time-picker) {
.el-input__wrapper {
border-radius: 4px;
}
}
:deep(.el-input-number) {
.el-input__wrapper {
border-radius: 4px;
}
}
@media (max-width: 768px) {
.page-header {
padding: 0 12px;
height: 60px;
.page-title {
font-size: 16px;
}
}
.filter-section {
padding: 12px;
height: auto;
.filter-row {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
:deep(.el-select) {
.el-input__wrapper {
width: 100%;
}
}
.filter-buttons {
margin-left: 0;
margin-top: 12px;
flex-direction: column;
gap: 8px;
.el-button {
width: 100%;
margin-bottom: 0;
}
}
}
.table-section {
padding: 12px;
overflow-x: auto;
overflow-y: hidden;
.dept-appthours-table {
min-width: 800px;
}
}
.pagination-section {
justify-content: center;
padding: 12px;
flex-wrap: wrap;
gap: 8px;
}
}
</style>