Compare commits

...

3 Commits

Author SHA1 Message Date
赵云
a26e3573b9 Fix Bug #426: 门诊医生站-检查开立:已选择列表应支持树形展开,显示套餐明细(项目/数量/单价)
- 已选择面板的套餐项增加"套餐"标签,便于用户识别
- 展开/收起图标改为 ArrowRight 旋转样式,符合标准交互习惯
- toggleItemExpand 函数增加 packageName 兜底判断,不强制依赖 isPackage 标记
- loadPackageDetailsForItem 添加 loading 状态和更健壮的 packageId 解析逻辑
- 新增 expanded-content 和 package-loading-hint CSS 样式

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:30:10 +08:00
关羽
0cb616f843 Fix Bug #446: 【手术管理-门诊手术安排】临时医嘱生成后界面非法关闭且按钮名称/功能显示不一致
根因: handleMedicalAdvice 中盲目重置 temporarySigned.value = false,导致重新打开医嘱弹窗时按钮状态错误。

修复:
1. index.vue: 根据已有医嘱数据是否有 requestId 来决定 temporarySigned 状态,而非盲目重置
2. temporaryMedical.vue: 新增 allItemsSubmitted 计算属性,当所有计费药品已提交(requestId)时显示"已签发"按钮并禁用

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:30:10 +08:00
关羽
e5152e7ea9 Fix Bug #408: 门诊医生站:检查标签页:选中检查申请记录后,"检查明细"标签页显示"暂无数据"
根因:handleRowClick 中 const resp = res.data || res 对 Axios 拦截器已解包的响应
进行二次解包,导致 resp 被赋为 ExamApply 实体对象(不含 items),后续 items 提取
逻辑始终返回空数组,明细列表无法加载。

修复:用 res.code !== undefined 判定 res 是否已是 AjaxResult 体,若是则直接使用,
否则再执行 res.data 解包。items 和数据提取统一从正确层级取值,避免二次解包。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 09:30:10 +08:00
3 changed files with 67 additions and 28 deletions

View File

@@ -413,29 +413,33 @@
:key="idx" :key="idx"
class="selected-item-card" class="selected-item-card"
> >
<!-- Bug #384修复: 项目卡片头部可展开/收起 --> <!-- Bug #384修复 + #426修复: 项目卡片头部可展开/收起 -->
<div class="card-header" @click="toggleItemExpand(item)"> <div class="card-header" @click="toggleItemExpand(item)">
<el-tag v-if="item.isPackage || item.packageName" size="small" type="warning" style="margin-right: 4px; flex-shrink: 0;">套餐</el-tag>
<span class="card-name">{{ item.name }}</span> <span class="card-name">{{ item.name }}</span>
<span class="card-price">¥{{ item.price }}</span> <span class="card-price">¥{{ item.price }}</span>
<!-- 展开图标 --> <!-- 展开/收起图标 -->
<el-icon :class="['expand-icon', { expanded: item.expanded }]"> <el-icon class="expand-icon" :class="{ expanded: item.expanded }">
<ArrowDown v-if="!item.expanded" /> <ArrowRight />
<ArrowUp v-if="item.expanded" />
</el-icon> </el-icon>
<!-- 删除按钮 --> <!-- 删除按钮 -->
<el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)"> <el-button link type="danger" size="small" @click.stop="handleRemoveItem(idx, item)">
<el-icon><Close /></el-icon> <el-icon><Close /></el-icon>
</el-button> </el-button>
</div> </div>
<!-- Bug #428修复: 展开后显示套餐明细或检查方法 --> <!-- Bug #428修复 + #426修复: 展开后显示套餐明细或检查方法 -->
<div v-if="item.expanded"> <div v-show="item.expanded" class="expanded-content">
<!-- 显示套餐明细 --> <!-- 显示套餐明细 -->
<div v-if="item.packageDetails && item.packageDetails.length > 0" class="package-details-list"> <div v-if="(item.isPackage || item.packageName) && item.packageDetails && item.packageDetails.length > 0" class="package-details-list">
<div class="detail-row" v-for="detail in item.packageDetails" :key="detail.id"> <div class="detail-row" v-for="detail in item.packageDetails" :key="detail.id">
<span class="detail-name">{{ detail.name }}</span> <span class="detail-name">{{ detail.name }}</span>
<span class="detail-info">数量: {{ detail.quantity }} 单价: ¥{{ detail.price }}</span> <span class="detail-info">数量: {{ detail.quantity }} 单价: ¥{{ detail.price }}</span>
</div> </div>
</div> </div>
<!-- 套餐明细加载中 -->
<div v-else-if="(item.isPackage || item.packageName) && item.packageDetailsLoading" class="package-loading-hint">
加载中...
</div>
<!-- 显示检查方法 --> <!-- 显示检查方法 -->
<div v-else-if="item.methods && item.methods.length > 0" class="method-list"> <div v-else-if="item.methods && item.methods.length > 0" class="method-list">
<div v-for="method in item.methods" :key="method.id" class="method-option"> <div v-for="method in item.methods" :key="method.id" class="method-option">
@@ -473,7 +477,7 @@
<script setup> <script setup>
import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue'; import { ref, reactive, computed, watch, onMounted, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { Printer, Delete, ArrowDown, ArrowUp, Close } from '@element-plus/icons-vue'; import { Printer, Delete, ArrowDown, ArrowUp, Close, ArrowRight } from '@element-plus/icons-vue';
import useUserStore from '@/store/modules/user'; import useUserStore from '@/store/modules/user';
import request from '@/utils/request'; import request from '@/utils/request';
import { listCheckMethod, searchCheckMethod, listCheckPackage } from '@/api/system/checkType'; import { listCheckMethod, searchCheckMethod, listCheckPackage } from '@/api/system/checkType';
@@ -592,11 +596,13 @@ async function loadPackageDetails(row, treeNode, resolve) {
} }
} }
// #428修复: 为已选择项目加载套餐明细通过packageId或packageName查询 // #428修复 + #426修复: 为已选择项目加载套餐明细通过packageId或packageName查询
async function loadPackageDetailsForItem(item) { async function loadPackageDetailsForItem(item) {
if (!item.isPackage || (!item.packageId && !item.packageName)) { // 只要有 packageName 就认为是套餐,不强制要求 isPackage 或 packageId
if (!item.packageName && !item.packageId) {
return; return;
} }
item.packageDetailsLoading = true;
try { try {
let packageId = item.packageId; let packageId = item.packageId;
if (!packageId && item.packageName) { if (!packageId && item.packageName) {
@@ -612,6 +618,10 @@ async function loadPackageDetailsForItem(item) {
} }
packageId = packages[0].id; packageId = packages[0].id;
} }
if (!packageId) {
item.packageDetails = [];
return;
}
const res = await request({ const res = await request({
url: `/system/package/${packageId}/details`, url: `/system/package/${packageId}/details`,
method: 'get' method: 'get'
@@ -630,6 +640,8 @@ async function loadPackageDetailsForItem(item) {
} catch (err) { } catch (err) {
console.error('加载套餐明细失败:', err); console.error('加载套餐明细失败:', err);
item.packageDetails = []; item.packageDetails = [];
} finally {
item.packageDetailsLoading = false;
} }
} }
const detailTableRef = ref(null); const detailTableRef = ref(null);
@@ -1136,17 +1148,19 @@ function handleRowClick(row) {
selectedItems.value = []; selectedItems.value = [];
activeDetailTab.value = 'applyForm'; activeDetailTab.value = 'applyForm';
request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => { request({ url: `/exam/apply/${row.applyNo}`, method: 'get' }).then(async res => {
const resp = res.data || res; // 响应结构判定Axios拦截器对 code===200 返回 res.dataAjaxResult体
// Bug #408修复: items 在 AjaxResult 顶层(res.items / resp.items),不在 ExamApply 对象内 // 但某些情况下可能返回完整 Axios 响应 {data: AjaxResult}。
// 防御性提取:优先取顶层 items兼容嵌套在 resp.data.items 的情况 // 用 res.code 判定是否已是 AjaxResult 体,避免二次解包导致 items 丢失。
let rawItems = res.items || resp.items; const isAjaxResult = res && typeof res === 'object' && res.code !== undefined;
if (!rawItems && resp.data && typeof resp.data === 'object') { const ajaxBody = isAjaxResult ? res : (res.data || res);
rawItems = resp.data.items;
} // items 在 AjaxResult 顶层data 字段是 ExamApply 实体
rawItems = rawItems || []; const rawItems = Array.isArray(ajaxBody.items) ? ajaxBody.items : [];
const d = resp.data || resp; const detailData = ajaxBody.data || {};
if (d) Object.assign(form, d);
if (Array.isArray(rawItems) && rawItems.length > 0) { if (detailData && typeof detailData === 'object') Object.assign(form, detailData);
if (rawItems.length > 0) {
try { try {
// 为每个项目加载检查方法 // 为每个项目加载检查方法
const itemsWithMethods = await Promise.all(rawItems.map(async m => { const itemsWithMethods = await Promise.all(rawItems.map(async m => {
@@ -1394,11 +1408,11 @@ async function handleItemSelect(checked, item, cat) {
// Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态 // Bug #382 修复:移除自动切换页签逻辑,保持当前页签状态
} }
// Bug #384修复: 展开/收起项目卡片 // Bug #384修复 + #426修复: 展开/收起项目卡片
async function toggleItemExpand(item) { async function toggleItemExpand(item) {
item.expanded = !item.expanded; item.expanded = !item.expanded;
// 如果是展开且该项目是套餐,加载套餐明细 // 如果是展开且该项目是套餐(通过 isPackage 或 packageName 判断),加载套餐明细
if (item.expanded && item.isPackage && (!item.packageDetails || item.packageDetails.length === 0)) { if (item.expanded && (item.isPackage || item.packageName) && (!item.packageDetails || item.packageDetails.length === 0) && !item.packageDetailsLoading) {
await loadPackageDetailsForItem(item); await loadPackageDetailsForItem(item);
} }
} }
@@ -1810,10 +1824,24 @@ defineExpose({ getList });
font-size: 12px; font-size: 12px;
color: #909399; color: #909399;
transition: transform 0.2s; transition: transform 0.2s;
transform: rotate(0deg);
} }
.expand-icon.expanded { .expand-icon.expanded {
transform: rotate(180deg); transform: rotate(90deg);
}
/* Bug #426修复: 展开内容容器 */
.expanded-content {
overflow: hidden;
}
/* Bug #426修复: 套餐明细加载提示 */
.package-loading-hint {
padding: 8px 10px;
font-size: 11px;
color: #c0c4cc;
text-align: center;
} }
/* Bug #428修复: 套餐明细列表样式 */ /* Bug #428修复: 套餐明细列表样式 */

View File

@@ -1528,7 +1528,11 @@ function handleMedicalAdvice(row) {
// 先清空旧数据 // 先清空旧数据
temporaryBillingMedicines.value = [] temporaryBillingMedicines.value = []
temporaryAdvices.value = [] temporaryAdvices.value = []
temporarySigned.value = false // 🔧 重置签名状态 // 🔧 修复 Bug #446: 如果是同一 encounter 且已有提交的医嘱(有 requestId保留签名状态
const hasSubmittedAdvices = temporaryAdvices.value.length > 0 &&
temporaryAdvices.value[0]?.originalMedicine?.encounterId === row.visitId &&
temporaryAdvices.value.some(a => a.originalMedicine?.requestId);
temporarySigned.value = hasSubmittedAdvices; // 修复:根据已有数据状态设置,而非盲目重置
temporaryMedicalLoading.value = true // 🔧 新增:开始加载 temporaryMedicalLoading.value = true // 🔧 新增:开始加载
// 调用计费接口获取数据 // 调用计费接口获取数据

View File

@@ -147,9 +147,10 @@
<el-button class="cancel-btn" @click="handleCancel">取消</el-button> <el-button class="cancel-btn" @click="handleCancel">取消</el-button>
<el-button <el-button
class="sign-btn" class="sign-btn"
:disabled="allItemsSubmitted"
@click="handleSignAndSubmit" @click="handleSignAndSubmit"
> >
{{ isSigned ? '提交医嘱' : '一键签名并生成医嘱' }} {{ allItemsSubmitted ? '已签发' : (isSigned ? '提交医嘱' : '一键签名并生成医嘱') }}
</el-button> </el-button>
</div> </div>
</div> </div>
@@ -310,6 +311,12 @@ const getMethodCodeDict = computed(() => {
return dict return dict
}) })
// 🔧 修复 Bug #446: 检查计费药品是否已全部提交(有 requestId用于区分"首次签名"和"已提交重开"
const allItemsSubmitted = computed(() => {
const meds = props.billingMedicines || []
return meds.length > 0 && meds.every(m => m.requestId)
})
// 响应式数据 - isSigned 从父组件传入的 prop 初始化 // 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp) const isSigned = ref(props.isSignedProp)