feat(检验申请): 优化检验申请界面布局并添加套餐金额字段
重构检验申请界面,将操作按钮移至表格标题栏以节省垂直空间 在诊断治疗DTO和SQL映射文件中添加套餐金额和服务费字段
This commit is contained in:
@@ -147,6 +147,12 @@ public class DiagnosisTreatmentDto {
|
|||||||
/** 费用套餐名称(JOIN inspection_basic_information.package_name) */
|
/** 费用套餐名称(JOIN inspection_basic_information.package_name) */
|
||||||
private String packageName;
|
private String packageName;
|
||||||
|
|
||||||
|
/** 套餐金额(JOIN inspection_basic_information.package_amount) */
|
||||||
|
private BigDecimal packageAmount;
|
||||||
|
|
||||||
|
/** 套餐服务费(JOIN inspection_basic_information.service_fee) */
|
||||||
|
private BigDecimal serviceFee;
|
||||||
|
|
||||||
/** 下级医技类型ID(关联 inspection_type 子类) */
|
/** 下级医技类型ID(关联 inspection_type 子类) */
|
||||||
@JsonSerialize(using = ToStringSerializer.class)
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
private Long subItemId;
|
private Long subItemId;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
T1.sub_item_id,
|
T1.sub_item_id,
|
||||||
T3.name AS test_type,
|
T3.name AS test_type,
|
||||||
T5.package_name,
|
T5.package_name,
|
||||||
|
T5.package_amount,
|
||||||
|
T5.service_fee,
|
||||||
T6.name AS sub_item_name
|
T6.name AS sub_item_name
|
||||||
FROM lab_activity_definition T1
|
FROM lab_activity_definition T1
|
||||||
/* 检验类型关联(逻辑关联,无外键) */
|
/* 检验类型关联(逻辑关联,无外键) */
|
||||||
@@ -97,6 +99,8 @@
|
|||||||
T1.sub_item_id,
|
T1.sub_item_id,
|
||||||
T3.name AS test_type,
|
T3.name AS test_type,
|
||||||
T5.package_name,
|
T5.package_name,
|
||||||
|
T5.package_amount,
|
||||||
|
T5.service_fee,
|
||||||
T6.name AS sub_item_name
|
T6.name AS sub_item_name
|
||||||
FROM lab_activity_definition T1
|
FROM lab_activity_definition T1
|
||||||
LEFT JOIN inspection_type T3
|
LEFT JOIN inspection_type T3
|
||||||
|
|||||||
@@ -1,28 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="inspection-application-container">
|
<el-container class="inspection-application-container">
|
||||||
|
|
||||||
<!-- 顶部操作按钮区 - Bug#334: 优化垂直空间利用率 -->
|
<!-- 顶部操作按钮区 - 隐藏,按钮移到卡片标题行 -->
|
||||||
<el-header class="top-action-bar" height="48px">
|
<el-header class="top-action-bar" height="0" style="overflow: hidden">
|
||||||
<el-row class="action-buttons" type="flex" justify="end" :gutter="8">
|
|
||||||
<el-button type="primary" size="default" @click="handleSave" class="save-btn" :loading="saving">
|
|
||||||
<el-icon><Document /></el-icon>
|
|
||||||
保存
|
|
||||||
</el-button>
|
|
||||||
<el-button type="primary" size="default" @click="handleNewApplication" class="new-btn">
|
|
||||||
<el-icon><Plus /></el-icon>
|
|
||||||
新增
|
|
||||||
</el-button>
|
|
||||||
</el-row>
|
|
||||||
</el-header>
|
</el-header>
|
||||||
|
|
||||||
<!-- 检验信息表格区 -->
|
<!-- 检验信息表格区 -->
|
||||||
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
|
<el-main class="inspection-section" style="width: 100%; max-width: 100%">
|
||||||
<el-card class="table-card" style="width: 100%">
|
<el-card class="table-card" style="width: 100%">
|
||||||
<template #header>
|
<template #header>
|
||||||
<el-row class="card-header" type="flex" align="middle">
|
<div class="table-card-header-bar">
|
||||||
<el-icon><DocumentChecked /></el-icon>
|
<span class="table-card-title"><el-icon><DocumentChecked /></el-icon> 检验信息</span>
|
||||||
<span>检验信息</span>
|
<span class="table-card-btns">
|
||||||
</el-row>
|
<el-button type="primary" size="default" @click="handleSave" :loading="saving">
|
||||||
|
<el-icon><Document /></el-icon> 保存
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" size="default" @click="handleNewApplication">
|
||||||
|
<el-icon><Plus /></el-icon> 新增
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-table
|
<el-table
|
||||||
ref="inspectionTableRef"
|
ref="inspectionTableRef"
|
||||||
@@ -512,28 +509,58 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 已选项目列表 -->
|
<!-- 已选项目列表 - 支持树形展开 -->
|
||||||
<el-scrollbar class="selected-tree" style="max-height: 220px">
|
<el-scrollbar class="selected-tree" style="max-height: 220px">
|
||||||
<el-list v-if="selectedInspectionItems.length > 0" :data="selectedInspectionItems" class="selected-items-list">
|
<div v-if="selectedInspectionItems.length > 0" class="selected-items-list">
|
||||||
<el-list-item
|
<div
|
||||||
v-for="item in selectedInspectionItems"
|
v-for="item in selectedInspectionItems"
|
||||||
:key="item.itemId"
|
:key="item.itemId"
|
||||||
class="selected-list-item"
|
class="selected-item-wrapper"
|
||||||
>
|
>
|
||||||
<el-row class="selected-item-content" type="flex" align="middle" style="width: 100%">
|
<!-- 主项目行 -->
|
||||||
|
<div
|
||||||
|
:class="['selected-item-content', { 'is-package': item.feePackageId }]"
|
||||||
|
@click="item.feePackageId && togglePackageExpand(item)"
|
||||||
|
>
|
||||||
|
<!-- 套餐展开图标 -->
|
||||||
|
<el-icon
|
||||||
|
v-if="item.feePackageId"
|
||||||
|
:class="['expand-icon', { 'is-expanded': item.expanded }]"
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
</el-icon>
|
||||||
<span class="item-itemName">{{ item.itemName }}</span>
|
<span class="item-itemName">{{ item.itemName }}</span>
|
||||||
<span class="item-price">¥{{ item.itemPrice }}</span>
|
<span class="item-price">¥{{ item.itemPrice }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
size="small"
|
size="small"
|
||||||
style="color: #f56c6c; margin-left: auto"
|
style="color: #f56c6c; margin-left: auto"
|
||||||
@click="removeInspectionItem(item)"
|
@click.stop="removeInspectionItem(item)"
|
||||||
>
|
>
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-row>
|
</div>
|
||||||
</el-list-item>
|
|
||||||
</el-list>
|
<!-- 套餐明细项(树形展开) -->
|
||||||
|
<div v-if="item.feePackageId && item.expanded" class="package-details">
|
||||||
|
<div v-if="item.loadingDetails" class="loading-details">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.details && item.details.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="detail in item.details"
|
||||||
|
:key="detail.detailId"
|
||||||
|
class="detail-item"
|
||||||
|
>
|
||||||
|
<span class="detail-name">{{ detail.itemName }}</span>
|
||||||
|
<span class="detail-price">¥{{ detail.unitPrice || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="no-details">暂无明细</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<el-empty v-if="selectedInspectionItems.length === 0" class="no-selection" description="暂无选择项目" />
|
<el-empty v-if="selectedInspectionItems.length === 0" class="no-selection" description="暂无选择项目" />
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -546,15 +573,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
|
import {onMounted, onUnmounted, reactive, ref, watch, computed, getCurrentInstance} from 'vue'
|
||||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||||
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading } from '@element-plus/icons-vue'
|
import { DocumentChecked, Plus, Document, Printer, Delete, Check, Loading, ArrowRight } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
deleteInspectionApplication, getApplyList,
|
deleteInspectionApplication, getApplyList,
|
||||||
saveInspectionApplication,
|
saveInspectionApplication,
|
||||||
getInspectionTypeList,
|
getInspectionTypeList,
|
||||||
getInspectionItemList,
|
|
||||||
getEncounterDiagnosis,
|
getEncounterDiagnosis,
|
||||||
getInspectionApplyDetail
|
getInspectionApplyDetail
|
||||||
} from '../api'
|
} from '../api'
|
||||||
|
import { getLabActivityDefinitionPage } from '@/api/lab/labActivityDefinition'
|
||||||
|
import { listInspectionPackageDetails } from '@/api/system/inspectionPackage'
|
||||||
import useUserStore from '@/store/modules/user.js'
|
import useUserStore from '@/store/modules/user.js'
|
||||||
// 迁移到 hiprint
|
// 迁移到 hiprint
|
||||||
import { previewPrint } from '@/utils/printUtils.js'
|
import { previewPrint } from '@/utils/printUtils.js'
|
||||||
@@ -806,7 +834,7 @@ const loadCategoryItems = async (categoryKey, loadMore = false) => {
|
|||||||
params.inspectionTypeId = category.typeId
|
params.inspectionTypeId = category.typeId
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getInspectionItemList(params)
|
const res = await getLabActivityDefinitionPage(params)
|
||||||
|
|
||||||
// 解析数据
|
// 解析数据
|
||||||
let records = []
|
let records = []
|
||||||
@@ -822,22 +850,31 @@ const loadCategoryItems = async (categoryKey, loadMore = false) => {
|
|||||||
total = records.length
|
total = records.length
|
||||||
}
|
}
|
||||||
|
|
||||||
// 映射数据格式
|
// 映射数据格式,计算套餐价格
|
||||||
const mappedItems = records.map(item => ({
|
const mappedItems = records.map(item => {
|
||||||
itemId: item.id || item.activityId || Math.random().toString(36).substring(2, 11),
|
// 计算价格:套餐项目使用 packageAmount + serviceFee,否则使用 retailPrice
|
||||||
itemName: item.name || item.itemName || '',
|
let itemPrice = item.retailPrice || 0
|
||||||
itemPrice: item.retailPrice || item.price || 0,
|
if (item.feePackageId && item.packageAmount) {
|
||||||
itemAmount: item.retailPrice || item.price || 0,
|
itemPrice = (Number(item.packageAmount) || 0) + (Number(item.serviceFee) || 0)
|
||||||
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
|
}
|
||||||
unit: item.unit || '',
|
|
||||||
|
return {
|
||||||
|
itemId: item.id || Math.random().toString(36).substring(2, 11),
|
||||||
|
itemName: item.name || '',
|
||||||
|
itemPrice: itemPrice,
|
||||||
|
itemAmount: itemPrice,
|
||||||
|
sampleType: item.specimenCode_dictText || '血液',
|
||||||
|
unit: item.permittedUnitCode || '',
|
||||||
itemQty: 1,
|
itemQty: 1,
|
||||||
serviceFee: 0,
|
serviceFee: item.serviceFee || 0,
|
||||||
type: category.label,
|
type: category.label,
|
||||||
isSelfPay: false,
|
isSelfPay: false,
|
||||||
activityId: item.activityId,
|
code: item.busNo || '',
|
||||||
code: item.busNo || item.code || item.activityCode,
|
inspectionTypeId: item.inspectionTypeId || null,
|
||||||
inspectionTypeId: item.inspectionTypeId || null
|
feePackageId: item.feePackageId || null,
|
||||||
}))
|
packageName: item.packageName || null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 更新分类数据
|
// 更新分类数据
|
||||||
if (loadMore) {
|
if (loadMore) {
|
||||||
@@ -936,21 +973,28 @@ const querySearchInspectionItems = async (queryString, cb) => {
|
|||||||
searchKey: queryString
|
searchKey: queryString
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await getInspectionItemList(params)
|
const res = await getLabActivityDefinitionPage(params)
|
||||||
|
|
||||||
let suggestions = []
|
let suggestions = []
|
||||||
if (res.data && res.data.records) {
|
if (res.data && res.data.records) {
|
||||||
// 映射数据格式,与 loadInspectionItemsByType 保持一致
|
// 映射数据格式,与 loadInspectionItemsByType 保持一致
|
||||||
suggestions = res.data.records.map(item => ({
|
suggestions = res.data.records.map(item => {
|
||||||
itemId: item.id || item.activityId,
|
// 计算价格
|
||||||
itemName: item.name || item.itemName || '',
|
let itemPrice = item.retailPrice || 0
|
||||||
itemPrice: item.retailPrice || item.price || 0,
|
if (item.feePackageId && item.packageAmount) {
|
||||||
sampleType: item.specimenCode_dictText || item.sampleType || '血液',
|
itemPrice = (Number(item.packageAmount) || 0) + (Number(item.serviceFee) || 0)
|
||||||
unit: item.unit || '',
|
}
|
||||||
code: item.busNo || item.code || item.activityCode,
|
return {
|
||||||
activityId: item.activityId,
|
itemId: item.id,
|
||||||
inspectionTypeId: item.inspectionTypeId || null
|
itemName: item.name || '',
|
||||||
}))
|
itemPrice: itemPrice,
|
||||||
|
sampleType: item.specimenCode_dictText || '血液',
|
||||||
|
unit: item.permittedUnitCode || '',
|
||||||
|
code: item.busNo || '',
|
||||||
|
inspectionTypeId: item.inspectionTypeId || null,
|
||||||
|
feePackageId: item.feePackageId || null
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(suggestions)
|
cb(suggestions)
|
||||||
@@ -1384,6 +1428,25 @@ const removeInspectionItem = (item) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换套餐展开状态(Bug #325)
|
||||||
|
const togglePackageExpand = async (item) => {
|
||||||
|
item.expanded = !item.expanded
|
||||||
|
if (item.expanded && !item.details && item.feePackageId) {
|
||||||
|
item.loadingDetails = true
|
||||||
|
try {
|
||||||
|
const res = await listInspectionPackageDetails(item.feePackageId)
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
item.details = res.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取套餐明细失败', error)
|
||||||
|
item.details = []
|
||||||
|
} finally {
|
||||||
|
item.loadingDetails = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 清空所有选择
|
// 清空所有选择
|
||||||
const clearAllSelected = () => {
|
const clearAllSelected = () => {
|
||||||
selectedInspectionItems.value = []
|
selectedInspectionItems.value = []
|
||||||
@@ -1647,15 +1710,33 @@ defineExpose({
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bug#334: 顶部操作按钮区 - 优化垂直空间利用率 */
|
/* Bug#334: 顶部操作按钮区 - 隐藏原区域,按钮移到卡片标题行 */
|
||||||
.top-action-bar {
|
.top-action-bar {
|
||||||
|
height: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片标题行:标题在左,按钮在右 */
|
||||||
|
.table-card-header-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid var(--el-border-color-light);
|
width: 100%;
|
||||||
background: var(--el-bg-color);
|
}
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
|
|
||||||
padding: 0 12px;
|
.table-card-header-bar .table-card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card-header-bar .table-card-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@@ -2245,6 +2326,24 @@ defineExpose({
|
|||||||
margin: 2px 0;
|
margin: 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected-item-content.is-package {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content.is-package:hover {
|
||||||
|
background: #f0f1f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content .expand-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content .expand-icon.is-expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
.selected-item-content .item-itemName {
|
.selected-item-content .item-itemName {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -2256,6 +2355,44 @@ defineExpose({
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 套餐明细样式 */
|
||||||
|
.package-details {
|
||||||
|
margin-left: 24px;
|
||||||
|
padding: 4px 0 4px 12px;
|
||||||
|
border-left: 2px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .detail-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .detail-price {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .loading-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.package-details .no-details {
|
||||||
|
padding: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.no-selection {
|
.no-selection {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
|
|||||||
Reference in New Issue
Block a user