feat: 金方相关
This commit is contained in:
@@ -3,6 +3,9 @@ import { requestClient } from '#/api/request';
|
||||
/** 金方库管理 API */
|
||||
const prefix = 'golden-formula/';
|
||||
|
||||
/** 金方来源字典 API */
|
||||
const sourcePrefix = 'golden-formula-source/';
|
||||
|
||||
export async function getGoldenFormulaList(data: any) {
|
||||
return requestClient.get<any>(`${prefix}list`, { params: data });
|
||||
}
|
||||
@@ -23,7 +26,32 @@ export async function deleteGoldenFormula(data: Record<string, any>) {
|
||||
return requestClient.post<any>(`${prefix}delete`, data);
|
||||
}
|
||||
|
||||
/** Excel 解析后的行批量导入 */
|
||||
export async function importGoldenFormula(data: { rows: Record<string, any>[] }) {
|
||||
/** Excel 解析后的行批量导入(带全局金方来源 source_id) */
|
||||
export async function importGoldenFormula(data: {
|
||||
rows: Record<string, any>[];
|
||||
source_id?: number;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}import`, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 药品存在性排查(全局中药饮片 type=1 + 别名表)
|
||||
* 预览页打开时调用,返回存在的药名 + 不存在的药名
|
||||
*/
|
||||
export async function checkGoldenFormulaDrugs(data: { drug_names: string[] }) {
|
||||
return requestClient.post<{ exists: string[]; missing: string[] }>(
|
||||
`${prefix}check-drugs`,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/** 金方来源字典 - 下拉选项 */
|
||||
export async function getGoldenFormulaSourceOption() {
|
||||
return requestClient.get<any[]>(`${sourcePrefix}option`);
|
||||
}
|
||||
|
||||
/** 金方来源字典 - 新增来源(预览页输入框新增) */
|
||||
export async function createGoldenFormulaSource(data: { name: string }) {
|
||||
return requestClient.post<any>(`${sourcePrefix}create`, data);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方导入预览弹窗
|
||||
* - 解析后的行在这里表格预览
|
||||
* - 顶部全局「金方来源」选择器:下拉选已有 + 输入新增,所有行共用同一 source_id
|
||||
* - 打开后批量排查药品存在性(中药饮片 type=1 + 别名表),表格加「不存在的药品」列红字标记
|
||||
* - 确认导入时 emit { rows, sourceId },sourceId 为空提示选择来源
|
||||
*/
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
checkGoldenFormulaDrugs,
|
||||
createGoldenFormulaSource,
|
||||
getGoldenFormulaSourceOption,
|
||||
} from '../api';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', payload: { rows: any[]; sourceId: number }): void;
|
||||
}>();
|
||||
|
||||
// 预览的原始行(Excel 解析后)
|
||||
const rows = ref<any[]>([]);
|
||||
// 金方来源下拉选项
|
||||
const sourceOptions = ref<{ label: string; value: number }[]>([]);
|
||||
// 选中的金方来源 id
|
||||
const sourceId = ref<number>(0);
|
||||
// 新增来源输入框文案
|
||||
const newSourceName = ref('');
|
||||
// 来源下拉 loading
|
||||
const sourceLoading = ref(false);
|
||||
// 新增来源提交中
|
||||
const creatingSource = ref(false);
|
||||
// 药品排查:不存在的药名集合
|
||||
const missingSet = ref<Set<string>>(new Set());
|
||||
// 排查中
|
||||
const checking = ref(false);
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
fullscreenButton: false,
|
||||
draggable: true,
|
||||
class: 'w-[90%]',
|
||||
onCancel() {
|
||||
modalApi.close();
|
||||
},
|
||||
onConfirm: async () => {
|
||||
if (sourceId.value <= 0) {
|
||||
message.warning('请选择金方来源');
|
||||
return;
|
||||
}
|
||||
if (!rows.value.length) {
|
||||
message.warning('没有可导入的数据');
|
||||
return;
|
||||
}
|
||||
emit('confirm', { rows: rows.value, sourceId: sourceId.value });
|
||||
modalApi.close();
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
// 关闭时重置状态
|
||||
rows.value = [];
|
||||
sourceId.value = 0;
|
||||
newSourceName.value = '';
|
||||
missingSet.value = new Set();
|
||||
return;
|
||||
}
|
||||
const data = modalApi.getData<{ rows?: any[] }>() || {};
|
||||
rows.value = Array.isArray(data.rows) ? data.rows : [];
|
||||
await loadSourceOptions();
|
||||
// 打开后批量排查药品存在性
|
||||
await runDrugCheck();
|
||||
},
|
||||
});
|
||||
|
||||
/** 加载金方来源下拉 */
|
||||
async function loadSourceOptions() {
|
||||
sourceLoading.value = true;
|
||||
try {
|
||||
const list = await getGoldenFormulaSourceOption();
|
||||
sourceOptions.value = (Array.isArray(list) ? list : []).map((item: any) => ({
|
||||
label: String(item.name || ''),
|
||||
value: Number(item.id || 0),
|
||||
}));
|
||||
} catch (e: any) {
|
||||
sourceOptions.value = [];
|
||||
message.error(e?.message || '加载金方来源失败');
|
||||
} finally {
|
||||
sourceLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增来源:成功后刷新下拉并选中 */
|
||||
async function handleCreateSource() {
|
||||
const name = newSourceName.value.trim();
|
||||
if (!name) {
|
||||
message.warning('请输入来源名称');
|
||||
return;
|
||||
}
|
||||
// 已存在同名则直接选中
|
||||
const exist = sourceOptions.value.find(
|
||||
(o) => o.label === name || o.label.includes(name),
|
||||
);
|
||||
if (exist) {
|
||||
sourceId.value = exist.value;
|
||||
newSourceName.value = '';
|
||||
message.success('已存在该来源,已自动选中');
|
||||
return;
|
||||
}
|
||||
creatingSource.value = true;
|
||||
try {
|
||||
const res = await createGoldenFormulaSource({ name });
|
||||
const newId = Number(res?.id || 0);
|
||||
if (newId > 0) {
|
||||
await loadSourceOptions();
|
||||
sourceId.value = newId;
|
||||
newSourceName.value = '';
|
||||
message.success('来源新增成功');
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '新增来源失败');
|
||||
} finally {
|
||||
creatingSource.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从所有行的 drugs_json 收集药名,去重后批量排查存在性
|
||||
*/
|
||||
async function runDrugCheck() {
|
||||
if (!rows.value.length) {
|
||||
missingSet.value = new Set();
|
||||
return;
|
||||
}
|
||||
const nameSet = new Set<string>();
|
||||
for (const row of rows.value) {
|
||||
const raw = row?.drugs_json;
|
||||
if (!raw) continue;
|
||||
try {
|
||||
const arr = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
if (Array.isArray(arr)) {
|
||||
for (const item of arr) {
|
||||
const n = String(item?.name || item?.drug_name || '').trim();
|
||||
if (n) nameSet.add(n);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// JSON 非法则跳过该行
|
||||
}
|
||||
}
|
||||
const names = Array.from(nameSet);
|
||||
if (!names.length) {
|
||||
missingSet.value = new Set();
|
||||
return;
|
||||
}
|
||||
checking.value = true;
|
||||
try {
|
||||
const res = await checkGoldenFormulaDrugs({ drug_names: names });
|
||||
const missing = Array.isArray(res?.missing) ? res.missing : [];
|
||||
missingSet.value = new Set(missing);
|
||||
} catch (e: any) {
|
||||
// 排查失败不阻塞预览
|
||||
missingSet.value = new Set();
|
||||
} finally {
|
||||
checking.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算单行的「不存在的药品」列表(该行药名 ∩ 缺失集合) */
|
||||
function rowMissingDrugs(row: any): string[] {
|
||||
const raw = row?.drugs_json;
|
||||
if (!raw) return [];
|
||||
let arr: any[] = [];
|
||||
try {
|
||||
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
if (Array.isArray(parsed)) arr = parsed;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const result: string[] = [];
|
||||
for (const item of arr) {
|
||||
const n = String(item?.name || item?.drug_name || '').trim();
|
||||
if (n && missingSet.value.has(n)) result.push(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 汇总:不存在的药品总数(用于顶部提示) */
|
||||
const missingSummary = computed(() => Array.from(missingSet.value));
|
||||
|
||||
/**
|
||||
* 判定单行是否异常处方
|
||||
* - drugs_json 解析后是对象且含 error(如 {error:无数据})
|
||||
* - 或规范化后药味数为 0(空数组 / 无法解析出有效药名)
|
||||
*/
|
||||
function isAbnormalRow(row: any): boolean {
|
||||
const raw = row?.drugs_json;
|
||||
if (raw === null || raw === undefined || raw === '') return true;
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object') return true;
|
||||
// 关联对象含 error:典型 {"error":"无数据"}
|
||||
if (!Array.isArray(parsed) && Object.prototype.hasOwnProperty.call(parsed, 'error')) {
|
||||
return true;
|
||||
}
|
||||
const list = Array.isArray(parsed)
|
||||
? parsed
|
||||
: Array.isArray(parsed?.drugs)
|
||||
? parsed.drugs
|
||||
: Array.isArray(parsed?.items)
|
||||
? parsed.items
|
||||
: [];
|
||||
// 药味数为 0
|
||||
const count = list.filter(
|
||||
(item: any) => String(item?.name || item?.drug_name || '').trim() !== '',
|
||||
).length;
|
||||
return count === 0;
|
||||
}
|
||||
|
||||
/** 异常处方数量(顶部提示用) */
|
||||
const abnormalCount = computed(
|
||||
() => rows.value.filter((r) => isAbnormalRow(r)).length,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="金方导入预览">
|
||||
<div class="flex flex-col gap-3">
|
||||
<!-- 顶部:金方来源全局选择器 + 新增 -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium">金方来源:</span>
|
||||
<Select
|
||||
v-model:value="sourceId"
|
||||
:options="sourceOptions"
|
||||
:loading="sourceLoading"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
placeholder="选择已有金方来源"
|
||||
style="min-width: 220px"
|
||||
/>
|
||||
<span class="text-xs text-muted-foreground">或新增:</span>
|
||||
<Input
|
||||
v-model:value="newSourceName"
|
||||
placeholder="输入新来源名称"
|
||||
style="width: 200px"
|
||||
@press-enter="handleCreateSource"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="creatingSource"
|
||||
size="small"
|
||||
@click="handleCreateSource"
|
||||
>
|
||||
新增来源
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 异常处方提示(仅警告,不拦截) -->
|
||||
<div
|
||||
v-if="abnormalCount > 0"
|
||||
class="rounded-lg border border-red-500/35 bg-red-500/10 px-3 py-2 text-xs"
|
||||
>
|
||||
<span class="font-medium">⚠ 异常处方:</span>
|
||||
共
|
||||
<Tag color="error" class="!mx-1">{{ abnormalCount }}</Tag>
|
||||
条(药品JSON含 error 或药味数为 0),仍可导入,入库后标记为异常
|
||||
</div>
|
||||
|
||||
<!-- 药品存在性排查提示(仅警告,不拦截) -->
|
||||
<div
|
||||
v-if="missingSummary.length"
|
||||
class="rounded-lg border border-orange-500/35 bg-orange-500/10 px-3 py-2 text-xs"
|
||||
>
|
||||
<span class="font-medium">⚠ 药品排查:</span>
|
||||
共发现
|
||||
<Tag color="orange" class="!mx-1">{{ missingSummary.length }}</Tag>
|
||||
个不存在的药品(仅提示,不拦截导入):
|
||||
<span class="text-orange-700">{{ missingSummary.join('、') }}</span>
|
||||
<span v-if="checking" class="ml-2 text-muted-foreground">排查中…</span>
|
||||
</div>
|
||||
|
||||
<!-- 预览表格 -->
|
||||
<Table
|
||||
:columns="[
|
||||
{ title: '序号', width: 60, customRender: ({ index }) => index + 1 },
|
||||
{ title: '药方名字', dataIndex: 'name', width: 140 },
|
||||
{ title: '条文编号', dataIndex: 'clause_number', width: 110 },
|
||||
{ title: '状态', key: 'abnormal', width: 90 },
|
||||
{
|
||||
title: '药品JSON',
|
||||
dataIndex: 'drugs_json',
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => String(text || ''),
|
||||
},
|
||||
{ title: '药材速览', dataIndex: 'herb_overview', width: 180, ellipsis: true },
|
||||
{
|
||||
title: '药方主治(文言文)',
|
||||
dataIndex: 'indication_original',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '现代主治(白话文)',
|
||||
dataIndex: 'indication_translation',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
},
|
||||
{ title: '拼音首拼', dataIndex: 'pinyin_initials', width: 100 },
|
||||
{ title: '拼音全拼', dataIndex: 'pinyin', width: 120 },
|
||||
{ title: '不存在的药品', key: 'missing', width: 180 },
|
||||
]"
|
||||
:data-source="rows"
|
||||
:pagination="{ pageSize: 10, showSizeChanger: true }"
|
||||
size="small"
|
||||
:scroll="{ x: 1400 }"
|
||||
:row-class-name="(record) => (isAbnormalRow(record) ? 'gf-abnormal-row' : '')"
|
||||
row-key="name"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'abnormal'">
|
||||
<Tag v-if="isAbnormalRow(record)" color="error" class="!m-0">异常</Tag>
|
||||
<Tag v-else color="success" class="!m-0">正常</Tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'missing'">
|
||||
<Tooltip
|
||||
v-if="rowMissingDrugs(record).length"
|
||||
:title="rowMissingDrugs(record).join('、')"
|
||||
>
|
||||
<span class="text-red-500">
|
||||
{{ rowMissingDrugs(record).join('、') }}
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tag v-else color="success" class="!m-0">齐全</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 异常处方行:浅红底,暗色下用半透明红 */
|
||||
:deep(.gf-abnormal-row) > td {
|
||||
background-color: hsl(var(--destructive) / 0.08) !important;
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 金方新增/编辑弹窗
|
||||
* 打开时拉取金方来源下拉,通过 updateSchema 注入 source_id 选项
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
@@ -10,7 +11,11 @@ import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
import { createGoldenFormula, updateGoldenFormula } from '../api';
|
||||
import {
|
||||
createGoldenFormula,
|
||||
getGoldenFormulaSourceOption,
|
||||
updateGoldenFormula,
|
||||
} from '../api';
|
||||
import { modalFormProps } from '../config/form';
|
||||
|
||||
const isUpdate = ref(false);
|
||||
@@ -55,12 +60,28 @@ const [Modal, modalApi] = useVbenModal({
|
||||
modalApi.setState({ confirmLoading: false });
|
||||
}
|
||||
},
|
||||
onOpenChange(isOpen: boolean) {
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
gridApi.value = isOpen ? modalApi.getData()?.gridApi : null;
|
||||
if (!isOpen) {
|
||||
formApi.resetForm();
|
||||
return;
|
||||
}
|
||||
// 打开时拉取金方来源下拉选项并注入
|
||||
try {
|
||||
const list = await getGoldenFormulaSourceOption();
|
||||
const options = (Array.isArray(list) ? list : []).map((item: any) => ({
|
||||
label: String(item.name || ''),
|
||||
value: Number(item.id || 0),
|
||||
}));
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'source_id',
|
||||
componentProps: { options },
|
||||
},
|
||||
]);
|
||||
} catch {
|
||||
// 拉取失败不阻塞表单
|
||||
}
|
||||
const { values, update } = modalApi.getData<Record<string, any>>() || {};
|
||||
isUpdate.value = !!update;
|
||||
if (values && update) {
|
||||
@@ -76,7 +97,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
});
|
||||
} else {
|
||||
formApi.resetForm();
|
||||
formApi.setValues({ sort: 0, status: 1, drugs_json: '' });
|
||||
formApi.setValues({ sort: 0, status: 1, drugs_json: '', source_id: 0 });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -26,10 +26,32 @@ export const modalFormProps: VbenFormProps = {
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'source',
|
||||
fieldName: 'clause_number',
|
||||
label: '条文编号',
|
||||
help: '典籍条文编号,如「第12条」,可空',
|
||||
componentProps: { placeholder: '如:第12条' },
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
fieldName: 'source_id',
|
||||
label: '金方来源',
|
||||
help: '选择来源字典;如无合适项可先在「金方来源管理」新增',
|
||||
componentProps: {
|
||||
showSearch: true,
|
||||
optionFilterProp: 'label',
|
||||
options: [],
|
||||
allowClear: true,
|
||||
placeholder: '选择金方来源',
|
||||
},
|
||||
defaultValue: 0,
|
||||
},
|
||||
{
|
||||
component: 'VbenInput',
|
||||
fieldName: 'source',
|
||||
label: '来源(旧文本)',
|
||||
formItemClass: 'col-span-6',
|
||||
componentProps: { placeholder: '如:伤寒论' },
|
||||
componentProps: { placeholder: '如:伤寒论(旧字段,优先用上面的来源选择)' },
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
importGoldenFormula,
|
||||
} from './api';
|
||||
import FormModal from './components/modal.vue';
|
||||
import ImportPreviewModal from './components/ImportPreviewModal.vue';
|
||||
import { STATUS_OPTIONS } from './config/constants';
|
||||
import { exportGoldenFormulaTemplate } from './utils/exportGoldenFormulaExcel';
|
||||
import { parseGoldenFormulaExcelBuffer } from './utils/parseGoldenFormulaExcel';
|
||||
@@ -51,6 +52,18 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
label: '状态',
|
||||
componentProps: { allowClear: true, options: STATUS_OPTIONS },
|
||||
},
|
||||
{
|
||||
component: 'VbenSelect',
|
||||
fieldName: 'is_abnormal',
|
||||
label: '异常标记',
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
options: [
|
||||
{ label: '正常', value: 0 },
|
||||
{ label: '异常', value: 1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
gridOptions: {
|
||||
@@ -58,10 +71,27 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
||||
columns: [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{ field: 'id', title: 'ID', width: 80 },
|
||||
{ field: 'name', title: '药方名字', minWidth: 140 },
|
||||
{ field: 'source', title: '来源', width: 120 },
|
||||
{
|
||||
field: 'name',
|
||||
title: '药方名字',
|
||||
minWidth: 140,
|
||||
slots: { default: 'name' },
|
||||
},
|
||||
{ field: 'clause_number', title: '条文编号', width: 110 },
|
||||
{
|
||||
field: 'source_name',
|
||||
title: '来源',
|
||||
width: 120,
|
||||
slots: { default: 'source_name' },
|
||||
},
|
||||
{ field: 'herb_overview', title: '药材速览', minWidth: 180 },
|
||||
{ field: 'drug_count', title: '药味数', width: 80 },
|
||||
{
|
||||
field: 'is_abnormal',
|
||||
title: '异常',
|
||||
width: 90,
|
||||
slots: { default: 'is_abnormal' },
|
||||
},
|
||||
{ field: 'pinyin_initials', title: '首拼', width: 100 },
|
||||
{ field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
|
||||
{ field: 'sort', title: '排序', width: 80 },
|
||||
@@ -104,6 +134,11 @@ const [FormModalComp, formModalApi] = useVbenModal({
|
||||
connectedComponent: FormModal,
|
||||
});
|
||||
|
||||
/** 导入预览弹窗:解析后预览 + 选金方来源 + 确认导入 */
|
||||
const [ImportPreviewModalComp, importPreviewApi] = useVbenModal({
|
||||
connectedComponent: ImportPreviewModal,
|
||||
});
|
||||
|
||||
const showModal = (data: any = {}, isUpdate = false) => {
|
||||
formModalApi.setData({
|
||||
values: data,
|
||||
@@ -144,7 +179,27 @@ async function handleImportFile(file: File) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const res = await importGoldenFormula({ rows: parsed.items });
|
||||
// 打开预览弹窗:用户选金方来源 + 确认后再提交
|
||||
importPreviewApi.setData({ rows: parsed.items });
|
||||
importPreviewApi.open();
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '解析失败');
|
||||
} finally {
|
||||
importing.value = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览弹窗确认后:带 source_id 提交导入,弹结果框
|
||||
*/
|
||||
async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
importing.value = true;
|
||||
try {
|
||||
const res = await importGoldenFormula({
|
||||
rows: payload.rows,
|
||||
source_id: payload.sourceId,
|
||||
});
|
||||
const ok = Number(res?.success || 0);
|
||||
const fail = Number(res?.fail || 0);
|
||||
const errors = Array.isArray(res?.errors) ? res.errors : [];
|
||||
@@ -161,12 +216,12 @@ async function handleImportFile(file: File) {
|
||||
} finally {
|
||||
importing.value = false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height title="金方管理">
|
||||
<FormModalComp />
|
||||
<ImportPreviewModalComp @confirm="handleConfirmImport" />
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
@@ -203,6 +258,19 @@ async function handleImportFile(file: File) {
|
||||
<Button :loading="importing" type="default">导入 Excel</Button>
|
||||
</Upload>
|
||||
</template>
|
||||
<!-- 异常处方:药方名字整列标红 -->
|
||||
<template #name="{ row }">
|
||||
<span :class="{ 'gf-name-abnormal': row.is_abnormal === 1 }">
|
||||
{{ row.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template #source_name="{ row }">
|
||||
{{ 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>
|
||||
</template>
|
||||
<template #status="{ row }">
|
||||
<Tag :color="row.status === 1 ? 'success' : 'default'">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
@@ -229,3 +297,10 @@ async function handleImportFile(file: File) {
|
||||
</Grid>
|
||||
</Page>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 异常处方药方名字:用主题 destructive 色,暗色下可读 */
|
||||
.gf-name-abnormal {
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,17 +8,16 @@ const THIN_BORDER: Partial<ExcelJS.Borders> = {
|
||||
};
|
||||
|
||||
const HEADERS = [
|
||||
'序号',
|
||||
'药方名字',
|
||||
'条文编号',
|
||||
'药品JSON',
|
||||
'药材速览',
|
||||
'原方',
|
||||
'药方主治(原文)',
|
||||
'药方主治(译文)',
|
||||
'简介',
|
||||
'封面',
|
||||
'药方拼音首拼',
|
||||
'药方拼音全拼',
|
||||
'金方来源',
|
||||
'药方主治(文言文)',
|
||||
'现代主治(白话文)',
|
||||
'拼音首拼',
|
||||
'拼音全拼',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
@@ -28,17 +27,16 @@ export async function exportGoldenFormulaTemplate(): Promise<ArrayBuffer> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const sheet = workbook.addWorksheet('金方导入');
|
||||
sheet.columns = [
|
||||
{ width: 14 },
|
||||
{ width: 52 },
|
||||
{ width: 24 },
|
||||
{ width: 48 },
|
||||
{ width: 24 },
|
||||
{ width: 24 },
|
||||
{ width: 20 },
|
||||
{ width: 20 },
|
||||
{ width: 12 },
|
||||
{ width: 16 },
|
||||
{ width: 12 },
|
||||
{ width: 8 }, // 序号
|
||||
{ width: 14 }, // 药方名字
|
||||
{ width: 14 }, // 条文编号
|
||||
{ width: 52 }, // 药品JSON
|
||||
{ width: 24 }, // 药材速览
|
||||
{ width: 48 }, // 原方
|
||||
{ width: 28 }, // 药方主治(文言文)
|
||||
{ width: 28 }, // 现代主治(白话文)
|
||||
{ width: 12 }, // 拼音首拼
|
||||
{ width: 16 }, // 拼音全拼
|
||||
];
|
||||
const headerRow = sheet.addRow([...HEADERS]);
|
||||
headerRow.height = 28;
|
||||
@@ -58,17 +56,16 @@ export async function exportGoldenFormulaTemplate(): Promise<ArrayBuffer> {
|
||||
const sampleOriginal =
|
||||
'桂枝三两,去皮 芍药三两 甘草二两,炙 生姜三两,切 大枣十二枚,擘。右五味,㕮咀三味,以水七升,微火煮取三升,去滓,适寒温,服一升。服已须臾,啜热稀粥一升余,以助药力。禁生冷、粘滑、肉面、五辛、酒酪、臭恶等物。';
|
||||
const sample = sheet.addRow([
|
||||
'桂枝汤',
|
||||
sampleDrugs,
|
||||
'桂枝、芍药、甘草、生姜、大枣',
|
||||
sampleOriginal,
|
||||
'太阳中风,阳浮而阴弱',
|
||||
'外感风寒表虚证',
|
||||
'解肌发表,调和营卫',
|
||||
'',
|
||||
'gzt',
|
||||
'guizhitang',
|
||||
'伤寒论',
|
||||
1, // 序号
|
||||
'桂枝汤', // 药方名字
|
||||
'第12条', // 条文编号
|
||||
sampleDrugs, // 药品JSON
|
||||
'桂枝、芍药、甘草、生姜、大枣', // 药材速览
|
||||
sampleOriginal, // 原方
|
||||
'太阳中风,阳浮而阴弱', // 药方主治(文言文)
|
||||
'外感风寒表虚证', // 现代主治(白话文)
|
||||
'gzt', // 拼音首拼
|
||||
'guizhitang', // 拼音全拼
|
||||
]);
|
||||
sample.height = 80;
|
||||
sample.eachCell((cell) => {
|
||||
@@ -90,7 +87,9 @@ export async function exportGoldenFormulaTemplate(): Promise<ArrayBuffer> {
|
||||
cell.border = THIN_BORDER;
|
||||
});
|
||||
const tips: [string, string][] = [
|
||||
['序号', '仅作行号,不导入;可空'],
|
||||
['药方名字', '必填'],
|
||||
['条文编号', '典籍条文编号,如「第12条」,可空'],
|
||||
[
|
||||
'药品JSON',
|
||||
'系统字段(导入用):name/dose(克数数字)/unit(固定g)/usage(先煎、后下等,可空)。展示扩展(仅展示):ancient_dose(如三两、十二枚)、prep(去皮、炙、切、擘)。勿把古代单位或炮制写进 dose/unit/usage。',
|
||||
@@ -100,13 +99,10 @@ export async function exportGoldenFormulaTemplate(): Promise<ArrayBuffer> {
|
||||
'原方',
|
||||
'典籍药味+煮法+服法+禁忌全文,如「桂枝三两,去皮…服一升…禁生冷…」',
|
||||
],
|
||||
['药方主治(原文)', '主治典籍原文'],
|
||||
['药方主治(译文)', '主治白话译文'],
|
||||
['简介', '方义/说明'],
|
||||
['封面', '封面图完整 URL,可空'],
|
||||
['药方拼音首拼', '可空,系统按药方名自动生成'],
|
||||
['药方拼音全拼', '可空,系统按药方名自动生成'],
|
||||
['金方来源', '如伤寒论、金匮要略'],
|
||||
['药方主治(文言文)', '主治典籍原文'],
|
||||
['现代主治(白话文)', '主治白话译文'],
|
||||
['拼音首拼', '可空,系统按药方名自动生成'],
|
||||
['拼音全拼', '可空,系统按药方名自动生成'],
|
||||
];
|
||||
tips.forEach(([k, v]) => {
|
||||
const row = tip.addRow([k, v]);
|
||||
|
||||
@@ -4,16 +4,14 @@ const MAX_ROWS = 2000;
|
||||
|
||||
export type ParsedGoldenFormulaRow = {
|
||||
name: string;
|
||||
clause_number: string;
|
||||
drugs_json: string;
|
||||
herb_overview: string;
|
||||
original_formula: string;
|
||||
indication_original: string;
|
||||
indication_translation: string;
|
||||
intro: string;
|
||||
cover: string;
|
||||
pinyin_initials: string;
|
||||
pinyin: string;
|
||||
source: string;
|
||||
status: number;
|
||||
sort: number;
|
||||
};
|
||||
@@ -51,16 +49,31 @@ function normalizeHeader(value: string): string {
|
||||
}
|
||||
|
||||
const NAME_ALIASES = ['药方名字', '药方名称', '方名', 'name'];
|
||||
const CLAUSE_ALIASES = ['条文编号', '条文', '条款', 'clause_number'];
|
||||
const DRUGS_ALIASES = ['药品JSON', '药品json', '药品', 'drugs_json', 'drugs'];
|
||||
const HERB_ALIASES = ['药材速览', '速览', 'herb_overview'];
|
||||
const ORIGINAL_ALIASES = ['原方', '原方全文', 'original_formula', '方文'];
|
||||
const IND_ORI_ALIASES = ['药方主治(原文)', '药方主治原文', '主治原文', 'indication_original'];
|
||||
const IND_TR_ALIASES = ['药方主治(译文)', '药方主治译文', '主治译文', 'indication_translation'];
|
||||
const INTRO_ALIASES = ['简介', 'intro'];
|
||||
const COVER_ALIASES = ['封面', '封面图', 'cover'];
|
||||
const PY_ABBR_ALIASES = ['药方拼音首拼', '拼音首拼', '首拼', 'pinyin_initials'];
|
||||
const PY_FULL_ALIASES = ['药方拼音全拼', '拼音全拼', '全拼', 'pinyin'];
|
||||
const SOURCE_ALIASES = ['金方来源', '来源', 'source'];
|
||||
const IND_ORI_ALIASES = [
|
||||
'药方主治(文言文)',
|
||||
'药方主治(文言文)',
|
||||
'药方主治原文',
|
||||
'主治文言文',
|
||||
'文言文',
|
||||
'indication_original',
|
||||
];
|
||||
const IND_TR_ALIASES = [
|
||||
'现代主治(白话文)',
|
||||
'现代主治(白话文)',
|
||||
'现代主治',
|
||||
'白话文',
|
||||
'药方主治(译文)',
|
||||
'药方主治(译文)',
|
||||
'药方主治译文',
|
||||
'主治译文',
|
||||
'indication_translation',
|
||||
];
|
||||
const PY_ABBR_ALIASES = ['拼音首拼', '药方拼音首拼', '首拼', 'pinyin_initials'];
|
||||
const PY_FULL_ALIASES = ['拼音全拼', '药方拼音全拼', '全拼', 'pinyin'];
|
||||
const STATUS_ALIASES = ['状态', 'status'];
|
||||
const SORT_ALIASES = ['排序', 'sort'];
|
||||
|
||||
@@ -129,16 +142,14 @@ export async function parseGoldenFormulaExcelBuffer(
|
||||
if (!nameCol) {
|
||||
throw new Error('模板表头缺少「药方名字」列,请重新下载模板');
|
||||
}
|
||||
const clauseCol = pickColumn(headerMap, CLAUSE_ALIASES);
|
||||
const drugsCol = pickColumn(headerMap, DRUGS_ALIASES);
|
||||
const herbCol = pickColumn(headerMap, HERB_ALIASES);
|
||||
const originalCol = pickColumn(headerMap, ORIGINAL_ALIASES);
|
||||
const indOriCol = pickColumn(headerMap, IND_ORI_ALIASES);
|
||||
const indTrCol = pickColumn(headerMap, IND_TR_ALIASES);
|
||||
const introCol = pickColumn(headerMap, INTRO_ALIASES);
|
||||
const coverCol = pickColumn(headerMap, COVER_ALIASES);
|
||||
const pyAbbrCol = pickColumn(headerMap, PY_ABBR_ALIASES);
|
||||
const pyFullCol = pickColumn(headerMap, PY_FULL_ALIASES);
|
||||
const sourceCol = pickColumn(headerMap, SOURCE_ALIASES);
|
||||
const statusCol = pickColumn(headerMap, STATUS_ALIASES);
|
||||
const sortCol = pickColumn(headerMap, SORT_ALIASES);
|
||||
const items: ParsedGoldenFormulaRow[] = [];
|
||||
@@ -160,16 +171,14 @@ export async function parseGoldenFormulaExcelBuffer(
|
||||
const statusRaw = getCellInt(row, statusCol, 1);
|
||||
items.push({
|
||||
name,
|
||||
clause_number: getCellText(row, clauseCol),
|
||||
drugs_json: getCellText(row, drugsCol),
|
||||
herb_overview: getCellText(row, herbCol),
|
||||
original_formula: getCellText(row, originalCol),
|
||||
indication_original: getCellText(row, indOriCol),
|
||||
indication_translation: getCellText(row, indTrCol),
|
||||
intro: getCellText(row, introCol),
|
||||
cover: getCellText(row, coverCol),
|
||||
pinyin_initials: getCellText(row, pyAbbrCol),
|
||||
pinyin: getCellText(row, pyFullCol),
|
||||
source: getCellText(row, sourceCol),
|
||||
status: statusRaw === 0 ? 0 : 1,
|
||||
sort: getCellInt(row, sortCol, 0),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user