feat:
1. 金方
This commit is contained in:
@@ -9,6 +9,11 @@ export interface UsePasteUploadListenOptions {
|
||||
maxSize?: number | (() => number);
|
||||
/** 过滤后的文件回调 */
|
||||
onFiles: (files: File[]) => void | Promise<void>;
|
||||
/**
|
||||
* 粘贴成功拿到合法文件后自动停止监听
|
||||
* 为什么默认 false:发票等组件仍要手动点「停止」;金方 Excel 导入需要成功即停
|
||||
*/
|
||||
autoStopOnSuccess?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,6 +141,11 @@ export function usePasteUploadListen(options: UsePasteUploadListenOptions) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// 成功拿到文件后先停监听(按需),再回调业务,避免重复粘贴
|
||||
if (options.autoStopOnSuccess) {
|
||||
stop();
|
||||
message.success('已粘贴文件,监听已结束');
|
||||
}
|
||||
void options.onFiles(filtered);
|
||||
};
|
||||
window.addEventListener('paste', handler);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 中药新增/编辑弹窗
|
||||
* 支持通过 modalApi.setData({ onSuccess }) 在保存成功后回调(金方缺失药「添加药材」复用)
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
@@ -10,10 +13,7 @@ import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createChinaMedicine, updateChinaMedicine } from '../api';
|
||||
import { modalFormProps } from '../config/form';
|
||||
import {getDrugUseList} from "#/views/doctor/doctor-reception/api";
|
||||
|
||||
|
||||
const userStore = useUserStore();
|
||||
import { getDrugUseList } from '#/views/doctor/doctor-reception/api';
|
||||
|
||||
const drugTime = ref([]);
|
||||
const drugType = ref([]);
|
||||
@@ -49,10 +49,11 @@ getDrugUseList().then((res) => {
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const gridApi = ref();
|
||||
/** 外部传入的成功回调(如金方缺失药处理) */
|
||||
const onSuccessCb = ref<null | (() => void | Promise<void>)>(null);
|
||||
|
||||
const [Form, formApi] = useVbenForm(modalFormProps);
|
||||
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
@@ -60,62 +61,70 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
formApi.validate().then(async (e: any) => {
|
||||
if (e.valid) {
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
const submitApi = isUpdate.value
|
||||
? updateChinaMedicine
|
||||
: createChinaMedicine;
|
||||
submitApi(values)
|
||||
.then(() => {
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload();
|
||||
modalApi.close();
|
||||
})
|
||||
.finally(() => {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
});
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
modalApi.setState({ loading: true, confirmLoading: true });
|
||||
try {
|
||||
const submitApi = isUpdate.value
|
||||
? updateChinaMedicine
|
||||
: createChinaMedicine;
|
||||
await submitApi(values);
|
||||
message.success('保存成功');
|
||||
gridApi.value?.reload?.();
|
||||
gridApi.value?.query?.();
|
||||
// 外部回调:金方缺失药「添加药材」成功后标记已解决
|
||||
if (typeof onSuccessCb.value === 'function') {
|
||||
await onSuccessCb.value();
|
||||
}
|
||||
});
|
||||
await formApi.validateAndSubmitForm();
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.setState({ loading: false, confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (isOpen) {
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
options: drugTime.value,
|
||||
},
|
||||
fieldName: 'time_id',
|
||||
if (!isOpen) {
|
||||
onSuccessCb.value = null;
|
||||
formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<Record<string, any>>() || {};
|
||||
gridApi.value = data.gridApi || null;
|
||||
onSuccessCb.value =
|
||||
typeof data.onSuccess === 'function' ? data.onSuccess : null;
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
options: drugTime.value,
|
||||
},
|
||||
{
|
||||
componentProps: {
|
||||
options: drugUnit.value,
|
||||
},
|
||||
fieldName: 'unit_id',
|
||||
fieldName: 'time_id',
|
||||
},
|
||||
{
|
||||
componentProps: {
|
||||
options: drugUnit.value,
|
||||
},
|
||||
{
|
||||
componentProps: {
|
||||
options: drugType.value,
|
||||
},
|
||||
fieldName: 'type_id',
|
||||
fieldName: 'unit_id',
|
||||
},
|
||||
{
|
||||
componentProps: {
|
||||
options: drugType.value,
|
||||
},
|
||||
{
|
||||
componentProps: {
|
||||
options: drugFrequency.value,
|
||||
},
|
||||
fieldName: 'frequency_id',
|
||||
fieldName: 'type_id',
|
||||
},
|
||||
{
|
||||
componentProps: {
|
||||
options: drugFrequency.value,
|
||||
},
|
||||
]);
|
||||
const { values, update } = modalApi.getData<Record<string, any>>();
|
||||
if (values) {
|
||||
console.log(values, 'sssssssssssss')
|
||||
isUpdate.value = update;
|
||||
formApi.setValues(values);
|
||||
}
|
||||
fieldName: 'frequency_id',
|
||||
},
|
||||
]);
|
||||
const { values, update } = data;
|
||||
isUpdate.value = !!update;
|
||||
if (values) {
|
||||
formApi.setValues(values);
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
formApi.setValues({ status: 1 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,9 @@ const prefix = 'golden-formula/';
|
||||
/** 金方来源字典 API */
|
||||
const sourcePrefix = 'golden-formula-source/';
|
||||
|
||||
/** 金方缺失药品 API */
|
||||
const missingPrefix = 'golden-formula-missing-drug/';
|
||||
|
||||
export async function getGoldenFormulaList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
@@ -45,6 +48,32 @@ export async function checkGoldenFormulaDrugs(data: { drug_names: string[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 药方设置详情:组成 + 本方缺失药名
|
||||
* xk-api GET /golden-formula/drugs-detail
|
||||
*/
|
||||
export async function getGoldenFormulaDrugsDetail(id: number) {
|
||||
return requestClient.get<{
|
||||
id: number;
|
||||
name: string;
|
||||
drugs_json: any[];
|
||||
herb_overview: string;
|
||||
is_abnormal: number;
|
||||
missing: string[];
|
||||
}>(`${prefix}drugs-detail`, { params: { id } });
|
||||
}
|
||||
|
||||
/**
|
||||
* 药方设置保存:只更新 drugs_json 相关字段
|
||||
* xk-api POST /golden-formula/update-drugs
|
||||
*/
|
||||
export async function updateGoldenFormulaDrugs(data: {
|
||||
id: number;
|
||||
drugs_json: Record<string, any>[];
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}update-drugs`, data);
|
||||
}
|
||||
|
||||
/** 金方来源字典 - 下拉选项 */
|
||||
export async function getGoldenFormulaSourceOption() {
|
||||
return requestClient.get<any[]>(`${sourcePrefix}option`);
|
||||
@@ -55,3 +84,114 @@ export async function createGoldenFormulaSource(data: { name: string }) {
|
||||
return requestClient.post<any>(`${sourcePrefix}create`, data);
|
||||
}
|
||||
|
||||
/** 缺失药品分页列表 */
|
||||
export async function getGoldenFormulaMissingDrugList(data: any) {
|
||||
return requestClient.get<any>(`${missingPrefix}list`, { params: data });
|
||||
}
|
||||
|
||||
/** 批量标记缺失药品为已解决 */
|
||||
export async function resolveGoldenFormulaMissingDrug(data: { ids: number[] }) {
|
||||
return requestClient.post<any>(`${missingPrefix}resolve`, data);
|
||||
}
|
||||
|
||||
/** 重新校验待处理缺失药品(已存在的自动标已解决) */
|
||||
export async function recheckGoldenFormulaMissingDrug() {
|
||||
return requestClient.post<{ checked: number; resolved: number }>(
|
||||
`${missingPrefix}recheck`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/** 软删除缺失药品记录 */
|
||||
export async function deleteGoldenFormulaMissingDrug(data: { ids: number[] }) {
|
||||
return requestClient.post<any>(`${missingPrefix}delete`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺失药名新建为中药饮片
|
||||
* xk-api POST /golden-formula-missing-drug/create-as-drug
|
||||
*/
|
||||
export async function createMissingAsDrug(data: {
|
||||
id?: number;
|
||||
drug_name?: string;
|
||||
}) {
|
||||
return requestClient.post<{
|
||||
drug_id: number;
|
||||
drug_name: string;
|
||||
refreshed: number;
|
||||
}>(`${missingPrefix}create-as-drug`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅标记缺失药已解决(中药管理弹窗新建成功后调用,不再二次建药)
|
||||
* xk-api POST /golden-formula-missing-drug/resolve-by-name
|
||||
*/
|
||||
export async function resolveMissingByName(data: {
|
||||
id?: number;
|
||||
drug_name?: string;
|
||||
remark?: string;
|
||||
}) {
|
||||
return requestClient.post<{
|
||||
drug_name: string;
|
||||
refreshed: number;
|
||||
}>(`${missingPrefix}resolve-by-name`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺失药名挂到目标药材别名表
|
||||
* xk-api POST /golden-formula-missing-drug/bind-as-alias
|
||||
*/
|
||||
export async function bindMissingAsAlias(data: {
|
||||
id?: number;
|
||||
drug_name?: string;
|
||||
target_drug_id: number;
|
||||
}) {
|
||||
return requestClient.post<{
|
||||
drug_id: number;
|
||||
drug_name: string;
|
||||
alias: string;
|
||||
refreshed: number;
|
||||
}>(`${missingPrefix}bind-as-alias`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览含某药名的金方(替换前勾选)
|
||||
* xk-api GET /golden-formula-missing-drug/formulas-by-drug-name
|
||||
*/
|
||||
export async function getFormulasByDrugName(drugName: string) {
|
||||
return requestClient.get<
|
||||
{
|
||||
id: number;
|
||||
name: string;
|
||||
clause_number: string;
|
||||
herb_overview: string;
|
||||
source_name: string;
|
||||
is_abnormal: number;
|
||||
has_unmatched: number;
|
||||
status: number;
|
||||
drug_count: number;
|
||||
}[]
|
||||
>(`${missingPrefix}formulas-by-drug-name`, {
|
||||
params: { drug_name: drugName },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换为药材表已有药材:金方 name 改为目标药名,旧名保留 origin_name 并挂别名
|
||||
* xk-api POST /golden-formula-missing-drug/replace-with-drug
|
||||
*/
|
||||
export async function replaceMissingWithDrug(data: {
|
||||
id?: number;
|
||||
drug_name?: string;
|
||||
target_drug_id: number;
|
||||
/** 预览勾选的金方 ID 列表(必填) */
|
||||
formula_ids: number[];
|
||||
}) {
|
||||
return requestClient.post<{
|
||||
drug_id: number;
|
||||
old_name: string;
|
||||
new_name: string;
|
||||
formula_count: number;
|
||||
refreshed: number;
|
||||
}>(`${missingPrefix}replace-with-drug`, data);
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方「药方设置」弹窗
|
||||
* 独立编辑药方组成(name/dose/unit/usage/ancient_dose/prep),并标红本方缺失药名
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Input, Space, Table, Tag, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getGoldenFormulaDrugsDetail,
|
||||
updateGoldenFormulaDrugs,
|
||||
} from '../api';
|
||||
import ResolveMissingDrugPanel from './ResolveMissingDrugPanel.vue';
|
||||
|
||||
/** 单味药编辑行 */
|
||||
interface DrugRow {
|
||||
key: string;
|
||||
name: string;
|
||||
dose: string;
|
||||
unit: string;
|
||||
usage: string;
|
||||
ancient_dose: string;
|
||||
prep: string;
|
||||
/** 替换已有药时保留的旧药名 */
|
||||
origin_name: string;
|
||||
}
|
||||
|
||||
const formulaId = ref(0);
|
||||
const formulaName = ref('');
|
||||
const drugRows = ref<DrugRow[]>([]);
|
||||
const missingSet = ref<Set<string>>(new Set());
|
||||
const loading = ref(false);
|
||||
const gridApi = ref();
|
||||
let rowKeySeq = 0;
|
||||
|
||||
/** 缺失药处理面板 */
|
||||
const resolveOpen = ref(false);
|
||||
const resolveDrugName = ref('');
|
||||
|
||||
/** 本方缺失药味数(按当前编辑行实时统计) */
|
||||
const missingCount = computed(() => {
|
||||
let n = 0;
|
||||
for (const row of drugRows.value) {
|
||||
const name = String(row.name || '').trim();
|
||||
if (name && missingSet.value.has(name)) n += 1;
|
||||
}
|
||||
return n;
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{ title: '药名', dataIndex: 'name', key: 'name', width: 130 },
|
||||
{ title: '旧名', dataIndex: 'origin_name', key: 'origin_name', width: 100 },
|
||||
{ title: '剂量(克)', dataIndex: 'dose', key: 'dose', width: 90 },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 70 },
|
||||
{ title: '煎法', dataIndex: 'usage', key: 'usage', width: 100 },
|
||||
{ title: '古代剂量', dataIndex: 'ancient_dose', key: 'ancient_dose', width: 100 },
|
||||
{ title: '炮制', dataIndex: 'prep', key: 'prep', width: 90 },
|
||||
{ title: '操作', key: 'action', width: 140, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
class: 'w-[960px]',
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
if (formulaId.value <= 0) {
|
||||
message.warning('金方无效');
|
||||
return;
|
||||
}
|
||||
// 过滤空药名行;至少允许空组成(异常方运营可后补)
|
||||
const drugs_json = drugRows.value
|
||||
.map((r) => {
|
||||
const row: Record<string, string> = {
|
||||
name: String(r.name || '').trim(),
|
||||
dose: String(r.dose || '').trim(),
|
||||
unit: String(r.unit || '').trim() || 'g',
|
||||
usage: String(r.usage || '').trim(),
|
||||
ancient_dose: String(r.ancient_dose || '').trim(),
|
||||
prep: String(r.prep || '').trim(),
|
||||
};
|
||||
const origin = String(r.origin_name || '').trim();
|
||||
if (origin) {
|
||||
row.origin_name = origin;
|
||||
}
|
||||
return row;
|
||||
})
|
||||
.filter((r) => r.name !== '');
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
await updateGoldenFormulaDrugs({
|
||||
id: formulaId.value,
|
||||
drugs_json,
|
||||
});
|
||||
message.success('药方设置已保存');
|
||||
gridApi.value?.query?.();
|
||||
gridApi.value?.reload?.();
|
||||
modalApi.close();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '保存失败');
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formulaId.value = 0;
|
||||
formulaName.value = '';
|
||||
drugRows.value = [];
|
||||
missingSet.value = new Set();
|
||||
gridApi.value = null;
|
||||
resolveOpen.value = false;
|
||||
resolveDrugName.value = '';
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ id?: number; gridApi?: any }>() || {};
|
||||
gridApi.value = data.gridApi || null;
|
||||
const id = Number(data.id || 0);
|
||||
if (id <= 0) {
|
||||
message.error('缺少金方 ID');
|
||||
modalApi.close();
|
||||
return;
|
||||
}
|
||||
formulaId.value = id;
|
||||
await loadDetail(id);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 拉取药方组成详情,并建立缺失药名集合
|
||||
*/
|
||||
async function loadDetail(id: number) {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getGoldenFormulaDrugsDetail(id);
|
||||
formulaName.value = String(res?.name || '');
|
||||
const missing = Array.isArray(res?.missing) ? res.missing : [];
|
||||
missingSet.value = new Set(missing.map((n: string) => String(n).trim()).filter(Boolean));
|
||||
const raw = Array.isArray(res?.drugs_json) ? res.drugs_json : [];
|
||||
// 关联对象含 error 时无法拆成药味行,给空表让运营重填
|
||||
if (raw.length === 0 || (raw as any).error || !Array.isArray(raw)) {
|
||||
const asObj = res?.drugs_json;
|
||||
if (asObj && typeof asObj === 'object' && !Array.isArray(asObj) && (asObj as any).error) {
|
||||
drugRows.value = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
drugRows.value = (Array.isArray(raw) ? raw : []).map((item: any) =>
|
||||
toDrugRow(item),
|
||||
);
|
||||
if (drugRows.value.length === 0) {
|
||||
addRow();
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '加载药方组成失败');
|
||||
drugRows.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 后端一行转为可编辑行 */
|
||||
function toDrugRow(item: any): DrugRow {
|
||||
rowKeySeq += 1;
|
||||
return {
|
||||
key: `d-${rowKeySeq}`,
|
||||
name: String(item?.name || item?.drug_name || ''),
|
||||
dose: String(item?.dose ?? ''),
|
||||
unit: String(item?.unit || 'g'),
|
||||
usage: String(item?.usage || ''),
|
||||
ancient_dose: String(item?.ancient_dose || ''),
|
||||
prep: String(item?.prep || ''),
|
||||
origin_name: String(item?.origin_name || item?.old_name || ''),
|
||||
};
|
||||
}
|
||||
|
||||
/** 新增一味空药 */
|
||||
function addRow() {
|
||||
rowKeySeq += 1;
|
||||
drugRows.value.push({
|
||||
key: `d-${rowKeySeq}`,
|
||||
name: '',
|
||||
dose: '',
|
||||
unit: 'g',
|
||||
usage: '',
|
||||
ancient_dose: '',
|
||||
prep: '',
|
||||
origin_name: '',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除一行 */
|
||||
function removeRow(key: string) {
|
||||
drugRows.value = drugRows.value.filter((r) => r.key !== key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开缺失药处理:添加药材 / 挂别名
|
||||
*/
|
||||
function openResolve(name: string) {
|
||||
const n = String(name || '').trim();
|
||||
if (!n || !missingSet.value.has(n)) {
|
||||
return;
|
||||
}
|
||||
resolveDrugName.value = n;
|
||||
resolveOpen.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完成后重新拉取组成(替换会改 name/origin_name,需整表回填)
|
||||
*/
|
||||
async function onResolveDone() {
|
||||
if (formulaId.value <= 0) return;
|
||||
try {
|
||||
await loadDetail(formulaId.value);
|
||||
gridApi.value?.query?.();
|
||||
} catch {
|
||||
// 刷新失败不阻塞
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="`药方设置${formulaName ? ` · ${formulaName}` : ''}`">
|
||||
<div class="gf-drugs-toolbar">
|
||||
<div class="gf-drugs-summary">
|
||||
<span>共 {{ drugRows.length }} 行</span>
|
||||
<Tag v-if="missingCount > 0" color="error">
|
||||
本方缺失 {{ missingCount }} 味(红字可点「处理」)
|
||||
</Tag>
|
||||
<Tag v-else color="success">本方药名均已匹配</Tag>
|
||||
</div>
|
||||
<Button type="dashed" size="small" @click="addRow">添加药味</Button>
|
||||
</div>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="drugRows"
|
||||
:loading="loading"
|
||||
:pagination="false"
|
||||
row-key="key"
|
||||
size="small"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<Input
|
||||
v-model:value="record.name"
|
||||
:class="{ 'gf-drug-missing': missingSet.has(String(record.name || '').trim()) }"
|
||||
placeholder="药名"
|
||||
allow-clear
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'origin_name'">
|
||||
<span class="gf-origin-name">{{ record.origin_name || '—' }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'dose'">
|
||||
<Input v-model:value="record.dose" placeholder="克数" allow-clear />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'unit'">
|
||||
<Input v-model:value="record.unit" placeholder="g" allow-clear />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'usage'">
|
||||
<Input v-model:value="record.usage" placeholder="先煎/后下" allow-clear />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'ancient_dose'">
|
||||
<Input v-model:value="record.ancient_dose" placeholder="三两" allow-clear />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'prep'">
|
||||
<Input v-model:value="record.prep" placeholder="去皮/炙" allow-clear />
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<Space :size="0">
|
||||
<Button
|
||||
v-if="missingSet.has(String(record.name || '').trim())"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openResolve(record.name)"
|
||||
>
|
||||
处理
|
||||
</Button>
|
||||
<Button type="link" danger size="small" @click="removeRow(record.key)">
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<ResolveMissingDrugPanel
|
||||
v-model:open="resolveOpen"
|
||||
:drug-name="resolveDrugName"
|
||||
:formula-id="formulaId"
|
||||
@done="onResolveDone"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
<style scoped>
|
||||
.gf-drugs-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
gap: 12px;
|
||||
}
|
||||
.gf-drugs-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 13px;
|
||||
}
|
||||
.gf-origin-name {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 12px;
|
||||
}
|
||||
/* 缺失药名输入框:用 destructive 强调,暗色可读 */
|
||||
:deep(.gf-drug-missing) {
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: 600;
|
||||
}
|
||||
:deep(.gf-drug-missing input) {
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,202 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方 Excel 导入选文件弹窗
|
||||
* - 点击 / 拖拽:UploadDragger
|
||||
* - 粘贴:点「开始监听粘贴」后 Ctrl+V,成功拿到文件后自动结束监听
|
||||
* 确认后由父级解析并打开预览弹窗
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { InboxOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Button,
|
||||
UploadDragger,
|
||||
message,
|
||||
type UploadFile,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { usePasteUploadListen } from '#/utils/use-paste-upload-listen';
|
||||
|
||||
const ACCEPT = '.xlsx,.xls';
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
/** 父级传入:选中文件确认后回调(解析 Excel) */
|
||||
const onConfirmFile = ref<null | ((file: File) => void | Promise<void>)>(null);
|
||||
|
||||
const {
|
||||
listening: pasteListening,
|
||||
start: startPasteListen,
|
||||
stop: stopPasteListen,
|
||||
} = usePasteUploadListen({
|
||||
accept: ACCEPT,
|
||||
// Excel 体积放宽到 30MB
|
||||
maxSize: 30 * 1024 * 1024,
|
||||
autoStopOnSuccess: true,
|
||||
onFiles: (files) => {
|
||||
const file = files[0];
|
||||
if (!file) return;
|
||||
applySelectedFile(file);
|
||||
},
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
onCancel() {
|
||||
resetState();
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
if (!selectedFile.value) {
|
||||
message.warning('请先选择、拖入或粘贴 Excel 文件');
|
||||
return;
|
||||
}
|
||||
const file = selectedFile.value;
|
||||
const cb = onConfirmFile.value;
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
if (typeof cb === 'function') {
|
||||
await cb(file);
|
||||
}
|
||||
// 解析成功才关弹窗;失败则保留文件便于重选/重试
|
||||
resetState();
|
||||
modalApi.close();
|
||||
} catch {
|
||||
// 错误提示由父级 handleImportFile 负责
|
||||
} finally {
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
resetState();
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{
|
||||
onConfirmFile?: (file: File) => void | Promise<void>;
|
||||
}>() || {};
|
||||
onConfirmFile.value =
|
||||
typeof data.onConfirmFile === 'function' ? data.onConfirmFile : null;
|
||||
},
|
||||
});
|
||||
|
||||
/** 重置选中与粘贴监听 */
|
||||
function resetState() {
|
||||
selectedFile.value = null;
|
||||
fileList.value = [];
|
||||
onConfirmFile.value = null;
|
||||
stopPasteListen();
|
||||
}
|
||||
|
||||
/** 统一写入选中文件(点击/拖拽/粘贴共用) */
|
||||
function applySelectedFile(file: File) {
|
||||
const name = (file.name || '').toLowerCase();
|
||||
if (!name.endsWith('.xlsx') && !name.endsWith('.xls')) {
|
||||
message.warning('仅支持 .xlsx / .xls 文件');
|
||||
return;
|
||||
}
|
||||
selectedFile.value = file;
|
||||
fileList.value = [
|
||||
{
|
||||
uid: `gf-${Date.now()}`,
|
||||
name: file.name,
|
||||
status: 'done',
|
||||
originFileObj: file as any,
|
||||
},
|
||||
];
|
||||
message.success(`已选择:${file.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dragger 选中变化:before-upload 已拦截实际上传,这里只取本地 File
|
||||
*/
|
||||
function handleChange(info: { file: UploadFile; fileList: UploadFile[] }) {
|
||||
const { file } = info;
|
||||
if (file.status === 'removed') {
|
||||
selectedFile.value = null;
|
||||
fileList.value = [];
|
||||
return;
|
||||
}
|
||||
const raw = (file.originFileObj || file) as File;
|
||||
if (raw instanceof File) {
|
||||
applySelectedFile(raw);
|
||||
}
|
||||
}
|
||||
|
||||
/** 切换粘贴监听:开始时提示;手动停止时也提示 */
|
||||
function handlePasteListenClick() {
|
||||
if (pasteListening.value) {
|
||||
stopPasteListen();
|
||||
message.info('已停止粘贴监听');
|
||||
return;
|
||||
}
|
||||
startPasteListen();
|
||||
message.success('已开启粘贴监听,请 Ctrl+V 粘贴 Excel(成功后自动结束)');
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="导入金方 Excel" class="w-[560px]">
|
||||
<div v-if="selectedFile" class="gf-import-selected">
|
||||
已选择文件:{{ selectedFile.name }}
|
||||
</div>
|
||||
<UploadDragger
|
||||
v-model:file-list="fileList"
|
||||
:before-upload="() => false"
|
||||
:max-count="1"
|
||||
:accept="ACCEPT"
|
||||
name="file"
|
||||
@change="handleChange"
|
||||
>
|
||||
<p class="gf-import-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或拖拽 Excel 到此区域</p>
|
||||
<p class="ant-upload-hint">仅支持 .xlsx / .xls,确认后进入导入预览</p>
|
||||
</UploadDragger>
|
||||
<div class="gf-import-paste">
|
||||
<Button
|
||||
:type="pasteListening ? 'primary' : 'default'"
|
||||
:danger="pasteListening"
|
||||
@click="handlePasteListenClick"
|
||||
>
|
||||
{{ pasteListening ? '停止监听粘贴' : '开始监听粘贴' }}
|
||||
</Button>
|
||||
<span v-if="pasteListening" class="gf-import-paste-tip">
|
||||
监听中:Ctrl+V 粘贴 Excel,成功后自动结束
|
||||
</span>
|
||||
<span v-else class="gf-import-paste-tip">
|
||||
也可点击开始监听后粘贴文件
|
||||
</span>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<style scoped>
|
||||
.gf-import-selected {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid hsl(var(--primary) / 0.35);
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 13px;
|
||||
}
|
||||
.gf-import-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 48px;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
.gf-import-paste {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.gf-import-paste-tip {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,275 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方缺失药品弹窗
|
||||
* 展示导入/药方设置产生的不存在药名;点击药名可选择「添加药材」或「写入别名表」
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Select, Table, Tag, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getGoldenFormulaMissingDrugList,
|
||||
recheckGoldenFormulaMissingDrug,
|
||||
resolveGoldenFormulaMissingDrug,
|
||||
} from '../api';
|
||||
import { MISSING_DRUG_STATUS_OPTIONS } from '../config/constants';
|
||||
import ResolveMissingDrugPanel from './ResolveMissingDrugPanel.vue';
|
||||
|
||||
const loading = ref(false);
|
||||
const rechecking = ref(false);
|
||||
const resolving = ref(false);
|
||||
const items = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(20);
|
||||
const statusFilter = ref<number | undefined>(0);
|
||||
const selectedRowKeys = ref<number[]>([]);
|
||||
|
||||
/** 处理面板状态 */
|
||||
const resolveOpen = ref(false);
|
||||
const resolveMissingId = ref(0);
|
||||
const resolveDrugName = ref('');
|
||||
|
||||
const columns = [
|
||||
{ title: '药名', dataIndex: 'drug_name', key: 'drug_name' },
|
||||
{ title: '出现次数', dataIndex: 'hit_count', key: 'hit_count', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170 },
|
||||
{ title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 170 },
|
||||
];
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
class: 'w-[860px]',
|
||||
showConfirmButton: false,
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
items.value = [];
|
||||
total.value = 0;
|
||||
page.value = 1;
|
||||
selectedRowKeys.value = [];
|
||||
statusFilter.value = 0;
|
||||
resolveOpen.value = false;
|
||||
return;
|
||||
}
|
||||
await loadList();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 拉取缺失药品分页列表
|
||||
*/
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
selectedRowKeys.value = [];
|
||||
try {
|
||||
const params: Record<string, any> = {
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
};
|
||||
if (statusFilter.value !== undefined && statusFilter.value !== null) {
|
||||
params.status = statusFilter.value;
|
||||
}
|
||||
const res = await getGoldenFormulaMissingDrugList(params);
|
||||
items.value = Array.isArray(res?.items) ? res.items : [];
|
||||
total.value = Number(res?.total || 0);
|
||||
} catch (e: any) {
|
||||
items.value = [];
|
||||
total.value = 0;
|
||||
message.error(e?.message || '加载失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 表格分页变更 */
|
||||
function onTableChange(pag: any) {
|
||||
page.value = Number(pag?.current || 1);
|
||||
pageSize.value = Number(pag?.pageSize || 20);
|
||||
loadList();
|
||||
}
|
||||
|
||||
/** 状态筛选变更后回到第一页 */
|
||||
function onStatusChange() {
|
||||
page.value = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新校验:药品库已存在的自动标已解决
|
||||
*/
|
||||
async function handleRecheck() {
|
||||
rechecking.value = true;
|
||||
try {
|
||||
const res = await recheckGoldenFormulaMissingDrug();
|
||||
message.success(
|
||||
`校验 ${Number(res?.checked || 0)} 条,自动解决 ${Number(res?.resolved || 0)} 条`,
|
||||
);
|
||||
await loadList();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '重新校验失败');
|
||||
} finally {
|
||||
rechecking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量标记已解决(当前勾选)
|
||||
*/
|
||||
async function handleResolve() {
|
||||
if (!selectedRowKeys.value.length) {
|
||||
message.warning('请先勾选待处理记录');
|
||||
return;
|
||||
}
|
||||
resolving.value = true;
|
||||
try {
|
||||
const res = await resolveGoldenFormulaMissingDrug({
|
||||
ids: selectedRowKeys.value,
|
||||
});
|
||||
message.success(`已标记 ${Number(res?.resolved || 0)} 条为已解决`);
|
||||
await loadList();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '操作失败');
|
||||
} finally {
|
||||
resolving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 勾选变更 */
|
||||
function onSelectChange(keys: (string | number)[]) {
|
||||
selectedRowKeys.value = keys.map((k) => Number(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击待处理药名:打开「添加药材 / 挂别名」选择面板
|
||||
*/
|
||||
function openResolve(record: any) {
|
||||
if (Number(record?.status) === 1) {
|
||||
message.info('该药名已解决');
|
||||
return;
|
||||
}
|
||||
resolveMissingId.value = Number(record?.id || 0);
|
||||
resolveDrugName.value = String(record?.drug_name || '').trim();
|
||||
if (!resolveDrugName.value) {
|
||||
message.warning('药名为空');
|
||||
return;
|
||||
}
|
||||
resolveOpen.value = true;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="缺失药品">
|
||||
<div class="gf-missing-toolbar">
|
||||
<div class="gf-missing-filters">
|
||||
<span class="gf-missing-label">状态</span>
|
||||
<Select
|
||||
v-model:value="statusFilter"
|
||||
allow-clear
|
||||
placeholder="全部"
|
||||
style="width: 140px"
|
||||
:options="MISSING_DRUG_STATUS_OPTIONS"
|
||||
@change="onStatusChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="gf-missing-actions">
|
||||
<Button :loading="rechecking" @click="handleRecheck">重新校验</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="resolving"
|
||||
:disabled="!selectedRowKeys.length"
|
||||
@click="handleResolve"
|
||||
>
|
||||
标记已解决
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="gf-missing-hint">
|
||||
点击待处理药名,可选择「添加药材」「放入别名表」或「替换为已有药材(保留旧名)」
|
||||
</p>
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="items"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
size="small"
|
||||
bordered
|
||||
:row-selection="{
|
||||
selectedRowKeys,
|
||||
onChange: onSelectChange,
|
||||
}"
|
||||
:pagination="{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t: number) => `共 ${t} 条`,
|
||||
}"
|
||||
@change="onTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'drug_name'">
|
||||
<a
|
||||
v-if="record.status !== 1"
|
||||
class="gf-missing-name"
|
||||
@click.prevent="openResolve(record)"
|
||||
>
|
||||
{{ record.drug_name }}
|
||||
</a>
|
||||
<span v-else>{{ record.drug_name }}</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<Tag v-if="record.status === 1" color="success">已解决</Tag>
|
||||
<Tag v-else color="error">待处理</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<ResolveMissingDrugPanel
|
||||
v-model:open="resolveOpen"
|
||||
:missing-id="resolveMissingId"
|
||||
:drug-name="resolveDrugName"
|
||||
@done="loadList"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
<style scoped>
|
||||
.gf-missing-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
gap: 12px;
|
||||
}
|
||||
.gf-missing-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.gf-missing-label {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 13px;
|
||||
}
|
||||
.gf-missing-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.gf-missing-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.gf-missing-name {
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.gf-missing-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,432 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 缺失药处理面板
|
||||
* - 添加药材:打开中药管理「新增中药」模态框
|
||||
* - 挂别名:ChineseDrugNameBubble 选目标药
|
||||
* - 替换已有:选目标药 → 预览勾选含同名药的金方 → 确认
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
|
||||
import { Button, Modal, Space, Table, Tag, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
ChineseDrugNameBubble,
|
||||
type ChineseDrugBubbleItem,
|
||||
} from '#/components/chinese-drug-name-bubble';
|
||||
import ChinaMedicineFormModal from '#/views/business/product/china-medicine/components/modal.vue';
|
||||
|
||||
import {
|
||||
bindMissingAsAlias,
|
||||
getFormulasByDrugName,
|
||||
replaceMissingWithDrug,
|
||||
resolveMissingByName,
|
||||
} from '../api';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
missingId?: number;
|
||||
drugName: string;
|
||||
/** 药方设置场景:预览时默认勾选当前金方 */
|
||||
formulaId?: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', v: boolean): void;
|
||||
(e: 'done'): void;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const searchStoreId = Number(userStore.userInfo?.store_id || 0);
|
||||
|
||||
const step = ref<'choose' | 'alias' | 'replace'>('choose');
|
||||
const submitting = ref(false);
|
||||
const selectedDrug = ref<ChineseDrugBubbleItem | null>(null);
|
||||
const selectedDisplayName = ref('');
|
||||
|
||||
/** 替换预览:含该药名的金方 */
|
||||
const previewLoading = ref(false);
|
||||
const previewFormulas = ref<any[]>([]);
|
||||
const selectedFormulaIds = ref<number[]>([]);
|
||||
|
||||
const previewColumns = [
|
||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 70 },
|
||||
{ title: '药方名字', dataIndex: 'name', key: 'name', ellipsis: true },
|
||||
{ title: '条文', dataIndex: 'clause_number', key: 'clause_number', width: 90 },
|
||||
{ title: '来源', dataIndex: 'source_name', key: 'source_name', width: 100 },
|
||||
{ title: '药材速览', dataIndex: 'herb_overview', key: 'herb_overview', ellipsis: true },
|
||||
{ title: '标记', key: 'flags', width: 120 },
|
||||
];
|
||||
|
||||
const selectedFormulaCount = computed(() => selectedFormulaIds.value.length);
|
||||
|
||||
const [ChinaMedicineModal, chinaMedicineModalApi] = useVbenModal({
|
||||
connectedComponent: ChinaMedicineFormModal,
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(v) => {
|
||||
if (v) {
|
||||
step.value = 'choose';
|
||||
selectedDrug.value = null;
|
||||
selectedDisplayName.value = '';
|
||||
previewFormulas.value = [];
|
||||
selectedFormulaIds.value = [];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function close() {
|
||||
emit('update:open', false);
|
||||
}
|
||||
|
||||
function buildBasePayload() {
|
||||
const payload: { id?: number; drug_name?: string } = {
|
||||
drug_name: props.drugName,
|
||||
};
|
||||
if (props.missingId && props.missingId > 0) {
|
||||
payload.id = props.missingId;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开中药管理新增弹窗
|
||||
*/
|
||||
function openCreateMedicineModal() {
|
||||
if (!props.drugName) {
|
||||
message.warning('药名为空');
|
||||
return;
|
||||
}
|
||||
const resolvePayload = buildBasePayload();
|
||||
const missingName = props.drugName;
|
||||
close();
|
||||
chinaMedicineModalApi.setData({
|
||||
update: false,
|
||||
values: {
|
||||
drug_name: missingName,
|
||||
drug_alias: missingName,
|
||||
drug_number: `GF${Date.now().toString().slice(-10)}`,
|
||||
status: 1,
|
||||
time_id: 1,
|
||||
type_id: 1,
|
||||
unit_id: 1,
|
||||
frequency_id: 1,
|
||||
},
|
||||
onSuccess: async () => {
|
||||
try {
|
||||
await resolveMissingByName({
|
||||
...resolvePayload,
|
||||
remark: '已通过中药管理新增',
|
||||
});
|
||||
message.success(`缺失药「${missingName}」已标记为已解决`);
|
||||
emit('done');
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '标记已解决失败,请在缺失药品里重新校验');
|
||||
}
|
||||
},
|
||||
});
|
||||
chinaMedicineModalApi.open();
|
||||
}
|
||||
|
||||
function onSelectDrug(item: ChineseDrugBubbleItem) {
|
||||
selectedDrug.value = item;
|
||||
selectedDisplayName.value = String(
|
||||
item?.drug?.drug_name || item?.drug_name || '',
|
||||
).trim();
|
||||
}
|
||||
|
||||
function resolveTargetDrugId(): number {
|
||||
const item = selectedDrug.value;
|
||||
if (!item) return 0;
|
||||
return Number(item.drug_id || item.drug?.id || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载含该缺失药名的金方预览,并默认勾选
|
||||
* - 有 formulaId:默认只勾当前方(可再加选)
|
||||
* - 否则:默认全选
|
||||
*/
|
||||
async function loadFormulaPreview() {
|
||||
if (!props.drugName) {
|
||||
previewFormulas.value = [];
|
||||
selectedFormulaIds.value = [];
|
||||
return;
|
||||
}
|
||||
previewLoading.value = true;
|
||||
try {
|
||||
const list = await getFormulasByDrugName(props.drugName);
|
||||
previewFormulas.value = Array.isArray(list) ? list : [];
|
||||
const allIds = previewFormulas.value.map((r) => Number(r.id)).filter((id) => id > 0);
|
||||
if (props.formulaId && props.formulaId > 0) {
|
||||
// 当前方优先勾选;若列表里没有当前方(异常),仍尽量带上
|
||||
selectedFormulaIds.value = allIds.includes(props.formulaId)
|
||||
? [props.formulaId]
|
||||
: allIds.length
|
||||
? [allIds[0]]
|
||||
: [];
|
||||
} else {
|
||||
selectedFormulaIds.value = allIds;
|
||||
}
|
||||
} catch (e: any) {
|
||||
previewFormulas.value = [];
|
||||
selectedFormulaIds.value = [];
|
||||
message.error(e?.message || '加载受影响金方失败');
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function goPick(next: 'alias' | 'replace') {
|
||||
step.value = next;
|
||||
selectedDrug.value = null;
|
||||
selectedDisplayName.value = '';
|
||||
if (next === 'replace') {
|
||||
await loadFormulaPreview();
|
||||
} else {
|
||||
previewFormulas.value = [];
|
||||
selectedFormulaIds.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function onFormulaSelectChange(keys: (string | number)[]) {
|
||||
selectedFormulaIds.value = keys.map((k) => Number(k)).filter((id) => id > 0);
|
||||
}
|
||||
|
||||
function selectAllFormulas() {
|
||||
selectedFormulaIds.value = previewFormulas.value
|
||||
.map((r) => Number(r.id))
|
||||
.filter((id) => id > 0);
|
||||
}
|
||||
|
||||
function clearFormulaSelection() {
|
||||
selectedFormulaIds.value = [];
|
||||
}
|
||||
|
||||
async function confirmAlias() {
|
||||
const targetDrugId = resolveTargetDrugId();
|
||||
if (!targetDrugId) {
|
||||
message.warning('请先搜索并选择目标药材');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
await bindMissingAsAlias({
|
||||
...buildBasePayload(),
|
||||
target_drug_id: targetDrugId,
|
||||
});
|
||||
message.success(`「${props.drugName}」已写入别名表`);
|
||||
emit('done');
|
||||
close();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '写入别名失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认替换:只改预览勾选的金方
|
||||
*/
|
||||
async function confirmReplace() {
|
||||
const targetDrugId = resolveTargetDrugId();
|
||||
if (!targetDrugId) {
|
||||
message.warning('请先搜索并选择目标药材');
|
||||
return;
|
||||
}
|
||||
if (!selectedFormulaIds.value.length) {
|
||||
message.warning('请至少勾选一个要修改的金方');
|
||||
return;
|
||||
}
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await replaceMissingWithDrug({
|
||||
...buildBasePayload(),
|
||||
target_drug_id: targetDrugId,
|
||||
formula_ids: selectedFormulaIds.value,
|
||||
});
|
||||
message.success(
|
||||
`已将「${res?.old_name || props.drugName}」替换为「${res?.new_name || selectedDisplayName.value}」(共 ${Number(res?.formula_count || 0)} 个金方,旧名已保留)`,
|
||||
);
|
||||
emit('done');
|
||||
close();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '替换失败');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<ChinaMedicineModal />
|
||||
<Modal
|
||||
:open="open"
|
||||
:title="`处理缺失药:${drugName || ''}`"
|
||||
:footer="null"
|
||||
destroy-on-close
|
||||
:width="step === 'replace' ? 860 : 520"
|
||||
@cancel="close"
|
||||
>
|
||||
<div v-if="step === 'choose'" class="gf-resolve-choose">
|
||||
<p class="gf-resolve-tip">
|
||||
该药名在药品库中不存在,请选择处理方式:
|
||||
</p>
|
||||
<Space direction="vertical" style="width: 100%">
|
||||
<Button type="primary" block @click="openCreateMedicineModal">
|
||||
添加药材(打开中药管理新增)
|
||||
</Button>
|
||||
<Button block @click="goPick('alias')">
|
||||
放入药材别名表(挂到已有药,不改金方药名)
|
||||
</Button>
|
||||
<Button block @click="goPick('replace')">
|
||||
替换为已有药材(预览勾选金方后确认)
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div v-else-if="step === 'alias'" class="gf-resolve-alias">
|
||||
<p class="gf-resolve-tip">
|
||||
将「{{ drugName }}」挂到下方目标药材的别名表(金方组成里的药名不变):
|
||||
</p>
|
||||
<div class="gf-resolve-search">
|
||||
<span class="gf-resolve-search-label">搜索中药</span>
|
||||
<ChineseDrugNameBubble
|
||||
:display-name="selectedDisplayName"
|
||||
:type="1"
|
||||
:store-id="searchStoreId"
|
||||
placeholder="输入药名搜索"
|
||||
@select="onSelectDrug"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="selectedDisplayName" class="gf-resolve-selected">
|
||||
已选:{{ selectedDisplayName }}
|
||||
<span v-if="resolveTargetDrugId()">(ID {{ resolveTargetDrugId() }})</span>
|
||||
</p>
|
||||
<Space>
|
||||
<Button @click="step = 'choose'">返回</Button>
|
||||
<Button type="primary" :loading="submitting" @click="confirmAlias">
|
||||
确认挂别名
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div v-else class="gf-resolve-replace">
|
||||
<p class="gf-resolve-tip">
|
||||
1)搜索并选择目标药材;2)预览勾选要一起修改的金方;3)确认后把「{{ drugName }}」替换为目标药名(旧名写入
|
||||
origin_name 并挂别名)。
|
||||
</p>
|
||||
<div class="gf-resolve-search">
|
||||
<span class="gf-resolve-search-label">搜索中药</span>
|
||||
<ChineseDrugNameBubble
|
||||
:display-name="selectedDisplayName"
|
||||
:type="1"
|
||||
:store-id="searchStoreId"
|
||||
placeholder="输入药名搜索"
|
||||
@select="onSelectDrug"
|
||||
/>
|
||||
</div>
|
||||
<p v-if="selectedDisplayName" class="gf-resolve-selected">
|
||||
已选目标:{{ selectedDisplayName }}
|
||||
<span v-if="resolveTargetDrugId()">(ID {{ resolveTargetDrugId() }})</span>
|
||||
</p>
|
||||
<div class="gf-preview-toolbar">
|
||||
<span class="gf-preview-title">
|
||||
受影响金方预览(已勾选 {{ selectedFormulaCount }} / {{ previewFormulas.length }})
|
||||
</span>
|
||||
<Space>
|
||||
<Button size="small" :loading="previewLoading" @click="loadFormulaPreview">
|
||||
刷新
|
||||
</Button>
|
||||
<Button size="small" @click="selectAllFormulas">全选</Button>
|
||||
<Button size="small" @click="clearFormulaSelection">清空</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Table
|
||||
size="small"
|
||||
bordered
|
||||
row-key="id"
|
||||
:loading="previewLoading"
|
||||
:columns="previewColumns"
|
||||
:data-source="previewFormulas"
|
||||
:pagination="previewFormulas.length > 8 ? { pageSize: 8 } : false"
|
||||
:row-selection="{
|
||||
selectedRowKeys: selectedFormulaIds,
|
||||
onChange: onFormulaSelectChange,
|
||||
}"
|
||||
class="gf-preview-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'flags'">
|
||||
<Tag v-if="record.is_abnormal === 1" color="error">异常</Tag>
|
||||
<Tag v-if="record.has_unmatched === 1" color="warning">未匹配</Tag>
|
||||
<Tag
|
||||
v-if="formulaId && Number(record.id) === Number(formulaId)"
|
||||
color="processing"
|
||||
>
|
||||
当前
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
<Space class="gf-resolve-actions">
|
||||
<Button @click="step = 'choose'">返回</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!selectedDisplayName || !selectedFormulaCount"
|
||||
@click="confirmReplace"
|
||||
>
|
||||
确认替换({{ selectedFormulaCount }} 个金方)
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<style scoped>
|
||||
.gf-resolve-tip {
|
||||
margin-bottom: 16px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
line-height: 1.6;
|
||||
}
|
||||
.gf-resolve-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.gf-resolve-search-label {
|
||||
flex-shrink: 0;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 13px;
|
||||
}
|
||||
.gf-resolve-search :deep(.chinese-drug-name-bubble),
|
||||
.gf-resolve-search :deep(.chinese-drug-name-bubble--compact) {
|
||||
width: 100% !important;
|
||||
min-width: 220px !important;
|
||||
max-width: none !important;
|
||||
flex: 1 1 auto !important;
|
||||
}
|
||||
.gf-resolve-selected {
|
||||
margin: 0 0 12px;
|
||||
color: hsl(var(--primary));
|
||||
font-size: 13px;
|
||||
}
|
||||
.gf-preview-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.gf-preview-title {
|
||||
font-size: 13px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
.gf-preview-table {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.gf-resolve-actions {
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方新增/编辑弹窗
|
||||
* 打开时拉取金方来源下拉,通过 updateSchema 注入 source_id 选项
|
||||
* 金方新增/编辑弹窗(基础信息)
|
||||
* 药方组成已拆到独立「药方设置」弹窗,本弹窗不再编辑 drugs_json
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -31,27 +31,11 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const e = await formApi.validate();
|
||||
if (!e.valid) return;
|
||||
const values = await formApi.getValues();
|
||||
// 表单里 drugs_json 是字符串;空串转空数组交给后端
|
||||
const drugsRaw = values.drugs_json;
|
||||
let drugs_json: any = drugsRaw;
|
||||
if (typeof drugsRaw === 'string') {
|
||||
const t = drugsRaw.trim();
|
||||
if (t === '') {
|
||||
drugs_json = [];
|
||||
} else {
|
||||
try {
|
||||
drugs_json = JSON.parse(t);
|
||||
} catch {
|
||||
message.error('药品JSON 不是合法 JSON');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
modalApi.setState({ confirmLoading: true });
|
||||
try {
|
||||
const payload = { ...values, drugs_json };
|
||||
// 基础信息保存不带 drugs_json,避免覆盖已有药方组成
|
||||
const api = isUpdate.value ? updateGoldenFormula : createGoldenFormula;
|
||||
await api(payload);
|
||||
await api(values);
|
||||
message.success('保存成功');
|
||||
gridApi.value?.query?.();
|
||||
gridApi.value?.reload?.();
|
||||
@@ -85,19 +69,10 @@ const [Modal, modalApi] = useVbenModal({
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
isUpdate.value = !!update;
|
||||
if (values && update) {
|
||||
const drugs = values.drugs_json;
|
||||
formApi.setValues({
|
||||
...values,
|
||||
drugs_json:
|
||||
typeof drugs === 'string'
|
||||
? drugs
|
||||
: drugs
|
||||
? JSON.stringify(drugs, null, 2)
|
||||
: '',
|
||||
});
|
||||
formApi.setValues({ ...values });
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
formApi.setValues({ sort: 0, status: 1, drugs_json: '', source_id: 0 });
|
||||
formApi.setValues({ sort: 0, status: 1, source_id: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,3 +4,19 @@ export const STATUS_OPTIONS = [
|
||||
{ label: '启用', value: 1 },
|
||||
{ label: '禁用', value: 0 },
|
||||
];
|
||||
|
||||
/**
|
||||
* 异常标记筛选(前端传 is_abnormal)
|
||||
* 0=正常 / 1=异常(JSON error或0味) / 2=有未匹配药(后端映射 has_unmatched=1)
|
||||
*/
|
||||
export const ABNORMAL_FILTER_OPTIONS = [
|
||||
{ label: '正常', value: 0 },
|
||||
{ label: '异常', value: 1 },
|
||||
{ label: '有未匹配药', value: 2 },
|
||||
];
|
||||
|
||||
/** 缺失药品处理状态 */
|
||||
export const MISSING_DRUG_STATUS_OPTIONS = [
|
||||
{ label: '待处理', value: 0 },
|
||||
{ label: '已解决', value: 1 },
|
||||
];
|
||||
|
||||
@@ -74,22 +74,10 @@ export const modalFormProps: VbenFormProps = {
|
||||
component: 'VbenInput',
|
||||
fieldName: 'herb_overview',
|
||||
label: '药材速览',
|
||||
help: '展示用;药方组成请在行操作「药方设置」中维护,保存组成时会按药名重算本字段',
|
||||
componentProps: { placeholder: '如:桂枝、芍药、甘草、生姜、大枣' },
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'drugs_json',
|
||||
label: '药品JSON',
|
||||
help: '系统字段:name/dose(克数)/unit(g)/usage(先煎后下)。展示扩展:ancient_dose(三两)、prep(去皮/炙/切/擘),仅展示不参与导入',
|
||||
componentProps: {
|
||||
type: 'textarea',
|
||||
rows: 5,
|
||||
placeholder:
|
||||
'[{"name":"桂枝","dose":"9","unit":"g","usage":"","ancient_dose":"三两","prep":"去皮"},{"name":"芍药","dose":"9","unit":"g","ancient_dose":"三两"},{"name":"甘草","dose":"6","unit":"g","ancient_dose":"二两","prep":"炙"},{"name":"生姜","dose":"9","unit":"g","ancient_dose":"三两","prep":"切"},{"name":"大枣","dose":"12","unit":"g","ancient_dose":"十二枚","prep":"擘"}]',
|
||||
},
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'original_formula',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方库管理:CRUD + Excel 导入
|
||||
* 金方库管理:CRUD + Excel 导入 + 药方设置 + 缺失药品
|
||||
* VIP 功能码 golden_formula 由「VIP等级配置」勾选分配,本页只管平台金方数据
|
||||
*/
|
||||
import type { VxeGridListeners } from '#/adapter/vxe-table';
|
||||
@@ -9,7 +9,7 @@ import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Button, Tag, Upload, message, Modal as AntModal } from 'ant-design-vue';
|
||||
import { Tag, message, Modal as AntModal } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
@@ -21,8 +21,11 @@ import {
|
||||
importGoldenFormula,
|
||||
} from './api';
|
||||
import FormModal from './components/modal.vue';
|
||||
import ImportExcelModal from './components/ImportExcelModal.vue';
|
||||
import ImportPreviewModal from './components/ImportPreviewModal.vue';
|
||||
import { STATUS_OPTIONS } from './config/constants';
|
||||
import FormulaDrugsModal from './components/FormulaDrugsModal.vue';
|
||||
import MissingDrugModal from './components/MissingDrugModal.vue';
|
||||
import { ABNORMAL_FILTER_OPTIONS, STATUS_OPTIONS } from './config/constants';
|
||||
import { exportGoldenFormulaTemplate } from './utils/exportGoldenFormulaExcel';
|
||||
import { parseGoldenFormulaExcelBuffer } from './utils/parseGoldenFormulaExcel';
|
||||
|
||||
@@ -58,10 +61,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
label: '异常标记',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '正常', value: 0 },
|
||||
{ label: '异常', value: 1 },
|
||||
],
|
||||
options: ABNORMAL_FILTER_OPTIONS,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -89,14 +89,14 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
{
|
||||
field: 'is_abnormal',
|
||||
title: '异常',
|
||||
width: 90,
|
||||
width: 110,
|
||||
slots: { default: 'is_abnormal' },
|
||||
},
|
||||
{ field: 'pinyin_initials', title: '首拼', width: 100 },
|
||||
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
{ field: 'created_at', title: '创建时间', width: 170 },
|
||||
{ title: '操作', slots: { default: 'action' }, width: 160, fixed: 'right' },
|
||||
{ title: '操作', slots: { default: 'action' }, width: 220, fixed: 'right' },
|
||||
],
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
@@ -134,11 +134,26 @@ const [FormModalComp, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModal,
|
||||
});
|
||||
|
||||
/** 导入选文件弹窗:拖拽 / 点击 / 粘贴 */
|
||||
const [ImportExcelModalComp, importExcelApi] = useVbenModal({
|
||||
connectedComponent: ImportExcelModal,
|
||||
});
|
||||
|
||||
/** 导入预览弹窗:解析后预览 + 选金方来源 + 确认导入 */
|
||||
const [ImportPreviewModalComp, importPreviewApi] = useVbenModal({
|
||||
connectedComponent: ImportPreviewModal,
|
||||
});
|
||||
|
||||
/** 药方设置:独立编辑组成 */
|
||||
const [FormulaDrugsModalComp, formulaDrugsModalApi] = useVbenModal({
|
||||
connectedComponent: FormulaDrugsModal,
|
||||
});
|
||||
|
||||
/** 缺失药品清单 */
|
||||
const [MissingDrugModalComp, missingDrugModalApi] = useVbenModal({
|
||||
connectedComponent: MissingDrugModal,
|
||||
});
|
||||
|
||||
const showModal = (data: any = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
values: data,
|
||||
@@ -148,6 +163,20 @@ const showModal = (data: any = {}, isUpdate = false) => {
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
/** 打开药方设置弹窗 */
|
||||
const showFormulaDrugsModal = (row: any) => {
|
||||
formulaDrugsModalApi.setData({
|
||||
id: Number(row?.id || 0),
|
||||
gridApi,
|
||||
});
|
||||
formulaDrugsModalApi.open();
|
||||
};
|
||||
|
||||
/** 打开缺失药品弹窗 */
|
||||
const showMissingDrugModal = () => {
|
||||
missingDrugModalApi.open();
|
||||
};
|
||||
|
||||
const handleDelete = async (ids: number[]) => {
|
||||
await deleteGoldenFormula({ ids });
|
||||
message.success('删除成功');
|
||||
@@ -163,6 +192,17 @@ async function handleDownloadTemplate() {
|
||||
);
|
||||
}
|
||||
|
||||
/** 打开导入选文件弹窗(不再直接调起系统文件选择) */
|
||||
function openImportExcelModal() {
|
||||
importExcelApi.setData({
|
||||
onConfirmFile: handleImportFile,
|
||||
});
|
||||
importExcelApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Excel → 打开展示预览(由导入选文件弹窗确认后调用)
|
||||
*/
|
||||
async function handleImportFile(file: File) {
|
||||
importing.value = true;
|
||||
try {
|
||||
@@ -172,22 +212,25 @@ async function handleImportFile(file: File) {
|
||||
const detail = parsed.firstInvalidReason
|
||||
? `:${parsed.firstInvalidReason}`
|
||||
: '';
|
||||
// warning 已提示用户;抛错仅用于保持选文件弹窗不关闭
|
||||
message.warning(
|
||||
parsed.invalidCount
|
||||
? `没有可导入的有效行(无效 ${parsed.invalidCount} 行)${detail}`
|
||||
: '文件无有效数据,请确认填写的是「金方导入」工作表',
|
||||
);
|
||||
return false;
|
||||
throw new Error('__import_empty__');
|
||||
}
|
||||
// 打开预览弹窗:用户选金方来源 + 确认后再提交
|
||||
importPreviewApi.setData({ rows: parsed.items });
|
||||
importPreviewApi.open();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '解析失败');
|
||||
if (e?.message !== '__import_empty__') {
|
||||
message.error(e?.message || '解析失败');
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
importing.value = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,12 +245,13 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
});
|
||||
const ok = Number(res?.success || 0);
|
||||
const fail = Number(res?.fail || 0);
|
||||
const missingCount = Number(res?.missing_count || 0);
|
||||
const errors = Array.isArray(res?.errors) ? res.errors : [];
|
||||
AntModal.info({
|
||||
title: '导入结果',
|
||||
content: `成功 ${ok} 条,失败 ${fail} 条${
|
||||
errors.length ? `\n${errors.slice(0, 10).join('\n')}` : ''
|
||||
}`,
|
||||
missingCount > 0 ? `,缺失药名 ${missingCount} 个(已写入缺失药品表)` : ''
|
||||
}${errors.length ? `\n${errors.slice(0, 10).join('\n')}` : ''}`,
|
||||
width: 520,
|
||||
});
|
||||
gridApi.reload();
|
||||
@@ -221,7 +265,10 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
<template>
|
||||
<Page auto-content-height title="金方管理">
|
||||
<FormModalComp />
|
||||
<ImportExcelModalComp />
|
||||
<ImportPreviewModalComp @confirm="handleConfirmImport" />
|
||||
<FormulaDrugsModalComp />
|
||||
<MissingDrugModalComp />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
@@ -235,6 +282,15 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
label: '下载模板',
|
||||
onClick: handleDownloadTemplate,
|
||||
},
|
||||
{
|
||||
label: '导入 Excel',
|
||||
loading: importing,
|
||||
onClick: openImportExcelModal,
|
||||
},
|
||||
{
|
||||
label: '缺失药品',
|
||||
onClick: showMissingDrugModal,
|
||||
},
|
||||
{
|
||||
label: '批量删除',
|
||||
danger: true,
|
||||
@@ -249,14 +305,6 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<Upload
|
||||
:show-upload-list="false"
|
||||
:before-upload="handleImportFile"
|
||||
accept=".xlsx,.xls"
|
||||
class="ml-2 inline-block"
|
||||
>
|
||||
<Button :loading="importing" type="default">导入 Excel</Button>
|
||||
</Upload>
|
||||
</template>
|
||||
<!-- 异常处方:药方名字整列标红 -->
|
||||
<template #name="{ row }">
|
||||
@@ -268,8 +316,16 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
{{ row.source_name || row.source || '—' }}
|
||||
</template>
|
||||
<template #is_abnormal="{ row }">
|
||||
<Tag v-if="row.is_abnormal === 1" color="error">异常</Tag>
|
||||
<Tag v-else color="success">正常</Tag>
|
||||
<span class="gf-flag-cell">
|
||||
<Tag v-if="row.is_abnormal === 1" color="error">异常</Tag>
|
||||
<Tag v-if="row.has_unmatched === 1" color="warning">未匹配</Tag>
|
||||
<Tag
|
||||
v-if="row.is_abnormal !== 1 && row.has_unmatched !== 1"
|
||||
color="success"
|
||||
>
|
||||
正常
|
||||
</Tag>
|
||||
</span>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.status === 1 ? 'success' : 'default'">
|
||||
@@ -283,6 +339,10 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
label: '编辑',
|
||||
onClick: () => showModal(row, true),
|
||||
},
|
||||
{
|
||||
label: '药方设置',
|
||||
onClick: () => showFormulaDrugsModal(row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
danger: true,
|
||||
@@ -303,4 +363,9 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: 600;
|
||||
}
|
||||
.gf-flag-cell {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user