feat(basicmanage): 新增医嘱组套对话框组件和相关API

- 实现 MedicalOrderSetDialog.vue 组件,支持医嘱组套的新增、编辑功能
- 添加中药医嘱基础列表组件 tcmMedicineList.vue,支持键盘导航选择
- 创建医嘱组套相关API接口文件,包含个人、科室、全院组套的增删改查功能
- 实现医嘱组套的组合拆组功能,支持批量操作
- 集成分页、搜索、缓存等优化功能提升用户体验
- 添加表单验证和数据校验机制确保数据完整性
This commit is contained in:
2026-03-17 09:35:07 +08:00
parent 16f6fbb8cf
commit 03939fb232
4 changed files with 2103 additions and 0 deletions

View File

@@ -0,0 +1,885 @@
<template>
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="1300px" @close="handleDialogClose">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
<div class="dialog-top-row">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" style="width: 220px" />
</el-form-item>
<el-form-item label="使用范围" v-if="showRangeSelector && !isEdit">
<el-select
v-model="rangeSelectValue"
placeholder="个人/科室/全院"
style="width: 220px"
@change="handleRangeChange"
>
<el-option label="个人" value="personal" />
<el-option label="科室" value="department" />
<el-option label="全院" value="hospital" />
</el-select>
</el-form-item>
<el-form-item
label="参与者"
prop="practitionerId"
v-if="currentTab === 'personal' && !isDoctorStation && !showRangeSelector"
>
<el-select
v-model="formData.practitionerId"
placeholder="请选择参与者"
clearable
style="width: 220px"
>
<el-option
v-for="item in participantListOptions"
:key="item.practitionerId"
:label="item.practitionerName"
:value="item.practitionerId"
/>
</el-select>
</el-form-item>
<el-form-item label="科室" prop="organizationId" v-if="currentTab === 'department'">
<el-tree-select
clearable
v-model="formData.organizationId"
:data="organization"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
check-strictly
default-expand-all
placeholder="请选择科室"
style="width: 220px"
:render-after-expand="false"
@change="handleOrgChange"
/>
</el-form-item>
</div>
</el-form>
<div style="margin-bottom: 10px">
<el-button type="primary" @click="handleAddRow">新增</el-button>
<el-button @click="handleCombineGroup">组合</el-button>
<el-button @click="handleSplitGroup">拆组</el-button>
</div>
<el-table
max-height="650"
ref="prescriptionRef"
:data="prescriptionList"
row-key="uniqueKey"
border
@cell-click="clickRow"
@selection-change="handleSelectionChange"
:expand-row-keys="expandOrder"
>
<el-table-column type="selection" width="55" :selectable="isRowSelectable" />
<el-table-column label="组" align="center" width="60">
<template #default="scope">
<span v-if="scope.row.groupId">{{ getGroupIcon(scope.row) }}</span>
</template>
</el-table-column>
<el-table-column label="类型" align="center" width="120">
<template #default="scope">
<template v-if="!scope.row.groupPackageId">
<el-radio-group v-model="scope.row.therapyEnum" size="small">
<el-radio-button label="1">长期</el-radio-button>
<el-radio-button label="2">临时</el-radio-button>
</el-radio-group>
</template>
<span v-else>
{{
scope.row.therapyEnum == '1' ? '长期' : scope.row.therapyEnum == '2' ? '临时' : '-'
}}
</span>
</template>
</el-table-column>
<el-table-column label="医嘱" align="center" prop="productName" width="300">
<template #default="scope">
<template v-if="getRowDisabled(scope.row)">
<el-popover
:popper-style="{ padding: '0' }"
placement="bottom-start"
:visible="scope.row.showPopover"
:width="1200"
>
<adviceBaseList
ref="adviceTableRef"
:popoverVisible="scope.row.showPopover"
:adviceQueryParams="adviceQueryParams"
@selectAdviceBase="(row) => selectAdviceBase(scope.row.uniqueKey, row)"
/>
<template #reference>
<el-input
:ref="'adviceRef' + scope.$index"
style="width: 100%"
v-model="scope.row.adviceName"
placeholder="请选择项目"
@input="(value) => handleInput(value, scope.row, scope.$index)"
@click="handleFocus(scope.row, scope.$index)"
@keyup.enter.stop="handleFocus(scope.row, scope.$index)"
@keydown="
(e) => {
if (!scope.row.showPopover) return;
if (['ArrowUp', 'ArrowDown', 'Enter'].includes(e.key)) {
e.preventDefault();
adviceTableRef.handleKeyDown(e);
}
}
"
@blur="handleBlur(scope.row)"
/>
</template>
</el-popover>
</template>
<span v-else>{{ scope.row.adviceName }}</span>
</template>
</el-table-column>
<el-table-column label="单次剂量" align="center" width="250" prop="sortNumber">
<template #default="scope">
<template v-if="scope.row.adviceType == 1">
<!-- 新增/未保存行统一按数量 + 单位 = 剂量 + 单位展示 -->
<template v-if="!scope.row.groupPackageId">
<el-input
style="width: 70px; margin-right: 10px"
v-model="scope.row.doseQuantity"
@input="
(value) => {
scope.row.dose = value * scope.row.unitConversionRatio;
}
"
/>
<span>
{{
scope.row.minUnitCode_dictText ||
scope.row.unitCodeName ||
scope.row.unitCode_dictText ||
''
}}
</span>
<span>{{ ' = ' }}</span>
<el-input
style="width: 70px; margin-right: 10px"
v-model="scope.row.dose"
@input="
(value) => {
scope.row.doseQuantity = value / scope.row.unitConversionRatio;
}
"
/>
<span>
{{
scope.row.doseUnitCode_dictText ||
scope.row.unitCodeName ||
scope.row.unitCode_dictText ||
''
}}
</span>
</template>
<span v-else>{{ scope.row.dose }}</span>
</template>
<span v-else>{{ '-' }}</span>
</template>
</el-table-column>
<el-table-column label="给药途径" align="center" width="150" prop="sortNumber">
<template #default="scope">
<template v-if="scope.row.adviceType == 1">
<template v-if="!scope.row.groupPackageId">
<el-select v-model="scope.row.methodCode" placeholder="给药途径" clearable filterable>
<el-option
v-for="dict in method_code"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</template>
<span v-else>{{ scope.row.methodCode }}</span>
</template>
<span v-else>{{ '-' }}</span>
</template>
</el-table-column>
<el-table-column label="用药频次" align="center" width="150" prop="sortNumber">
<template #default="scope">
<template v-if="scope.row.adviceType == 1">
<template v-if="!scope.row.groupPackageId">
<el-select
v-model="scope.row.rateCode"
placeholder="频次"
style="width: 120px"
filterable
>
<el-option
v-for="dict in getRateOptions(scope.row)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</template>
<span v-else>{{ scope.row.rateCode_dictText }}</span>
</template>
<span v-else>{{ '-' }}</span>
</template>
</el-table-column>
<el-table-column label="用药天数" align="center" width="100" prop="sortNumber">
<template #default="scope">
<template v-if="scope.row.adviceType == 1">
<template v-if="!scope.row.groupPackageId">
<el-input
v-model="scope.row.dispensePerDuration"
@change="handleQuantityChange(scope.row)"
/>
</template>
<span v-else>{{ scope.row.dispensePerDuration }}</span>
</template>
<span v-else>{{ '-' }}</span>
</template>
</el-table-column>
<el-table-column label="总量/执行次数" align="center" width="150" prop="sortNumber">
<template #default="scope">
<template v-if="!scope.row.groupPackageId">
<el-input
v-model="scope.row.sortNumber"
type="number"
min="1"
@change="handleQuantityChange(scope.row)"
/>
</template>
<span v-else>{{ scope.row.sortNumber }}</span>
</template>
</el-table-column>
<el-table-column label="单位" align="center" width="120" prop="unitCode">
<template #default="scope">
<template v-if="!scope.row.groupPackageId">
<el-select
v-model="scope.row.selectUnitCode"
placeholder="请选择单位"
@change="handleUnitChange(scope.row)"
>
<el-option
v-if="scope.row.minUnitCode"
:key="scope.row.minUnitCode"
:label="scope.row.minUnitCode_dictText || scope.row.minUnitCode"
:value="scope.row.minUnitCode"
/>
<el-option
v-if="scope.row.unitCode"
:key="scope.row.unitCode"
:label="scope.row.unitCode_dictText || scope.row.unitCode"
:value="scope.row.unitCode"
/>
</el-select>
</template>
<span>{{ scope.row.unitCodeName }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="80">
<template #default="scope">
<el-button
type="danger"
icon="Delete"
circle
size="small"
@click="handleDeleteRow(scope.$index, scope.row)"
/>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue';
import adviceBaseList from './adviceBaseList';
import {
queryParticipantList,
queryGroupDetail,
getOrgTree,
} from './api.js';
import { saveOrderGroup } from '@/views/doctorstation/components/api.js';
import useUserStore from '@/store/modules/user';
import { ElMessage } from 'element-plus';
const props = defineProps({
// 是否在“医生站/住院医生站”场景复用个人不选人practitionerId 置空),科室可选科室
isDoctorStation: {
type: Boolean,
default: false,
},
method_code: {
type: Array,
default: () => [],
},
rate_code: {
type: Array,
default: () => [],
},
// 顶部是否显示“个人/科室/全院”切换(用于住院医生站顶部【组套】按钮场景)
showRangeSelector: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['saved']);
const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const dialogVisible = ref(false);
const dialogTitle = ref('');
const currentTab = ref('personal'); // personal | department | hospital
const isEdit = ref(false);
const submitLoading = ref(false);
const rangeSelectValue = ref('personal');
const participantListOptions = ref([]);
const organization = ref([]);
const adviceQueryParams = reactive({});
const formData = reactive({
groupPackageId: undefined,
name: '',
practitionerId: undefined,
organizationId: undefined,
});
const prescriptionList = ref([]);
const expandOrder = ref([]);
const nextId = ref(1);
const rowIndex = ref(0);
const selectedRows = ref([]);
const groupIndex = ref(1);
const formRef = ref();
const prescriptionRef = ref();
const adviceTableRef = ref();
const formRules = computed(() => {
const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
};
// 独立页面:个人必须选参与者;科室必须选科室
if (!props.isDoctorStation && currentTab.value === 'personal') {
rules.practitionerId = [{ required: true, message: '请选择参与者', trigger: 'change' }];
}
if (currentTab.value === 'department') {
rules.organizationId = [{ required: true, message: '请选择科室', trigger: 'change' }];
}
return rules;
});
onMounted(() => {
queryParticipantList().then((res) => {
participantListOptions.value = res.data || [];
});
getOrgTree().then((res) => {
organization.value = res.data?.records || [];
});
});
function openAdd(tab) {
currentTab.value = tab;
isEdit.value = false;
dialogTitle.value = '新增医嘱';
dialogVisible.value = true;
if (props.showRangeSelector) rangeSelectValue.value = tab;
prescriptionList.value = [];
expandOrder.value = [];
nextId.value = 1;
rowIndex.value = 0;
selectedRows.value = [];
groupIndex.value = 1;
formData.groupPackageId = undefined;
formData.name = '';
formData.practitionerId = undefined;
formData.organizationId = undefined;
Object.keys(adviceQueryParams).forEach((key) => {
delete adviceQueryParams[key];
});
prescriptionRef.value?.clearSelection?.();
if (tab === 'personal') {
// 医生站场景:个人组套需要关联当前医生才能查询到
formData.practitionerId = userStore.practitionerId;
} else if (tab === 'department') {
formData.organizationId = userStore.orgId;
}
addEmptyRow();
}
function handleRangeChange(tab) {
openAdd(tab);
}
/**
* 从外部选中的医嘱直接生成组套明细
* @param tab 使用范围personal / department / hospital
* @param rows 选中的处方行列表
*/
function openFromSelection(tab, rows = []) {
currentTab.value = tab;
isEdit.value = false;
dialogTitle.value = '另存组套';
dialogVisible.value = true;
if (props.showRangeSelector) rangeSelectValue.value = tab;
// 重置表单
formData.groupPackageId = undefined;
formData.name = '';
formData.practitionerId = undefined;
formData.organizationId = undefined;
if (tab === 'personal') {
// 医生站场景:个人组套需要关联当前医生才能查询到
formData.practitionerId = userStore.practitionerId;
} else if (tab === 'department') {
formData.organizationId = userStore.orgId;
}
const validRows = (rows || []).filter((i) => i.adviceDefinitionId);
if (validRows.length === 0) {
proxy.$modal.msgWarning('所选医嘱中没有有效的医嘱项,请先选择医嘱后再另存组套');
dialogVisible.value = false;
return;
}
prescriptionList.value = validRows.map((row, index) => {
let therapyEnum = row.therapyEnum;
if (!therapyEnum && row.contentJson) {
try {
const content = JSON.parse(row.contentJson);
therapyEnum = content.therapyEnum;
} catch (e) {}
}
therapyEnum = therapyEnum != null ? String(therapyEnum) : '1';
return {
uniqueKey: index + 1,
showPopover: false,
check: false,
isEdit: false,
statusEnum: 1,
adviceDefinitionId: row.adviceDefinitionId,
adviceTableName: row.adviceTableName || 'advice_definition',
sortNumber: row.quantity ?? row.sortNumber ?? 1,
selectUnitCode: row.selectUnitCode || row.unitCode,
adviceName: row.adviceName || row.productName,
unitCodeName: row.unitCodeName || row.unitCode_dictText || '',
methodCode: row.methodCode,
rateCode: row.rateCode,
dispensePerDuration: row.dispensePerDuration,
dose: row.dose,
doseQuantity: row.doseQuantity,
minUnitCode: row.minUnitCode,
minUnitCode_dictText: row.minUnitCode_dictText,
unitCode: row.unitCode,
unitCode_dictText: row.unitCode_dictText,
groupId: row.groupId,
groupOrder: row.groupOrder,
therapyEnum: therapyEnum,
adviceType: row.adviceType,
};
});
expandOrder.value = [];
nextId.value = prescriptionList.value.length + 1;
}
function openEdit(tab, row) {
currentTab.value = tab;
isEdit.value = true;
dialogTitle.value = '编辑医嘱';
dialogVisible.value = true;
formData.groupPackageId = row.groupPackageId;
formData.name = row.name;
formData.practitionerId = row.practitionerId;
formData.organizationId = row.organizationId;
queryGroupDetail({ groupPackageId: row.groupPackageId }).then((res) => {
const detailList = res.data || [];
prescriptionList.value = detailList.map((item, index) => {
const therapyEnum = item.therapyEnum != null ? String(item.therapyEnum) : '1'; // 统一转成字符串
return {
uniqueKey: index + 1,
showPopover: false,
check: false,
isEdit: false,
statusEnum: 1,
groupPackageId: item.groupPackageId,
adviceDefinitionId: item.orderDefinitionId,
adviceTableName: item.orderDefinitionTable,
sortNumber: item.quantity,
selectUnitCode: item.unitCode,
adviceName: item.orderDefinitionName,
unitCodeName: item.unitCodeName,
groupId: item.groupId,
groupOrder: item.groupOrder,
therapyEnum, // 长期/临时类型1-长期2-临时
// 回显单次剂量/给药途径/用药频次/天数等字段
dispensePerDuration: item.dispensePerDuration,
dose: item.dose,
doseQuantity: item.doseQuantity,
methodCode: item.methodCode,
rateCode: item.rateCode,
// 医嘱类型(药品=1没有则按表名推断药品表 -> 药品)
adviceType:
item.adviceType !== undefined
? item.adviceType
: item.orderDefinitionTable === 'med_medication_definition'
? 1
: undefined,
};
});
nextId.value = prescriptionList.value.length + 1;
addEmptyRow();
});
}
function addEmptyRow() {
prescriptionList.value.unshift({
uniqueKey: nextId.value++,
showPopover: false,
check: false,
isEdit: true,
statusEnum: 1,
therapyEnum: '1', // 默认长期
});
}
function handleAddRow() {
addEmptyRow();
}
function handleSelectionChange(selection) {
selectedRows.value = selection || [];
}
function handleCombineGroup() {
const rows = selectedRows.value || [];
if (rows.length <= 1) {
proxy.$modal.msgWarning('至少选择两项');
return;
}
// 必须都已选择医嘱
if (rows.some((r) => !r.adviceDefinitionId)) {
proxy.$modal.msgWarning('请先完成医嘱选择');
return;
}
// 相同分组用法需要相同
const methodSet = new Set();
rows.forEach((r) => {
if (r.methodCode != null) methodSet.add(r.methodCode);
});
if (methodSet.size > 1) {
proxy.$modal.msgWarning('同一分组药品用法必须相同');
return;
}
// 相同分组频次需要相同
const rateSet = new Set();
rows.forEach((r) => {
if (r.rateCode != null) rateSet.add(r.rateCode);
});
if (rateSet.size > 1) {
proxy.$modal.msgWarning('同一分组药品频次必须相同');
return;
}
const timestamp = Date.now().toString();
const gid = timestamp + groupIndex.value;
const newList = [...prescriptionList.value];
rows.forEach((item, selectIndex) => {
const idx = newList.findIndex((r) => r.uniqueKey === item.uniqueKey);
if (idx !== -1) {
newList[idx].groupId = gid;
newList[idx].groupOrder = selectIndex + 1;
}
});
groupIndex.value += 1;
prescriptionList.value = newList;
prescriptionRef.value?.clearSelection?.();
proxy.$modal.msgSuccess('组套成功');
}
function isRowSelectable(row) {
return !!row.adviceDefinitionId;
}
function getGroupIcon(row) {
if (!row.groupId) return '';
const sameGroup = prescriptionList.value
.filter((item) => item.groupId === row.groupId)
.sort((a, b) => {
const aOrder = a.groupOrder ?? 0;
const bOrder = b.groupOrder ?? 0;
return aOrder - bOrder;
});
if (sameGroup.length === 0) return '';
const index = sameGroup.findIndex((item) => item.uniqueKey === row.uniqueKey);
if (index === 0) {
return '┏';
}
if (index === sameGroup.length - 1) {
return '┗';
}
return '┃';
}
function getRateOptions(row) {
const all = props.rate_code || [];
if (row?.therapyEnum == '2') {
return all.filter(
(dict) => dict.value === 'ST' || (dict.label && dict.label.indexOf('立即') !== -1)
);
}
return all;
}
function handleSplitGroup() {
const rows = selectedRows.value || [];
if (rows.length < 1) {
proxy.$modal.msgWarning('至少选择一项');
return;
}
if (rows.some((r) => !r.groupId)) {
proxy.$modal.msgWarning('包含非组合数据无法拆组');
return;
}
const newList = [...prescriptionList.value];
rows.forEach((item) => {
const idx = newList.findIndex((r) => r.uniqueKey === item.uniqueKey);
if (idx !== -1) {
newList[idx].groupId = undefined;
newList[idx].groupOrder = undefined;
}
});
prescriptionList.value = newList;
prescriptionRef.value?.clearSelection?.();
proxy.$modal.msgSuccess('拆组成功');
}
function submitForm() {
formRef.value.validate((valid) => {
if (!valid) return;
// 验证至少选择了一条医嘱
const detailList = prescriptionList.value.filter((item) => item.adviceDefinitionId);
if (detailList.length === 0) {
proxy?.$modal?.msgWarning?.('请至少选择一条医嘱');
return;
}
submitLoading.value = true;
setTimeout(() => {
console.log('[submitForm] 当前tab:', currentTab.value);
const params = { ...formData };
console.log('[submitForm] formData:', formData);
// 添加 rangeCode 字段,用于后端判断组套类型
if (currentTab.value === 'personal') {
params.rangeCode = 1;
} else if (currentTab.value === 'department') {
params.rangeCode = 2;
} else {
params.rangeCode = 3;
}
console.log('[submitForm] 设置后的rangeCode:', params.rangeCode);
// 医生站场景:个人组套需要设置 practitionerId 为当前医生,否则查询时查不到
console.log('[submitForm] isDoctorStation:', props.isDoctorStation, 'currentTab:', currentTab.value);
console.log('[submitForm] userStore.practitionerId:', userStore.practitionerId);
if (props.isDoctorStation && currentTab.value === 'personal') {
params.practitionerId = userStore.practitionerId;
console.log('[submitForm] 已设置 practitionerId:', params.practitionerId);
} else {
console.log('[submitForm] 未设置 practitionerId, 当前值:', params.practitionerId);
}
params.detailList = detailList.map((item) => ({
orderDefinitionId: item.adviceDefinitionId,
orderDefinitionTable: item.adviceTableName,
quantity: item.sortNumber,
unitCode: item.selectUnitCode,
methodCode: item.methodCode,
rateCode: item.rateCode,
dispensePerDuration: item.dispensePerDuration,
dose: item.dose,
doseQuantity: item.doseQuantity,
groupId: item.groupId,
groupOrder: item.groupOrder,
therapyEnum: item.therapyEnum || '1',
}));
console.log('[submitForm] 准备保存组套, params:', params);
// 使用 saveOrderGroup它会根据 rangeCode 自动选择正确的API
saveOrderGroup(params)
.then((res) => {
console.log('[submitForm] 保存API返回:', res);
if (res.code === 200) {
ElMessage.success(res.message || '保存成功');
console.log('[submitForm] 准备触发 saved 事件');
emit('saved');
console.log('[submitForm] saved 事件已触发');
dialogVisible.value = false;
prescriptionList.value = [];
} else {
proxy?.$modal?.msgError?.(res.message || '保存失败');
}
})
.catch((err) => {
console.error('保存失败:', err);
proxy?.$modal?.msgError?.(err?.message || '保存失败,请检查网络或联系管理员');
})
.finally(() => {
submitLoading.value = false;
});
}, 200);
});
}
function selectAdviceBase(key, row) {
const currentRow = prescriptionList.value[rowIndex.value];
const preservedTherapyEnum = currentRow?.therapyEnum || row.therapyEnum || '1';
prescriptionList.value[rowIndex.value] = {
...prescriptionList.value[rowIndex.value],
...JSON.parse(JSON.stringify(row)),
};
prescriptionList.value[rowIndex.value].dose = undefined;
prescriptionList.value[rowIndex.value].doseQuantity = undefined;
prescriptionList.value[rowIndex.value].doseUnitCode = row.doseUnitCode;
prescriptionList.value[rowIndex.value].minUnitCode = row.minUnitCode;
prescriptionList.value[rowIndex.value].unitCode =
row.partAttributeEnum == 1 ? row.minUnitCode : row.unitCode;
prescriptionList.value[rowIndex.value].categoryEnum = row.categoryCode;
prescriptionList.value[rowIndex.value].isEdit = false;
prescriptionList.value[rowIndex.value].definitionId = JSON.parse(
JSON.stringify(row)
).chargeItemDefinitionId;
prescriptionList.value[rowIndex.value].therapyEnum = preservedTherapyEnum;
addEmptyRow();
expandOrder.value = [key];
}
function handleFocus(row, index) {
rowIndex.value = index;
row.showPopover = true;
adviceQueryParams.searchKey = row.adviceName || '';
// 设置 organizationId否则 adviceBaseList 会跳过 API 调用
adviceQueryParams.organizationId = userStore.orgId;
}
function handleInput(value, row, index) {
adviceQueryParams.searchKey = value || '';
handleFocus(row, index);
}
function handleBlur(row) {
row.showPopover = false;
}
function clickRow() {}
function getRowDisabled(row) {
return row.isEdit;
}
function handleQuantityChange(row) {
if (row.sortNumber && row.sortNumber > 0) row.sortNumber = parseInt(row.sortNumber);
else row.sortNumber = 1;
prescriptionList.value = [...prescriptionList.value];
}
function handleUnitChange(row) {
prescriptionList.value = [...prescriptionList.value];
}
function handleDeleteRow(index) {
if (prescriptionList.value.length <= 1) {
proxy.$modal.msgWarning('至少保留一行');
return;
}
proxy.$modal
.confirm('确定要删除该行吗?')
.then(() => {
prescriptionList.value.splice(index, 1);
proxy.$modal.msgSuccess('删除成功');
})
.catch(() => {});
}
function isLeafNode(node) {
return !node.children || node.children.length === 0;
}
function handleDialogClose() {
formRef.value?.resetFields?.();
prescriptionList.value = [];
}
function handleOrgChange(value) {
if (!value) return;
const findNode = (nodes) => {
for (let node of nodes) {
if (node.id === value) return node;
if (node.children && node.children.length > 0) {
const found = findNode(node.children);
if (found) return found;
}
}
return null;
};
const selectedNode = findNode(organization.value);
if (selectedNode && !isLeafNode(selectedNode)) {
proxy.$modal.msgWarning('只能选择末级科室');
formData.organizationId = null;
}
}
defineExpose({
openAdd,
openEdit,
openFromSelection,
});
</script>
<style scoped>
.dialog-top-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.dialog-top-row :deep(.el-form-item) {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,129 @@
import request from '@/utils/request'
/**
* 获取个人组套
* @param {*} queryParams
*/
export function getPersonalList(queryParams) {
return request({
url: '/personalization/orders-group-package/get-personal',
method: 'get',
params: queryParams
})
}
/**
* 获取科室组套
* @param {*} queryParams
*/
export function getDeptList(queryParams) {
return request({
url: '/personalization/orders-group-package/get-organization',
method: 'get',
params: queryParams
})
}
/**
* 获取全院组套
* @param {*} queryParams
*/
export function getAllList(queryParams) {
return request({
url: '/personalization/orders-group-package/get-hospital',
method: 'get',
params: queryParams
})
}
/**
* 保存个人组套
* @param {*} data
*/
export function savePersonal(data) {
return request({
url: '/personalization/orders-group-package/save-personal',
method: 'post',
data: data
})
}
/**
* 保存科室组套
* @param {*} data
*/
export function saveDepartment(data) {
return request({
url: '/personalization/orders-group-package/save-organization',
method: 'post',
data: data
})
}
/**
* 保存全院组套
* @param {*} data
*/
export function saveAll(data) {
return request({
url: '/personalization/orders-group-package/save-hospital',
method: 'post',
data: data
})
}
/**
* 查询组套明细
* @param {*} data
*/
export function queryGroupDetail(params) {
return request({
url: '/personalization/orders-group-package/get-group-package-detail',
method: 'get',
params: params
})
}
/**
* 删除组套
* @param {*} data
*/
export function deleteGroup(data) {
return request({
url: '/personalization/orders-group-package/group-package-detail?groupPackageId=' + data.groupPackageId,
method: 'delete'
})
}
/**
* 查询参与者下拉列表
* @param {*} data
*/
export function queryParticipantList(params) {
return request({
url: '/app-common/practitioner-list',
method: 'get',
params: params
})
}
/**
* 获取科室列表
*/
export function getOrgTree() {
return request({
url: '/base-data-manage/organization/organization',
method: 'get',
})
}
/**
* 获取中药列表
*/
export function getTcmMedicine(params) {
return request({
url: '/doctor-station/chinese-medical/tcm-advice-base-info',
method: 'get',
params: params,
});
}

View File

@@ -0,0 +1,269 @@
<template>
<div @keyup="handleKeyDown" tabindex="0" ref="tableWrapper" class="table-container">
<el-table
ref="adviceBaseRef"
height="350"
:data="adviceBaseList"
highlight-current-row
@current-change="handleCurrentChange"
row-key="adviceCode"
v-loading="loading"
@cell-click="clickRow"
>
<el-table-column label="名称" align="center" prop="adviceName" />
<el-table-column label="类型" align="center" prop="categoryCodeText" />
<el-table-column label="医保等级" align="center" prop="chrgitmLv_dictText" />
<el-table-column label="包装单位" align="center" prop="unitCode_dictText" />
<el-table-column label="最小单位" align="center" prop="minUnitCode_dictText" />
<el-table-column label="库存数量" align="center">
<template #default="scope">{{ handleQuantity(scope.row) }}</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="queryParams.pageNum"
:page-size="20"
:total="total"
layout="prev, pager, next"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue';
import { getTcmMedicine } from './api';
import { debounce } from 'lodash-es';
const props = defineProps({
searchKey: {
type: String,
default: '',
},
organizationId: {
type: String,
default: '',
},
});
const emit = defineEmits(['selectMedicine']);
const total = ref(0);
const loading = ref(false);
const adviceBaseRef = ref();
const tableWrapper = ref();
const currentIndex = ref(0);
const currentSelectRow = ref({});
const queryParams = ref({
pageSize: 20,
pageNum: 1,
searchKey: '',
organizationId: props.organizationId,
});
const adviceBaseList = ref([]);
// 前端缓存 - 使用 sessionStorage 持久化缓存
const TCM_CACHE_PREFIX = 'tcm_advice_cache_';
const TCM_CACHE_EXPIRE = 5 * 60 * 1000; // 缓存5分钟
function getTcmCacheKey(orgId, pageNum, searchKey) {
return TCM_CACHE_PREFIX + orgId + '_' + pageNum + '_' + (searchKey || 'none');
}
function getFromTcmCache(orgId, pageNum, searchKey) {
try {
const key = getTcmCacheKey(orgId, pageNum, searchKey);
const cachedData = sessionStorage.getItem(key);
if (!cachedData) return null;
const cacheData = JSON.parse(cachedData);
if (Date.now() - cacheData.timestamp > TCM_CACHE_EXPIRE) {
sessionStorage.removeItem(key);
return null;
}
console.log('从TCM前端缓存获取:', key);
return cacheData;
} catch (e) {
console.error('读取TCM缓存失败:', e);
return null;
}
}
function setToTcmCache(orgId, pageNum, searchKey, data, total) {
try {
const key = getTcmCacheKey(orgId, pageNum, searchKey);
const cacheData = {
data: data,
total: total,
timestamp: Date.now()
};
sessionStorage.setItem(key, JSON.stringify(cacheData));
console.log('写入TCM前端缓存:', key);
} catch (e) {
console.error('写入TCM缓存失败:', e);
}
}
// 防抖函数 - 避免重复请求
const debouncedGetList = debounce(
() => {
// 只有当 organizationId 有效时才请求
if (!queryParams.value.organizationId) {
console.log('organizationId 无效,跳过请求');
return;
}
getList();
},
300,
{ leading: false, trailing: true }
);
// 监听搜索关键字变化
watch(
() => props.searchKey,
(newValue) => {
queryParams.value.searchKey = newValue || '';
debouncedGetList();
},
{ deep: true }
);
// 监听 organizationId 变化
watch(
() => props.organizationId,
(newValue) => {
queryParams.value.organizationId = newValue;
// organizationId 变化时重新加载
if (newValue) {
debouncedGetList();
}
},
{ deep: true }
);
// 不再页面加载时立即请求,等待用户点击时再请求
function getList() {
const orgId = queryParams.value.organizationId;
const pageNum = queryParams.value.pageNum;
const searchKey = queryParams.value.searchKey;
// 尝试从本地缓存获取(只有第一页且无搜索关键字时使用缓存)
if (pageNum === 1 && !searchKey) {
const cached = getFromTcmCache(orgId, pageNum, searchKey);
if (cached) {
adviceBaseList.value = cached.data;
total.value = cached.total;
return;
}
}
// 显示 loading
loading.value = true;
getTcmMedicine(queryParams.value).then((res) => {
adviceBaseList.value = res.data.records || [];
total.value = res.data.total || 0;
// 缓存第一页数据
if (pageNum === 1 && !searchKey) {
setToTcmCache(orgId, pageNum, searchKey, adviceBaseList.value, total.value);
}
nextTick(() => {
currentIndex.value = 0;
if (adviceBaseList.value.length > 0 && adviceBaseRef.value) {
adviceBaseRef.value.setCurrentRow(adviceBaseList.value[0]);
}
});
})
.finally(() => {
loading.value = false;
});
}
// 当前页改变
function handlePageChange(page) {
queryParams.value.pageNum = page;
getList();
}
// 处理键盘事件
const handleKeyDown = (event) => {
const key = event.key;
const data = adviceBaseList.value;
switch (key) {
case 'ArrowUp':
event.preventDefault();
if (currentIndex.value > 0) {
currentIndex.value--;
setCurrentRow(data[currentIndex.value]);
}
break;
case 'ArrowDown':
event.preventDefault();
if (currentIndex.value < data.length - 1) {
currentIndex.value++;
setCurrentRow(data[currentIndex.value]);
}
break;
case 'Enter':
event.preventDefault();
if (currentSelectRow.value) {
emit('selectMedicine', currentSelectRow.value);
}
break;
}
};
function handleQuantity(row) {
if (row.inventoryList) {
const totalQuantity = row.inventoryList.reduce((sum, item) => sum + (item.quantity || 0), 0);
return totalQuantity.toString() + (row.minUnitCode_dictText || '');
}
return 0;
}
// 设置选中行(带滚动)
const setCurrentRow = (row) => {
adviceBaseRef.value.setCurrentRow(row);
const tableBody = adviceBaseRef.value.$el.querySelector('.el-table__body-wrapper');
const currentRowEl = adviceBaseRef.value.$el.querySelector('.current-row');
if (tableBody && currentRowEl) {
currentRowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
// 当前行变化时更新索引
const handleCurrentChange = (currentRow) => {
currentIndex.value = adviceBaseList.value.findIndex((item) => item === currentRow);
currentSelectRow.value = currentRow;
};
function clickRow(row) {
emit('selectMedicine', row);
}
defineExpose({
handleKeyDown,
});
</script>
<style scoped>
.table-container {
display: flex;
flex-direction: column;
}
.pagination-wrapper {
display: flex;
justify-content: center;
padding: 10px 0;
background: #fff;
}
.popover-table-wrapper:focus {
outline: 2px solid #409eff;
}
</style>

View File

@@ -0,0 +1,820 @@
<template>
<div class="app-container tcm-medical-order-set">
<el-row :gutter="20" class="full-height">
<!-- 左侧树形结构 -->
<el-col :span="5" class="left-panel">
<el-card shadow="never" class="tree-card panel-shadow">
<template #header>
<span>中医组套</span>
</template>
<el-tree
:data="treeData"
node-key="id"
:props="treeProps"
default-expand-all
highlight-current
@node-click="(data, node) => handleTreeNodeClick(data, node)"
/>
</el-card>
</el-col>
<!-- 右侧操作区域 -->
<el-col :span="19" class="right-panel">
<!-- 上方表单:组套公共字段 -->
<el-card shadow="never" class="form-card panel-shadow">
<div style="display: flex; justify-content: flex-end; margin-bottom: 10px">
<el-button type="primary" plain @click="clearForm">清空 </el-button>
<el-button type="primary" @click="createNew">新建 </el-button>
<el-button type="primary" @click="saveCommonForm">保存 </el-button>
</div>
<div class="common-bar">
<el-form :model="commonForm" :inline="true" class="demo-form-inline">
<el-form-item label="组套名称">
<el-input v-model="commonForm.name" placeholder="组套名称" clearable />
</el-form-item>
<el-form-item label="使用范围">
<el-select
v-model="commonForm.useRange"
placeholder="个人/科室/全院"
clearable
@change="handleRangeChange"
>
<el-option label="个人" value="personal" />
<el-option label="科室" value="department" />
<el-option label="全院" value="hospital" />
</el-select>
</el-form-item>
<el-form-item label="使用人" v-if="commonForm.useRange === 'personal'">
<el-select
v-model="commonForm.practitionerId"
placeholder="请选择使用人"
clearable
style="width: 200px"
>
<el-option
v-for="item in participantListOptions"
:key="item.practitionerId"
:label="item.practitionerName"
:value="item.practitionerId"
/>
</el-select>
</el-form-item>
<el-form-item label="科室" v-if="commonForm.useRange === 'department'">
<el-tree-select
clearable
v-model="commonForm.organizationId"
:data="organization"
:props="{ value: 'id', label: 'name', children: 'children' }"
value-key="id"
check-strictly
default-expand-all
placeholder="请选择科室"
style="width: 200px"
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="用法">
<el-select v-model="commonForm.method" placeholder="用法" clearable>
<el-option
v-for="item in method_code"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="频次">
<el-select v-model="commonForm.frequency" placeholder="频次" clearable>
<el-option
v-for="item in rate_code"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</el-card>
<!-- 下方:中药明细卡片列表 -->
<el-card shadow="never" class="detail-card panel-shadow" style="height: 100%">
<div class="drug-card-list">
<!-- 已有药品卡片 -->
<el-card
v-for="(item, index) in drugList"
:key="item.id"
class="drug-card"
shadow="never"
>
<template v-if="!item.isEditing">
<div class="drug-card-body">
<div class="drug-top">
<div class="drug-view-name">{{ item.drugName || '-' }}</div>
</div>
<div class="drug-divider"></div>
<div class="drug-bottom" style="margin-top: 10px">
<div class="drug-bottom-left">
<span class="drug-view-qty">{{ item.quantity }} {{ item.unit }}</span>
</div>
<div class="drug-view-actions">
<el-button
type="primary"
text
:icon="Edit"
@click.stop="editDrug(item)"
title="编辑"
/>
<el-button
v-if="drugList.length > 1"
type="danger"
text
:icon="Delete"
@click.stop="removeDrug(index)"
title="删除"
/>
</div>
</div>
</div>
</template>
<template v-else>
<div class="drug-card-body">
<div class="drug-top">
<el-form :model="item" class="drug-card-form">
<el-form-item>
<el-popover
:popper-style="{ padding: '0' }"
placement="bottom-start"
:visible="item.showPopover"
:width="800"
>
<tcmMedicineList
:ref="(el) => setMedicineTableRef(el, item)"
:searchKey="item.searchKey"
:organizationId="commonForm.organizationId"
@selectMedicine="(row) => handleSelectMedicine(row, item)"
/>
<template #reference>
<el-input
v-model="item.drugName"
placeholder="请选择中药"
clearable
@input="(val) => handleSearchKeyChange(val, item)"
@click="() => handleClick(item)"
@keydown="(e) => handleKeyDown(e, item)"
@blur="() => handleBlur(item)"
/>
</template>
</el-popover>
</el-form-item>
</el-form>
</div>
<div class="drug-bottom">
<div class="drug-bottom-left">
<div class="quantity-wrap">
<el-input v-model="item.quantity" />
<el-input value="g" style="width: 80px" disabled />
</div>
</div>
</div>
<div class="drug-edit-actions">
<el-button type="primary" link @click.stop="saveDrug(item)"> 保存 </el-button>
<el-button type="danger" link @click.stop="removeDrug(index)"> 删除 </el-button>
</div>
</div>
</template>
</el-card>
<!-- 始终存在的新增卡片 -->
<el-card class="drug-card add-card" shadow="never" @click="addDrug">
<div class="add-card-content">
<el-icon class="add-icon">
<Plus />
</el-icon>
<span>新增</span>
</div>
</el-card>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup name="TcmMedicalOrderSet">
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { ElMessage } from 'element-plus';
import {
getPersonalList,
getDeptList,
getAllList,
queryGroupDetail,
queryParticipantList,
getOrgTree,
savePersonal,
saveDepartment,
saveAll,
} from './components/api';
import TcmMedicineList from './components/tcmMedicineList.vue';
import { Edit, Delete, Plus } from '@element-plus/icons-vue';
const { proxy } = getCurrentInstance();
// 字典数据
const { method_code, rate_code } = proxy.useDict('method_code', 'rate_code');
// 左侧树形结构数据
const treeData = ref([]);
const treeProps = {
children: 'children',
label: 'label',
value: 'value',
};
// 加载左侧树数据
function loadTreeData() {
Promise.all([
getPersonalList({ tcmFlag: 1 }),
getDeptList({ tcmFlag: 1 }),
getAllList({ tcmFlag: 1 }),
])
.then(([personalRes, deptRes, allRes]) => {
const personalList = personalRes?.data || [];
const deptList = deptRes?.data || [];
const allList = allRes?.data || [];
treeData.value = [
{
id: 'personal',
label: '个人',
children: personalList.map((item) => ({
id: item.groupPackageId,
value: item.groupPackageId,
label: item.name,
practitionerId: item.practitionerId || '',
organizationId: '',
})),
},
{
id: 'department',
label: '科室',
children: deptList.map((item) => ({
id: item.groupPackageId,
value: item.groupPackageId,
label: item.name,
practitionerId: '',
organizationId: item.organizationId || '',
})),
},
{
id: 'hospital',
label: '全院',
children: allList.map((item) => ({
id: item.groupPackageId,
value: item.groupPackageId,
label: item.name,
practitionerId: '',
organizationId: '',
})),
},
];
})
.catch(() => {
ElMessage.error('加载组套树失败');
});
}
onMounted(() => {
loadTreeData();
loadParticipantList();
loadOrganization();
});
// 使用人列表
const participantListOptions = ref([]);
// 科室树形数据
const organization = ref([]);
// 加载使用人列表
function loadParticipantList() {
queryParticipantList().then((res) => {
participantListOptions.value = res.data || [];
});
}
// 加载科室树
function loadOrganization() {
getOrgTree().then((res) => {
organization.value = res.data.records || [];
});
}
// 使用范围变更
function handleRangeChange() {
commonForm.practitionerId = '';
commonForm.organizationId = '';
}
// 上方公共字段表单
const commonForm = reactive({
name: '',
useRange: '',
practitionerId: '',
organizationId: '',
method: '',
frequency: '',
});
// 下方药品卡片列表
const drugList = ref([]);
const nextDrugId = ref(1);
const currentSetName = ref('');
// 动态设置表格组件引用
function setMedicineTableRef(el, item) {
if (el) {
item.tableRef = el;
}
}
function createEmptyDrug() {
return {
id: nextDrugId.value++,
drugCode: '',
drugName: '',
orderDefinitionId: '',
quantity: 1,
unitCode: '',
unit: 'g',
isEditing: true,
showPopover: false,
searchKey: '',
tableRef: null,
rateCode: '',
methodCode: '',
};
}
// 点击树节点
function handleTreeNodeClick(data, node) {
if (data.children != undefined) {
return;
}
// 从父节点获取使用范围personal/department/hospital
const useRange = node.parent?.data?.id || '';
// 组套名称从树节点获取
commonForm.name = data.label || '';
commonForm.useRange = useRange;
// 从树节点数据中获取使用人/科室
commonForm.practitionerId = data.practitionerId || '';
commonForm.organizationId = data.organizationId || '';
// 调用查询接口回显数据
queryGroupDetail({ groupPackageId: data.value })
.then((res) => {
const detailList = res.data || [];
if (detailList.length === 0) {
drugList.value = [createEmptyDrug()];
return;
}
// 取第一个药品的频次和用法赋值到表单
const firstDrug = detailList[0];
commonForm.method = firstDrug.methodCode || '';
commonForm.frequency = firstDrug.rateCode || '';
// 回显药品列表
drugList.value = detailList.map((detail, index) => ({
id: index,
drugCode: '',
drugName: detail.orderDefinitionName || '',
orderDefinitionId: detail.orderDefinitionId,
quantity: detail.quantity,
unitCode: detail.unitCode,
unit: detail.unitCode_dictText || 'g',
isEditing: false,
showPopover: false,
searchKey: '',
tableRef: null,
rateCode: detail.rateCode,
methodCode: detail.methodCode,
}));
currentSetName.value = data.label;
})
.catch(() => {
ElMessage.error('加载组套详情失败');
});
}
// 新增药品
function addDrug() {
// 如果有药品列表,检查上一个药品是否未保存
if (drugList.value.length > 0) {
const last = drugList.value[drugList.value.length - 1];
// 如果上一个药品未选择药品,不允许添加下一个
if (!last.orderDefinitionId) {
ElMessage.warning('请先选择上一个药品');
return;
}
// 如果上一个药品已有药品ID且处于编辑状态自动保存
if (last.isEditing) {
const msg = validateDrug(last);
if (!msg) {
last.isEditing = false;
}
}
}
drugList.value.push(createEmptyDrug());
}
// 删除药品
function removeDrug(index) {
if (drugList.value.length <= 1) {
ElMessage.warning('至少保留一条药品信息');
return;
}
drugList.value.splice(index, 1);
}
function validateDrug(item) {
if (!item.orderDefinitionId) return '请选择中药';
if (!item.quantity || Number(item.quantity) <= 0) return '请填写数量';
return '';
}
function saveDrug(item) {
const msg = validateDrug(item);
if (msg) {
ElMessage.warning(msg);
return false;
}
item.isEditing = false;
return true;
}
function editDrug(item) {
item.isEditing = true;
}
// 处理搜索关键字变化
function handleSearchKeyChange(value, item) {
item.searchKey = value;
}
// 处理点击输入框
function handleClick(item) {
item.showPopover = true;
}
// 处理失焦
function handleBlur(item) {
item.showPopover = false;
item.searchKey = '';
}
// 处理键盘事件(上下键和回车)
function handleKeyDown(event, item) {
if (!item.showPopover) return;
const key = event.key;
if (['ArrowUp', 'ArrowDown', 'Enter'].includes(key)) {
event.preventDefault();
if (item.tableRef) {
item.tableRef.handleKeyDown(event);
}
}
}
// 选择中药时,回填信息
function handleSelectMedicine(row, item) {
item.drugCode = row.adviceCode;
item.drugName = row.adviceName;
item.orderDefinitionId = row.adviceDefinitionId;
item.unitCode = row.unitCode;
item.unit = row.unitCode_dictText || 'g';
item.showPopover = false;
item.searchKey = '';
}
// 清空表单
function clearForm() {
commonForm.name = '';
commonForm.useRange = '';
commonForm.practitionerId = '';
commonForm.organizationId = '';
commonForm.method = '';
commonForm.frequency = '';
drugList.value = [];
}
// 新建组套
function createNew() {
clearForm();
commonForm.useRange = 'personal';
drugList.value = [createEmptyDrug()];
}
// 保存组套
function saveCommonForm() {
// 校验组套名称
if (!commonForm.name) {
ElMessage.warning('请填写组套名称');
return;
}
// 校验使用范围
if (!commonForm.useRange) {
ElMessage.warning('请选择使用范围');
return;
}
// 校验个人时选择使用人
if (commonForm.useRange === 'personal' && !commonForm.practitionerId) {
ElMessage.warning('请选择使用人');
return;
}
// 校验科室时选择科室
if (commonForm.useRange === 'department' && !commonForm.organizationId) {
ElMessage.warning('请选择科室');
return;
}
// 校验药品列表
if (drugList.value.length === 0) {
ElMessage.warning('请添加药品');
return;
}
// 校验每个药品
for (let i = 0; i < drugList.value.length; i++) {
const item = drugList.value[i];
if (!item.orderDefinitionId) {
ElMessage.warning(`请选择第${i + 1}个药品`);
return;
}
if (!item.quantity || Number(item.quantity) <= 0) {
ElMessage.warning(`请填写第${i + 1}个药品的数量`);
return;
}
}
// 生成组套ID时间戳
const groupId = Date.now();
// 构建详情列表
const detailList = drugList.value.map((item) => ({
orderDefinitionId: item.orderDefinitionId,
orderDefinitionTable: 'order_definition',
quantity: item.quantity,
unitCode: item.unitCode,
dose: item.quantity,
rateCode: commonForm.frequency,
dispensePerDuration: 7,
methodCode: commonForm.method,
doseQuantity: item.quantity,
groupId: groupId,
}));
// 构建请求数据
const data = {
name: commonForm.name,
tcmFlag: 1,
detailList: detailList,
};
// 根据使用范围添加不同字段
if (commonForm.useRange === 'personal') {
data.practitionerId = commonForm.practitionerId;
} else if (commonForm.useRange === 'department') {
data.organizationId = commonForm.organizationId;
}
// 全院不传 practitionerId 和 organizationId
// 调用对应接口
let saveApi;
if (commonForm.useRange === 'personal') {
saveApi = savePersonal;
} else if (commonForm.useRange === 'department') {
saveApi = saveDepartment;
} else {
saveApi = saveAll;
}
saveApi(data)
.then(() => {
ElMessage.success('保存成功');
loadTreeData();
})
.catch((err) => {
ElMessage.error('保存失败:' + (err.message || '未知错误'));
});
}
</script>
<style scoped>
.tcm-medical-order-set {
height: 90vh;
}
.full-height {
height: 100%;
}
.left-panel,
.right-panel {
height: 100%;
}
.tree-card {
height: 100%;
}
.right-panel {
display: flex;
flex-direction: column;
gap: 10px;
}
.form-card,
.detail-card {
width: 100%;
}
.panel-shadow {
border: 1px solid var(--el-border-color);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.1);
}
.common-form {
display: flex;
flex-wrap: wrap;
gap: 10px 14px;
}
.common-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
.common-item {
width: 300px;
margin-bottom: 0;
}
.common-item :deep(.el-input__wrapper),
.common-item :deep(.el-select__wrapper) {
height: 40px;
}
.common-item :deep(.el-input__inner) {
height: 40px;
}
.drug-card-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.drug-card {
width: 220px;
border: 1px solid var(--el-border-color);
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.12);
height: 120px;
}
.drug-card :deep(.el-card__body) {
padding: 10px 10px 8px;
}
.drug-card-body {
display: flex;
flex-direction: column;
height: 120px;
}
.drug-top {
flex: 0 0 auto;
}
.drug-divider {
margin: 6px 0;
border-top: 1px dashed var(--el-border-color);
}
.drug-bottom {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.drug-bottom-left {
flex: 1 1 auto;
min-width: 0;
}
.drug-card-form {
margin-top: 0;
}
.compact-item {
margin-bottom: 8px;
}
.compact-item :deep(.el-form-item__label) {
padding: 0 0 4px;
line-height: 1.2;
}
.compact-item :deep(.el-form-item__content) {
line-height: 1.2;
}
.quantity-wrap {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.unit-select {
width: 80px;
}
.drug-select {
flex: 1;
min-width: 0;
}
.drug-edit-actions {
display: flex;
justify-content: flex-end;
gap: 0px;
padding-top: 2px;
flex-shrink: 0;
}
.drug-view-name {
font-weight: 600;
color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.drug-view-qty {
font-size: 13px;
color: var(--el-text-color-regular);
flex: none;
}
.drug-view-actions {
display: flex;
justify-content: flex-end;
gap: 6px;
flex-shrink: 0;
}
.drug-view-actions :deep(.el-button),
.drug-edit-actions :deep(.el-button) {
padding: 4px;
min-width: 0;
}
.add-card {
border-style: dashed;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-color-primary);
box-shadow: 0 3px 14px rgba(64, 158, 255, 0.18);
}
.add-card-content {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.add-icon {
font-size: 18px;
}
.empty-tip {
margin-top: 80px;
}
.demo-form-inline .el-input {
--el-input-width: 200px;
}
.demo-form-inline .el-select {
--el-select-width: 200px;
}
.el-form-item--default {
margin-bottom: 10px;
}
</style>