2026-08-07 11:11:58 +08:00

456 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<el-drawer v-loading="loading" :close-on-click-modal="false" :model-value="visible"
:title="data._id ? '编辑药品' : '新增药品'" size="70%" style="--el-drawer-padding-primary: 0" @close="close">
<my-layout>
<layout-main>
<el-form :label-width="140" :model="taskForm" label-suffix="" class="p-15px pr-30px"
:class="loading ? 'pointer-events-none' : ''">
<div active-title-bar class="mb-2.5">药品基本信息</div>
<el-row>
<el-col :span="12" v-for="item in basicInfoList" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop" :class="requiredMap[item.prop] ? 'is-required' : ''">
<el-select v-if="item.options" filterable clearable :model-value="form[item.prop]"
:placeholder="`请选择${item.label}`" class="w-full" @update:model-value="changeValue($event, item.prop)">
<el-option v-for="option in item.options" :key="option" :label="option" :value="option" />
</el-select>
<el-input v-else clearable :model-value="form[item.prop]" :maxlength="item.maxlength"
:placeholder="`请输入${item.label}`" @update:model-value="changeValue($event, item.prop)" />
</el-form-item>
</el-col>
</el-row>
<div active-title-bar class="mb-2.5">药品标准用法用量</div>
<el-row>
<el-col :span="12" v-for="item in usageInfo" :key="item.prop">
<el-form-item :label="item.label" :prop="item.prop" :class="requiredMap[item.prop] ? 'is-required' : ''">
<el-select v-if="item.options" filterable clearable :model-value="form[item.prop]"
:placeholder="`请选择${item.label}`" class="w-full" @update:model-value="changeValue($event, item.prop)">
<el-option v-for="option in item.options" :key="option" :label="option" :value="option" />
</el-select>
<el-input v-else clearable :model-value="form[item.prop]" :maxlength="item.maxlength"
:placeholder="`请输入${item.label}`" @update:model-value="changeValue($event, item.prop)" />
</el-form-item>
</el-col>
</el-row>
<div active-title-bar class="mb-2.5">药品规则配置</div>
<el-row>
<el-col :span="24">
<el-form-item label="适应诊断" prop="applicableDiagnosisIds">
<div class="w-full">
<el-popover v-model:visible="diagnosisPopoverVisible" placement="bottom-start" trigger="click"
:width="600">
<template #reference>
<el-input v-model="diagnosisKeyword" clearable placeholder="请输入诊断名称、编码或拼音码搜索"
@input="searchDiagnoses" />
</template>
<div v-loading="diagnosisLoading" class="diagnosis-dropdown">
<div v-for="item in diagnosisOptions" :key="item._id" class="diagnosis-option"
:class="{ 'diagnosis-option--selected': isDiagnosisSelected(item._id) }"
@click="toggleDiagnosis(item)">
<span class="diagnosis-option-label">{{ diagnosisLabel(item) }}</span>
<span v-if="isDiagnosisSelected(item._id)" class="diagnosis-option-check"></span>
</div>
<el-empty v-if="!diagnosisLoading && !diagnosisOptions.length" :image-size="60"
description="暂无诊断" />
</div>
</el-popover>
<div v-if="selectedDiagnosisOptions.length" class="selected-diagnoses">
<el-tag v-for="item in selectedDiagnosisOptions" :key="item._id" closable
@close="toggleDiagnosis(item)">
{{ diagnosisLabel(item) }}
</el-tag>
</div>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</layout-main>
<layout-item>
<div common-shadow--r class="text-center py-10px">
<el-button class="w-100px" @click="close()">取消</el-button>
<el-button class="w-100px" :loading="loading" type="primary" @click="save()">保存</el-button>
</div>
</layout-item>
</my-layout>
</el-drawer>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { ElMessage } from "element-plus";
import { addHlwDrugInfo, editHlwDrugInfo, addOnlineDrugInfo, editOnlineDrugInfo, getOnlineDrugRepeatTags, getHlwDiagnosisList } from "@/api/hlw";
import categoryList from '@/baseData/drug-category';
import dosageUnitList from '@/baseData/dosageUnit.js';
import medicineInfoStore from '@/store/medicine-info';
import { getDrugFieldDict, getDrugList } from './drug-dict.js'
import MyLayout, { LayoutMain, LayoutItem } from "@/components/layout";
const emits = defineEmits(['close', 'change'])
const props = defineProps({
visible: {
type: Boolean,
default: false
},
// 库类型hlw = 互联网医院药品库(drug-info)online = 配送/线上药品库(online-drug-info)
libType: {
type: String,
default: 'hlw'
},
memberInfo: { type: Object, default: () => ({}) },
data: {
type: Object,
default: () => ({})
}
})
const medicineStore = medicineInfoStore()
const { attachMedicineConfig } = medicineStore
const { onlineMedicine, storeMedicine } = storeToRefs(medicineStore)
const medicineUseInfo = computed(() => props.libType === 'online' ? onlineMedicine.value : storeMedicine.value)
const getOptionNames = list => [...new Set(list.map(item => typeof item === 'string' ? item : item?.name).filter(Boolean))]
const unitOptions = computed(() => getOptionNames(medicineUseInfo.value.unitList))
const dosageFormList = computed(() => getOptionNames(medicineUseInfo.value.dosageFormList ||[]))
const frequencyOptions = computed(() => getOptionNames(medicineUseInfo.value.frequencyList))
const usageOptions = computed(() => getOptionNames(medicineUseInfo.value.usageList))
const repeatTagList = ref([])
const diagnosisOptions = ref([])
const diagnosisDefaultOptions = ref([])
const diagnosisLoading = ref(false)
const diagnosisKeyword = ref('')
const diagnosisPopoverVisible = ref(false)
let diagnosisRequestId = 0
let diagnosisSearchTimer = null
const basicInfo = computed(() => [
{ label: '药品名称', prop: "name", maxlength: 100 },
{ label: '药品规格', prop: "specification", maxlength: 100 },
{ label: '包装单位', prop: "unit", options: unitOptions.value },
{ label: '剂型', prop: "dosage_form", options: dosageFormList.value },
{ label: '包装量', prop: "package_amount", maxlength: 100 },
{ label: '生产厂家', prop: "manufacturer", maxlength: 100 },
{ label: '商品条形码', prop: "barcode", maxlength: 100 },
{ label: '医保编码', prop: "insurance_code", maxlength: 100 },
{ label: '药品分类', prop: "category", options: categoryList },
{ label: '药品拼音码', prop: "pinyin_code", maxlength: 100 },
{ label: '通用名', prop: "generic_name", maxlength: 100 },
{ label: '通用名拼音码', prop: "generic_pinyin", maxlength: 100 },
])
// 在线药品库扩展字段:管理分类 + 剂型
const onlineBasicInfoExtra = [
{ label: '管理分类', prop: "manage_category", options: ['处方药', 'OTC'] },
{ label: '剂型', prop: "dosage_form", maxlength: 100 },
{ label: 'ERP编码', prop: "his_drug_code", maxlength: 100 },
]
// 根据 libType 决定展示哪些基础信息字段
const basicInfoList = computed(() => {
const idField = props.libType === 'online'
? { label: '药库货品ID', prop: "product_id", maxlength: 100 }
: { label: 'ERP ID', prop: "erpId", maxlength: 100 };
const fields = [...basicInfo.value.slice(0, 9), idField, ...basicInfo.value.slice(9)];
return props.libType === 'online' ? [...fields, ...onlineBasicInfoExtra] : fields;
})
const usageInfo = computed(() => [
{ label: '单次标准剂量', prop: "dosage", maxlength: 100 },
{ label: '剂量单位', prop: "dosage_unit", options: dosageUnitList },
{ label: '给药标准频次', prop: "frequency", options: frequencyOptions.value },
{ label: '给药标准方式', prop: "administration_method", options: usageOptions.value },
{ label: '用药标准天数', prop: "days", maxlength: 100 },
{ label: '发药标准数量', prop: "recommended_quantity", maxlength: 100 },
])
const suitableInfo = [
{ label: '适用性别', prop: 'suitableGender', options: [{ label: '通用', value: 'common' }, { label: '男', value: 'male' }, { label: '女', value: 'female' }] },
{ label: '是否儿童用药', prop: 'suitableChild', options: [{ label: '是', value: true }, { label: '否', value: false }] },
// { label: '重复用药标识', prop: 'repeatTag', multiple: true, options: new Array(5).fill(1).map((_, index) => ({ label: index + 1, value: index + 1 })) },
]
const requiredMap = computed(() => {
const m = getDrugFieldDict(props.libType).reduce((acc, cur) => {
acc[cur.field] = typeof cur.required === 'boolean' ? cur.required : false
return acc
}, {})
m.medication_proof_desc = Boolean(m.medication_proof_desc);
m.usage_restriction_desc = Boolean(m.usage_restriction_desc);
if (props.libType === 'online') {
m.his_drug_code = true
}
return m
})
const editForm = ref({})
const form = computed(() => ({ ...props.data, ...editForm.value }))
const tableRef = ref(null);
const loading = ref(false)
function changeValue(val, prop) {
editForm.value[prop] = val
}
function diagnosisLabel(item) {
return item.code ? `${item.name}${item.code}` : item.name;
}
const selectedDiagnosisOptions = computed(() => {
const optionMap = new Map(diagnosisOptions.value.map(item => [String(item._id), item]));
const defaultOptionMap = new Map(diagnosisDefaultOptions.value.map(item => [String(item._id), item]));
return (form.value.applicableDiagnosisIds || [])
.map(id => optionMap.get(String(id)) || defaultOptionMap.get(String(id)))
.filter(Boolean);
})
function isDiagnosisSelected(id) {
return (form.value.applicableDiagnosisIds || []).some(item => String(item) === String(id));
}
function toggleDiagnosis(item) {
const id = String(item._id);
const selectedIds = (form.value.applicableDiagnosisIds || []).map(itemId => String(itemId));
const nextIds = selectedIds.includes(id)
? selectedIds.filter(itemId => itemId !== id)
: [...selectedIds, id];
changeValue(nextIds, 'applicableDiagnosisIds');
}
function mergeDiagnosisOptions(items = []) {
const optionMap = new Map(diagnosisOptions.value.map(item => [String(item._id), item]));
items.forEach(item => optionMap.set(String(item._id), item));
diagnosisOptions.value = [...optionMap.values()];
}
function replaceDiagnosisOptions(items = []) {
const selectedIds = new Set((form.value.applicableDiagnosisIds || []).map(id => String(id)));
const selectedOptions = diagnosisOptions.value.filter(item => selectedIds.has(String(item._id)));
const optionMap = new Map(selectedOptions.map(item => [String(item._id), item]));
items.forEach(item => optionMap.set(String(item._id), item));
diagnosisOptions.value = [...optionMap.values()];
}
async function fetchDiagnoses(params, requestId, replace = false) {
const res = await getHlwDiagnosisList({ page: 1, pageSize: 30, ...params });
if (requestId !== diagnosisRequestId) return;
if (!res.success) {
ElMessage.error(res.message || '适应诊断加载失败');
return;
}
const list = res.data && Array.isArray(res.data.data) ? res.data.data : [];
if (replace) {
replaceDiagnosisOptions(list);
} else {
mergeDiagnosisOptions(list);
}
}
function searchDiagnoses(keyword = '') {
if (diagnosisSearchTimer) {
clearTimeout(diagnosisSearchTimer);
diagnosisSearchTimer = null;
}
const normalizedKeyword = typeof keyword === 'string' ? keyword.trim() : '';
const requestId = ++diagnosisRequestId;
if (!normalizedKeyword) {
diagnosisLoading.value = false;
replaceDiagnosisOptions(diagnosisDefaultOptions.value);
return;
}
diagnosisLoading.value = true;
diagnosisSearchTimer = setTimeout(async () => {
await fetchDiagnoses({ keyword: normalizedKeyword }, requestId, true);
if (requestId === diagnosisRequestId) diagnosisLoading.value = false;
diagnosisSearchTimer = null;
}, 300);
}
async function initializeDiagnoses() {
diagnosisOptions.value = [];
const selectedIds = Array.isArray(props.data.applicableDiagnosisIds)
? props.data.applicableDiagnosisIds.map(id => String(id))
: [];
const requestId = ++diagnosisRequestId;
diagnosisLoading.value = true;
const requests = [fetchDiagnoses({}, requestId)];
if (selectedIds.length) {
requests.push(fetchDiagnoses({ ids: selectedIds, pageSize: Math.max(30, selectedIds.length) }, requestId));
}
await Promise.all(requests);
diagnosisDefaultOptions.value = [...diagnosisOptions.value];
if (requestId === diagnosisRequestId) diagnosisLoading.value = false;
}
function close() {
if (loading.value) return;
emits('close')
}
async function save() {
if (loading.value) return;
const isEdit = Boolean(props.data._id);
const verifyData = isEdit ? form.value : editForm.value;
const [drug] = getDrugList([verifyData], 'field', true, props.libType, {
unit: unitOptions.value,
frequency: frequencyOptions.value,
administration_method: usageOptions.value,
});
if (drug.msgs.length) {
ElMessage.info(drug.msgs[0])
return
}
if (Object.keys(editForm.value).length) {
loading.value = true;
const isOnline = props.libType === 'online';
const apiMap = {
update: isOnline ? editOnlineDrugInfo : editHlwDrugInfo,
add: isOnline ? addOnlineDrugInfo : addHlwDrugInfo,
}
const api = props.data._id ? apiMap.update : apiMap.add;
const payload = {
params: { ...editForm.value },
id: props.data._id,
operator: props.memberInfo.anotherName,
operatorId: props.memberInfo._id,
}
if (!props.data._id) {
payload.params.onSale = true;
}
const { success, message } = await api(payload);
loading.value = false;
if (success) {
ElMessage.success(message)
emits('change')
close();
} else {
ElMessage.error(message)
return
}
}
}
async function getRepeatTags() {
const res = await getOnlineDrugRepeatTags()
repeatTagList.value = res && res.data && Array.isArray(res.data.data) ? res.data.data.map(i => ({ label: `${i}`, value: i })) : [];
}
watch(() => props.visible, async (val) => {
if (val) {
// reset local editForm when opening drawer
editForm.value = {}
diagnosisKeyword.value = ''
await Promise.all([attachMedicineConfig(), initializeDiagnoses()])
if (props.libType === 'online') {
getRepeatTags()
}
// initialize select values based on props.data for edit scenario
if (props.data) {
if (props.data.medication_proof_desc) {
editForm.value.medication_proof_desc = props.data.medication_proof_desc
editForm.value.need_medication_proof = true
} else {
editForm.value.need_medication_proof = false
}
if (props.data.usage_restriction_desc) {
editForm.value.usage_restriction_desc = props.data.usage_restriction_desc
editForm.value.has_usage_restriction = true
} else {
editForm.value.has_usage_restriction = false
}
if (props.data.repeatTag && !Array.isArray(props.data.repeatTag)) {
editForm.value.repeatTag = [props.data.repeatTag]
}
}
} else {
diagnosisPopoverVisible.value = false
diagnosisRequestId += 1
diagnosisLoading.value = false
if (diagnosisSearchTimer) {
clearTimeout(diagnosisSearchTimer)
diagnosisSearchTimer = null
}
}
})
function selectChange(prop, val) {
// store select value and clear description when selecting '否' (false)
changeValue(val, prop)
if (prop === 'need_medication_proof' && !val) {
changeValue('', 'medication_proof_desc')
}
if (prop === 'has_usage_restriction' && !val) {
changeValue('', 'usage_restriction_desc')
}
}
</script>
<style scoped>
.other-info-row {
display: flex;
align-items: center;
/* vertically center select and input */
gap: 12px;
width: 100%;
}
.other-info-select {
flex: 0 0 140px;
/* slightly wider to accommodate labels */
}
.other-info-desc {
flex: 1 1 0;
/* take remaining space */
min-width: 0;
/* allow input to shrink properly inside flex */
}
.diagnosis-dropdown {
min-height: 60px;
max-height: 320px;
overflow-y: auto;
}
.diagnosis-option {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 36px;
padding: 0 12px;
border-radius: 4px;
cursor: pointer;
color: var(--el-text-color-regular);
}
.diagnosis-option:hover,
.diagnosis-option--selected {
background: var(--el-fill-color-light);
}
.diagnosis-option--selected {
color: var(--el-color-primary);
}
.diagnosis-option-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.diagnosis-option-check {
flex-shrink: 0;
margin-left: 16px;
font-size: 18px;
font-weight: 600;
color: var(--el-color-primary);
}
.selected-diagnoses {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
</style>