fix(clinic): 修复门诊手术安排计费弹窗vxe-table布局与项目选择问题

问题:
  1. vxe-table expand列40px切换格中渲染复杂编辑表单,内容溢出导致表头表体列错位
  2. adviceBaseList clickRow未解构vxe-table 4.x cell-click事件对象{row},导致selectAdviceBase数据错误
  3. prescriptionList数组元素替换(arr[i]={})不被vxe-table变更检测,选中项目后数据未填入input
  4. 保存按钮调用formRef{index}但表单已迁出expand列,运行时抛undefined.validate异常
This commit is contained in:
wangjian963
2026-06-08 14:42:54 +08:00
parent f940078208
commit a04fa368b1
2 changed files with 178 additions and 170 deletions

View File

@@ -263,9 +263,8 @@ const handleCurrentChange = (currentRow) => {
currentSelectRow.value = currentRow;
};
function clickRow(row, column, cell, event) {
// cell-click 事件会传递 row, column, cell, event 四个参数
// 确保传递的是完整的行数据
function clickRow({ row }) {
// vxe-table 4.x cell-click 事件参数是 { row, column, ... } 对象,需解构取 row
if (row) {
emit('selectAdviceBase', row);
}

View File

@@ -38,170 +38,15 @@
max-height="650"
:data="prescriptionList"
:row-config="{ keyField: 'uniqueKey', expandRowKeys: expandOrder }"
:column-config="{ resizable: true }"
border
auto-resize
@cell-dblclick="clickRowDb"
>
<vxe-column
type="expand"
width="40"
>
<template #default="scope">
<el-form
:ref="'formRef' + scope.rowIndex"
:model="scope.row"
:rules="rowRules"
>
<div style="padding: 16px; background: #f8f9fa; border-radius: 8px">
<!-- 药品类型adviceType == 1和耗材类型adviceType == 2使用相同的界面 -->
<template v-if="scope.row.adviceType == 1 || scope.row.adviceType == 2">
<div style="display: flex; align-items: center; margin-bottom: 16px; gap: 16px">
<span style="font-size: 16px; font-weight: 600">
{{
scope.row.adviceName +
' ' +
(scope.row.volume ? scope.row.volume + ' ' : '') +
(scope.row.unitPrice ? scope.row.unitPrice + ' 元/' : '') +
(scope.row.unitCode_dictText || '')
}}
</span>
<div class="form-group">
<!-- 库存不为空时显示批号选择 -->
<el-select
v-if="scope.row.stockList && scope.row.stockList.length > 0"
v-model="scope.row.lotNumber"
style="width: 180px; margin-right: 20px"
placeholder="选择批号"
>
<el-option
v-for="item in scope.row.stockList"
:key="item.lotNumber"
:value="item.lotNumber"
:label="
item.locationName +
' ' +
'批次号: ' +
item.lotNumber +
' ' +
' 库存:' +
(item.quantity / scope.row.partPercent).toFixed(2) +
item.unitCode_dictText +
' 单价:' +
item.price.toFixed(2) +
'/' +
item.unitCode_dictText
"
@click="handleNumberClick(item, scope.rowIndex)"
/>
</el-select>
<!-- 库存为空时显示提示 -->
<span
v-else
style="color: #EF4444; margin-right: 20px; font-size: 14px;"
>
无可用库存
</span>
<el-form-item
label="数量:"
prop="quantity"
class="required-field"
data-prop="quantity"
>
<el-input-number
v-model="scope.row.quantity"
placeholder="数量"
style="width: 70px"
controls-position="right"
:controls="false"
@keyup.enter.prevent="handleEnter('quantity', scope.row, scope.rowIndex)"
@input="calculateTotalPrice(scope.row, scope.rowIndex)"
/>
</el-form-item>
<el-select
v-if="scope.row.unitCodeList && scope.row.unitCodeList.length > 0"
v-model="scope.row.unitCode"
style="width: 70px; margin-right: 20px"
placeholder="单位"
@change="calculateTotalAmount(scope.row, scope.rowIndex)"
>
<template
v-for="item in scope.row.unitCodeList"
:key="item.value"
>
<el-option
v-if="item.type != unitMap['dose']"
:value="item.value"
:label="item.label"
/>
</template>
</el-select>
<span class="total-amount">
总金额{{ scope.row.totalPrice ? scope.row.totalPrice + ' 元' : '0.00 元' }}
</span>
</div>
<el-button
type="primary"
@click="handleSaveSign(scope.row, scope.rowIndex)"
>
保存
</el-button>
</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; margin-bottom: 16px; gap: 16px">
<span style="font-size: 16px; font-weight: 600">
{{
scope.row.adviceName + ' ' + scope.row.unitPrice
? Number(scope.row.unitPrice).toFixed(2)
: '-' + '元'
}}
</span>
<div class="form-group">
<el-form-item
label="执行次数:"
prop="quantity"
class="required-field"
data-prop="quantity"
>
<el-input-number
v-model="scope.row.quantity"
placeholder="执行次数"
style="width: 100px; margin: 0 20px"
controls-position="right"
:controls="false"
@keyup.enter.prevent="handleEnter('quantity', scope.row, scope.rowIndex)"
@input="calculateTotalPrice(scope.row, scope.rowIndex)"
/>
</el-form-item>
<el-tree-select
v-model="scope.row.orgId"
clearable
:data="organization"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
check-strictly
placeholder="请选择执行科室"
style="min-width: 150px; width: auto;"
class="org-select"
/>
<span class="total-amount">
总金额{{ scope.row.totalPrice ? scope.row.totalPrice + ' 元' : '0.00 元' }}
</span>
<span style="font-size: 16px; font-weight: 600">
<!-- 金额: {{ scope.row.priceList[0].price }} -->
</span>
</div>
<el-button
type="primary"
@click="handleSaveSign(scope.row, scope.rowIndex)"
>
保存
</el-button>
</div>
</template>
</div>
</el-form>
</template>
</vxe-column>
/>
<vxe-column
title=""
align="center"
@@ -370,6 +215,7 @@
<vxe-column
title="总量"
align="center"
width="100"
field=""
>
<template #default="scope">
@@ -383,6 +229,7 @@
align="right"
field=""
header-align="center"
width="130"
>
<template #default="scope">
<span
@@ -430,6 +277,151 @@
</template>
</vxe-column>
</vxe-table>
<!-- 编辑表单卡片:独立于表格,选中项目后显示在表格下方 -->
<div
v-if="editingRow"
class="edit-form-card"
>
<el-form
:ref="'editFormRef'"
:model="editingRow"
:rules="rowRules"
>
<div style="padding: 16px; background: #f8f9fa; border-radius: 8px">
<template v-if="editingRow.adviceType == 1 || editingRow.adviceType == 2">
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap">
<span style="font-size: 16px; font-weight: 600">
{{
editingRow.adviceName +
' ' +
(editingRow.volume ? editingRow.volume + ' ' : '') +
(editingRow.unitPrice ? editingRow.unitPrice + ' 元/' : '') +
(editingRow.unitCode_dictText || '')
}}
</span>
<div class="form-group">
<el-select
v-if="editingRow.stockList && editingRow.stockList.length > 0"
v-model="editingRow.lotNumber"
style="width: 180px; margin-right: 20px"
placeholder="选择批号"
>
<el-option
v-for="item in editingRow.stockList"
:key="item.lotNumber"
:value="item.lotNumber"
:label="
item.locationName +
' 批次号: ' + item.lotNumber +
' 库存:' + (item.quantity / editingRow.partPercent).toFixed(2) +
item.unitCode_dictText +
' 单价:' + item.price.toFixed(2) + '/' + item.unitCode_dictText
"
@click="handleNumberClick(item, editingRowIndex)"
/>
</el-select>
<span
v-else
style="color: #EF4444; margin-right: 20px; font-size: 14px;"
>无可用库存</span>
<el-form-item
label="数量"
prop="quantity"
class="required-field"
data-prop="quantity"
>
<el-input-number
v-model="editingRow.quantity"
placeholder="数量"
style="width: 70px"
controls-position="right"
:controls="false"
@keyup.enter.prevent="handleEnter('quantity', editingRow, editingRowIndex)"
@input="calculateTotalPrice(editingRow, editingRowIndex)"
/>
</el-form-item>
<el-select
v-if="editingRow.unitCodeList && editingRow.unitCodeList.length > 0"
v-model="editingRow.unitCode"
style="width: 70px; margin-right: 20px"
placeholder="单位"
@change="calculateTotalAmount(editingRow, editingRowIndex)"
>
<template
v-for="item in editingRow.unitCodeList"
:key="item.value"
>
<el-option
v-if="item.type != unitMap['dose']"
:value="item.value"
:label="item.label"
/>
</template>
</el-select>
<span class="total-amount">
总金额:{{ editingRow.totalPrice ? editingRow.totalPrice + ' 元' : '0.00 元' }}
</span>
</div>
<el-button
type="primary"
@click="handleSaveSign(editingRow, editingRowIndex)"
>
保存
</el-button>
</div>
</template>
<template v-else>
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap">
<span style="font-size: 16px; font-weight: 600">
{{
editingRow.adviceName + ' ' + editingRow.unitPrice
? Number(editingRow.unitPrice).toFixed(2)
: '-' + '元'
}}
</span>
<div class="form-group">
<el-form-item
label="执行次数"
prop="quantity"
class="required-field"
data-prop="quantity"
>
<el-input-number
v-model="editingRow.quantity"
placeholder="执行次数"
style="width: 100px; margin: 0 20px"
controls-position="right"
:controls="false"
@keyup.enter.prevent="handleEnter('quantity', editingRow, editingRowIndex)"
@input="calculateTotalPrice(editingRow, editingRowIndex)"
/>
</el-form-item>
<el-tree-select
v-model="editingRow.orgId"
clearable
:data="organization"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
check-strictly
placeholder="请选择执行科室"
style="min-width: 150px; width: auto;"
class="org-select"
/>
<span class="total-amount">
总金额:{{ editingRow.totalPrice ? editingRow.totalPrice + ' 元' : '0.00 元' }}
</span>
</div>
<el-button
type="primary"
@click="handleSaveSign(editingRow, editingRowIndex)"
>
保存
</el-button>
</div>
</template>
</div>
</el-form>
</div>
</div>
</template>
@@ -444,7 +436,7 @@ import {
getEncounterDiagnosis,
} from './api';
import adviceBaseList from './adviceBaseList';
import {getCurrentInstance, nextTick, ref, watch} from 'vue';
import {getCurrentInstance, nextTick, ref, watch, computed} from 'vue';
const emit = defineEmits(['selectDiagnosis']);
const prescriptionList = ref([]);
@@ -493,6 +485,14 @@ const isAdding = ref(false);
const isSaving = ref(false); // #437 防重复提交锁
const prescriptionRef = ref();
const expandOrder = ref([]); //目前的展开行
const editingRow = computed(() => {
if (expandOrder.value.length === 0) return null;
return prescriptionList.value.find(r => r.uniqueKey === expandOrder.value[0]) || null;
});
const editingRowIndex = computed(() => {
if (expandOrder.value.length === 0) return -1;
return prescriptionList.value.findIndex(r => r.uniqueKey === expandOrder.value[0]);
});
const stockList = ref([]);
const groupList = ref([])
const { proxy } = getCurrentInstance();
@@ -546,8 +546,11 @@ watch(
nextTick(() => {
const index = prescriptionList.value.findIndex((row) => row.uniqueKey === newValue[0]);
const items = proxy.$refs['formRef' + index]?.$el?.querySelectorAll('[data-prop]');
requiredProps.value = Array.from(items).map((item) => item.dataset.prop);
const formEl = proxy.$refs['editFormRef'];
if (formEl) {
const items = formEl.$el?.querySelectorAll('[data-prop]') || formEl.querySelectorAll?.('[data-prop]');
if (items) requiredProps.value = Array.from(items).map((item) => item.dataset.prop);
}
});
} else {
requiredProps.value = {};
@@ -831,11 +834,9 @@ async function selectAdviceBase(key, row) {
});
}
// 将选中的基础项“覆盖”到当前处方行(这是之前正常工作的核心逻辑)
prescriptionList.value[rowIndex.value] = {
...prescriptionList.value[rowIndex.value],
...JSON.parse(JSON.stringify(row)),
};
// 将选中的基础项“覆盖”到当前处方行
// 用 Object.assign 原地修改,确保 vxe-table 能检测到变更重新渲染
Object.assign(prescriptionList.value[rowIndex.value], JSON.parse(JSON.stringify(row)));
// 后续字段处理保持原样
// 🔧 修复执行科室逻辑:诊疗项目优先使用项目维护的所属科室(row.orgId)
@@ -1271,7 +1272,7 @@ function handleSaveSign(row, index) {
return;
}
isSaving.value = true; // #437 立即加锁,消除 TOCTOU 竞态
proxy.$refs['formRef' + index].validate((valid) => {
proxy.$refs['editFormRef'].validate((valid) => {
if (!valid) {
isSaving.value = false; // 验证失败释放锁
return;
@@ -1391,6 +1392,14 @@ defineExpose({ getListInfo, closeAllPopovers });
:deep(.vxe-table--expand-btn) {
display: none !important;
}
// 编辑表单卡片:独立于表格,显示在表格下方
.edit-form-card {
margin-top: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.medicine-title {
font-size: 16px;
font-weight: 600;