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