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

815 lines
24 KiB
Vue
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="package-management">
<!-- 顶部筛选栏 -->
<div class="filter-section">
<el-form :model="queryParams" :inline="true" label-width="80px">
<el-form-item label="日期">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="卫生机构">
<el-select v-model="queryParams.organization" placeholder="请选择机构" style="width: 150px" clearable filterable>
<el-option
v-for="org in organizationOptions"
:key="org.value"
:label="org.label"
:value="org.value"
/>
</el-select>
</el-form-item>
<el-form-item label="套餐名称">
<el-input
v-model="queryParams.packageName"
placeholder="请输入套餐名称"
style="width: 200px"
clearable
/>
</el-form-item>
<el-form-item label="套餐级别">
<el-select v-model="queryParams.packageLevel" placeholder="请选择套餐级别" style="width: 150px" clearable>
<el-option
v-for="item in packageLevelOptions"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</el-form-item>
<el-form-item label="套餐类别">
<el-select v-model="queryParams.packageType" placeholder="请选择套餐类别" style="width: 150px" clearable>
<el-option label="检查套餐" value="检查套餐" />
</el-select>
</el-form-item>
<el-form-item label="科室">
<el-select v-model="queryParams.department" placeholder="请选择科室" style="width: 150px" clearable filterable>
<el-option
v-for="dept in departments"
:key="dept.dictValue"
:label="dept.dictLabel"
:value="dept.deptCode || dept.busNoPrefix || dept.rawOrg?.busNo || dept.dictLabel"
/>
</el-select>
</el-form-item>
<el-form-item label="用户">
<el-input
v-model="queryParams.user"
placeholder="请输入用户名称"
style="width: 150px"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery" icon="Search">查询</el-button>
<el-button @click="handleReset" icon="Refresh">重置</el-button>
<el-button type="success" @click="handleAdd" icon="Plus">新增</el-button>
</el-form-item>
</el-form>
</div>
<!-- 表格展示区 -->
<div class="table-section">
<div class="table-wrapper">
<el-table
:data="tableData"
border
style="width: 100%"
v-loading="loading"
:max-height="600"
>
<el-table-column prop="id" label="ID" width="80" align="center" />
<el-table-column prop="organization" label="卫生机构" width="120" align="center" />
<el-table-column prop="maintainDate" label="日期" width="120" align="center" />
<el-table-column prop="packageName" label="套餐名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="packageType" label="套餐类别" width="100" align="center" />
<el-table-column prop="packageLevel" label="套餐级别" width="100" align="center">
<template #default="{ row }">
{{ getLevelLabel(row.packageLevel) }}
</template>
</el-table-column>
<el-table-column prop="department" label="科室" width="150" align="center">
<template #default="{ row }">
<span :title="row.department && /^[A-Z]\d{2}$/.test(row.department.trim()) ? '旧编码格式,建议编辑套餐重新选择科室' : ''">
{{ getDeptName(row.department) }}
</span>
</template>
</el-table-column>
<el-table-column prop="user" label="用户" width="100" align="center" />
<el-table-column prop="packagePrice" label="金额" width="100" align="center">
<template #default="{ row }">
{{ (row.packagePrice || 0).toFixed(2) }}
</template>
</el-table-column>
<el-table-column prop="serviceFee" label="服务费" width="100" align="center">
<template #default="{ row }">
{{ (row.serviceFee || 0).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="总金额" width="100" align="center">
<template #default="{ row }">
{{ ((row.packagePrice || 0) + (row.serviceFee || 0)).toFixed(2) }}
</template>
</el-table-column>
<el-table-column label="组合套餐" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.packagePriceEnabled === 1 ? 'success' : 'danger'">
{{ row.packagePriceEnabled === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="显示套餐名" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.showPackageName === 1 ? 'success' : 'info'">
{{ row.showPackageName === 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="启用标志" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.isDisabled === 0 ? 'success' : 'danger'">
{{ row.isDisabled === 0 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="creator" label="操作人" width="100" align="center" />
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="{ row }">
<div class="actions">
<el-button
class="btn btn-edit"
size="small"
circle
@click="handleEdit(row)"
title="编辑"
>
</el-button>
<el-button
class="btn btn-view"
size="small"
circle
@click="handleView(row)"
title="查看"
>
👁
</el-button>
<el-button
class="btn btn-delete"
size="small"
circle
@click="handleDelete(row)"
title="删除"
>
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 底部分页 -->
<div class="pagination-section">
<el-pagination
v-model:current-page="queryParams.pageNo"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleQuery"
@current-change="handleQuery"
/>
</div>
</div>
</template>
<script setup>
import {onMounted, reactive, ref} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {getDicts} from '@/api/system/dict/data'
import {listDept} from '@/api/system/dept'
import {delCheckPackage, getCheckPackage, listCheckPackage} from '@/api/system/checkType'
import {getTenantPage} from '@/api/system/tenant'
import request from '@/utils/request'
import useUserStore from '@/store/modules/user'
// 定义emit事件
const emit = defineEmits(['switch-to-settings'])
const userStore = useUserStore()
//删除
function handleDelete(row) {
const currentUser = userStore.name
console.log('当前用户:', currentUser, '套餐创建者:', row.creator)
//只有创建者本人才能删除creator为空时不能删除
if(!row.creator){
ElMessage.warning('该套餐创建者未知,无法删除')
return
}
if(row.creator !== currentUser){
ElMessage.warning(`该套餐由"${row.creator}"创建,您没有权限删除`)
return
}
ElMessageBox.confirm(
`确认删除套餐ID:${row.id} - ${row.packageName} 吗?删除后将无法恢复`,
'确认删除',
{
confirmButtonText:'确定删除',
cancelButtonText:'取消',
type: 'warning',
buttonSize:'default'
}
).then(async () => {
try{const response = await delCheckPackage(row.id)
if(response && response.code === 200 || response.code === 0){
ElMessage.success('删除成功')
handleQuery()
}else{
ElMessage.error(response?.msg || response?.message || '删除失败')
}
}catch(error){
console.error('删除失败:',error)
const errorMsg = error?.response?.data?.msg || error?.message || ''
if(errorMsg.includes('foreign key') || errorMsg.includes('violates foreign key')){
ElMessage.warning('该套餐已被使用,无法删除')
}else{
ElMessage.error('删除失败:'+(error.message || '未知错误'))
}
}
}).catch(() => {})
}
// 查询参数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
organization: '',
packageName: '',
packageLevel: '',
packageType: '',
department: '',
user: ''
})
// 日期范围
const dateRange = ref([])
// 表格数据
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
// 套餐级别选项
const packageLevelOptions = ref([])
// 科室选项
const departments = ref([])
// 卫生机构选项
const organizationOptions = ref([])
// 初始化数据
onMounted(async () => {
// 获取套餐级别字典
try {
const levelResponse = await getDicts('examination_item_package_level')
if (levelResponse && levelResponse.data) {
packageLevelOptions.value = levelResponse.data
}
} catch (error) {
console.error('获取套餐级别字典失败:', error)
}
// 获取科室列表 - 使用Organization完整API包含编码和名称
try {
// 使用Organization完整API获取科室列表包含busNo编码
const orgResponse = await request({
url: '/base-data-manage/organization/organization',
method: 'get',
params: {
pageNo: 1,
pageSize: 1000 // 获取足够多的数据
}
})
let orgList = []
if (orgResponse) {
if (orgResponse.data) {
if (orgResponse.data.records && Array.isArray(orgResponse.data.records)) {
orgList = orgResponse.data.records
} else if (Array.isArray(orgResponse.data)) {
orgList = orgResponse.data
}
} else if (Array.isArray(orgResponse)) {
orgList = orgResponse
}
}
// 展开树结构过滤出科室类型typeEnum=2
if (orgList && orgList.length > 0) {
const flattenList = []
function flatten(nodes) {
nodes.forEach(node => {
if (node.typeEnum === 2 && node.busNo && node.name) {
flattenList.push(node)
}
if (node.children && node.children.length > 0) {
flatten(node.children)
}
})
}
flatten(orgList)
orgList = flattenList
}
if (orgList && orgList.length > 0) {
departments.value = orgList.map(org => {
const busNo = (org.busNo || org.code || '').trim()
const name = (org.name || org.deptName || '').trim()
const busNoPrefix = busNo ? busNo.split('.')[0] : ''
return {
dictValue: name,
dictLabel: name,
deptId: org.id || org.deptId,
deptCode: busNo || name,
busNoPrefix: busNoPrefix,
rawOrg: org
}
})
} else {
// 如果Organization API没有数据使用系统部门API
const deptResponse = await listDept()
let deptList = []
if (deptResponse) {
if (Array.isArray(deptResponse)) {
deptList = deptResponse
} else if (deptResponse.data) {
if (Array.isArray(deptResponse.data)) {
deptList = deptResponse.data
} else if (deptResponse.data.data && Array.isArray(deptResponse.data.data)) {
deptList = deptResponse.data.data
}
} else if (deptResponse.code === 200 && deptResponse.data) {
deptList = Array.isArray(deptResponse.data) ? deptResponse.data : []
}
}
if (deptList && deptList.length > 0) {
departments.value = deptList.map(dept => ({
dictValue: dept.deptName || dept.name,
dictLabel: dept.deptName || dept.name,
deptId: dept.deptId || dept.id,
deptCode: dept.deptName || dept.name
}))
} else {
// 如果获取失败,尝试使用字典方式
try {
const dictResponse = await getDicts('dept')
if (dictResponse && dictResponse.data) {
departments.value = dictResponse.data
}
} catch (dictError) {
console.error('获取科室字典失败:', dictError)
}
}
}
} catch (error) {
console.error('获取科室列表失败:', error)
// 如果获取失败,尝试使用字典方式
try {
const dictResponse = await getDicts('dept')
if (dictResponse && dictResponse.data) {
departments.value = dictResponse.data
}
} catch (dictError) {
console.error('获取科室字典也失败:', dictError)
}
}
// 获取卫生机构列表
try {
const tenantResponse = await getTenantPage({ pageNum: 1, pageSize: 100 })
if (tenantResponse && tenantResponse.code === 200) {
const tenantData = tenantResponse.data || {}
let tenantList = []
if (Array.isArray(tenantData)) {
tenantList = tenantData
} else if (tenantData.records) {
tenantList = tenantData.records
} else if (tenantData.rows) {
tenantList = tenantData.rows
} else if (tenantData.list) {
tenantList = tenantData.list
}
// 过滤启用的机构
organizationOptions.value = tenantList
.filter(item => item && item.status === '0')
.map(item => ({
value: item.tenantName || item.name || item.orgName || String(item.id),
label: item.tenantName || item.name || item.orgName || String(item.id)
}))
}
} catch (error) {
console.error('获取卫生机构列表失败:', error)
}
// 加载列表数据
handleQuery()
})
// 获取级别标签
function getLevelLabel(value) {
const item = packageLevelOptions.value.find(i => i.dictValue === value)
return item ? item.dictLabel : value
}
// 获取科室名称(根据编码或名称查找)
function getDeptName(deptValue) {
if (!deptValue) return ''
// 去除前后空格
const trimmedValue = String(deptValue).trim()
if (!trimmedValue) return ''
// 如果科室列表为空,直接返回原值
if (!departments.value || departments.value.length === 0) {
return trimmedValue
}
// 先尝试精确匹配编码(去除所有空格,转大写)
let dept = departments.value.find(d => {
const code = String(d.deptCode || '').trim().replace(/\s+/g, '').toUpperCase()
const searchValue = trimmedValue.replace(/\s+/g, '').toUpperCase()
return code && code === searchValue
})
// 如果找不到尝试通过busNo前缀匹配优先使用存储的前缀
if (!dept) {
dept = departments.value.find(d => {
const busNoPrefix = String(d.busNoPrefix || '').trim().replace(/\s+/g, '').toUpperCase()
const searchValue = trimmedValue.replace(/\s+/g, '').toUpperCase()
return busNoPrefix && busNoPrefix === searchValue
})
}
// 如果找不到尝试通过原始busNo精确匹配
if (!dept) {
dept = departments.value.find(d => {
const rawBusNo = String(d.rawOrg?.busNo || '').trim().replace(/\s+/g, '').toUpperCase()
const searchValue = trimmedValue.replace(/\s+/g, '').toUpperCase()
return rawBusNo && rawBusNo === searchValue
})
}
// 如果找不到尝试层级编码匹配busNo可能是 "A01.001",搜索值是 "A01"
if (!dept) {
dept = departments.value.find(d => {
const rawBusNo = String(d.rawOrg?.busNo || '').trim().replace(/\s+/g, '').toUpperCase()
const searchValue = trimmedValue.replace(/\s+/g, '').toUpperCase()
if (!rawBusNo) return false
// 如果busNo包含点号取点号前的部分进行匹配
const busNoPrefix = rawBusNo.split('.')[0]
// 匹配方式:前缀匹配、完全匹配、或者搜索值作为前缀
return busNoPrefix === searchValue ||
rawBusNo.startsWith(searchValue + '.') ||
rawBusNo === searchValue
})
}
// 如果找不到,尝试匹配名称
if (!dept) {
dept = departments.value.find(d => {
const label = String(d.dictLabel || '').trim()
const value = String(d.dictValue || '').trim()
return label === trimmedValue || value === trimmedValue
})
}
// 如果还是找不到,尝试部分匹配(编码可能不完整,或者编码格式不同)
if (!dept) {
dept = departments.value.find(d => {
const code = String(d.deptCode || '').trim().replace(/\s+/g, '').toUpperCase()
const rawBusNo = String(d.rawOrg?.busNo || '').trim().replace(/\s+/g, '').toUpperCase()
const searchValue = trimmedValue.replace(/\s+/g, '').toUpperCase()
if (!code && !rawBusNo) return false
// 尝试多种匹配方式
return (code && code.includes(searchValue)) ||
(code && searchValue.includes(code)) ||
(rawBusNo && rawBusNo.includes(searchValue)) ||
(rawBusNo && searchValue.includes(rawBusNo)) ||
(rawBusNo && rawBusNo.startsWith(searchValue)) ||
(code && code.length >= 3 && searchValue.length >= 3 && code.substring(0, 3) === searchValue.substring(0, 3))
})
}
if (dept && dept.dictLabel) {
return dept.dictLabel
}
// 无法匹配时,返回原始编码值
const isOldFormat = /^[A-Z]\d{2}$/.test(trimmedValue)
if (isOldFormat) {
return trimmedValue + ' (旧格式)'
}
return trimmedValue
}
// 查询
async function handleQuery() {
try {
loading.value = true
// 构建查询参数
const params = {
...queryParams
}
// 处理日期范围
if (dateRange.value && dateRange.value.length === 2) {
params.startDate = dateRange.value[0]
params.endDate = dateRange.value[1]
}
const response = await listCheckPackage(params)
if (response && response.data) {
// 处理不同的响应格式
if (Array.isArray(response.data)) {
tableData.value = response.data
total.value = response.data.length
} else if (response.data.records) {
tableData.value = response.data.records
total.value = response.data.total || 0
} else {
tableData.value = []
total.value = 0
}
}
} catch (error) {
console.error('查询失败:', error)
ElMessage.error('查询失败: ' + (error.message || '未知错误'))
} finally {
loading.value = false
}
}
// 重置
function handleReset() {
queryParams.pageNo = 1
queryParams.pageSize = 10
queryParams.organization = ''
queryParams.packageName = ''
queryParams.packageLevel = ''
queryParams.packageType = ''
queryParams.department = ''
queryParams.user = ''
dateRange.value = []
handleQuery()
}
// 新增
function handleAdd() {
emit('switch-to-settings', { mode: 'add', data: null })
}
// 编辑
async function handleEdit(row) {
try {
const response = await getCheckPackage(row.id)
if (response && (response.code === 200 || response.code === 0) && response.data) {
emit('switch-to-settings', { mode: 'edit', data: response.data })
} else {
ElMessage.error('加载套餐数据失败')
}
} catch (error) {
console.error('加载套餐数据失败:', error)
ElMessage.error('数据加载失败,请重试')
}
}
// 查看
async function handleView(row) {
try {
const response = await getCheckPackage(row.id)
if (response && (response.code === 200 || response.code === 0) && response.data) {
emit('switch-to-settings', { mode: 'view', data: response.data })
} else {
ElMessage.error('加载套餐数据失败')
}
} catch (error) {
console.error('加载套餐数据失败:', error)
ElMessage.error('数据加载失败,请重试')
}
}
// 查询参数
</script>
<style scoped>
.package-management {
padding: 24px;
background-color: #f5f7fa;
min-height: 100vh;
width: 100%;
height: 100%;
overflow: auto;
box-sizing: border-box;
}
.filter-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
width: 100%;
box-sizing: border-box;
}
.filter-section :deep(.el-form-item) {
margin-bottom: 16px;
}
.table-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.table-wrapper {
width: 100%;
overflow-x: auto;
overflow-y: auto;
max-height: calc(100vh - 400px);
min-height: 400px;
}
.pagination-section {
display: flex;
justify-content: flex-end;
padding: 16px 20px;
}
/* 统一的操作按钮样式 */
.actions {
display: flex;
justify-content: center;
gap: 6px;
position: relative;
z-index: 10;
}
.btn {
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 14px;
font-weight: bold;
position: relative;
z-index: 10;
color: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
}
.btn:hover {
transform: translateY(-2px) scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
}
.btn:active {
transform: translateY(0) scale(0.95);
}
.btn-confirm {
background: linear-gradient(135deg, #52C41A 0%, #73d13d 100%);
}
.btn-confirm:hover {
background: linear-gradient(135deg, #389E0D 0%, #52C41A 100%);
}
.btn-edit {
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
}
.btn-edit:hover {
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
}
.btn-add {
background: linear-gradient(135deg, #1890FF 0%, #40a9ff 100%);
}
.btn-add:hover {
background: linear-gradient(135deg, #096DD9 0%, #1890FF 100%);
}
.btn-view {
background: linear-gradient(135deg, #722ED1 0%, #9254DE 100%);
}
.btn-view:hover {
background: linear-gradient(135deg, #531DAE 0%, #722ED1 100%);
}
.btn-delete {
background: linear-gradient(135deg, #FF4D4F 0%, #ff7875 100%);
z-index: 20;
pointer-events: auto;
}
.btn-delete:hover {
background: linear-gradient(135deg, #CF1322 0%, #FF4D4F 100%);
}
/* 优化滚动条样式 - 支持水平和垂直滚动 */
.table-wrapper {
scrollbar-width: thin;
scrollbar-color: #c1c1c1 #f1f1f1;
}
.table-wrapper::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.table-wrapper::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.table-wrapper::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.table-wrapper::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 确保表格可以水平滚动 */
.table-wrapper :deep(.el-table) {
min-width: 100%;
}
.table-wrapper :deep(.el-table__body-wrapper) {
overflow-x: auto;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.filter-section :deep(.el-form) {
display: flex;
flex-wrap: wrap;
}
}
@media (max-width: 768px) {
.package-management {
padding: 16px;
}
.filter-section,
.table-section,
.pagination-section {
padding: 16px;
}
.table-wrapper {
max-height: calc(100vh - 350px);
}
}
</style>