feat(检验申请): 优化检验申请界面布局并添加套餐金额字段

重构检验申请界面,将操作按钮移至表格标题栏以节省垂直空间
在诊断治疗DTO和SQL映射文件中添加套餐金额和服务费字段
This commit is contained in:
wangjian963
2026-04-07 18:30:40 +08:00
parent e9d4f57815
commit ce64c4519c
3 changed files with 208 additions and 61 deletions

View File

@@ -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 || '',
itemQty: 1,
serviceFee: 0,
type: category.label,
isSelfPay: false,
activityId: item.activityId,
code: item.busNo || item.code || item.activityCode,
inspectionTypeId: item.inspectionTypeId || null
}))
// 映射数据格式,计算套餐价格
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: item.serviceFee || 0,
type: category.label,
isSelfPay: false,
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;