feat: 金方相关
This commit is contained in:
@@ -1168,9 +1168,13 @@ function openCommonPrescriptionModal() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开金方导入(VIP:golden_formula)
|
||||
* 打开金方导入(VIP:golden_formula);仅中药处方可用
|
||||
*/
|
||||
function openGoldenFormulaModal() {
|
||||
if (Number(prescriptionStore.activeCategory) !== 1) {
|
||||
message.warning('金方仅支持中药处方');
|
||||
return;
|
||||
}
|
||||
if (!canUseGoldenFormula.value) {
|
||||
message.warning('当前门店未开通金方 VIP 功能');
|
||||
return;
|
||||
@@ -1590,7 +1594,7 @@ const cancelSaveCommonPrescription = () => {
|
||||
选择常用方
|
||||
</Button>
|
||||
<Button
|
||||
v-if="canUseGoldenFormula"
|
||||
v-if="prescriptionStore.activeCategory === 1 && canUseGoldenFormula"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openGoldenFormulaModal"
|
||||
|
||||
@@ -499,16 +499,24 @@ export async function aiSavePrescriptionDrugSelectionApi(data: {
|
||||
);
|
||||
}
|
||||
|
||||
/** 金方列表(VIP:golden_formula) */
|
||||
/** 金方列表(VIP:golden_formula);支持 source_id 筛选 */
|
||||
export async function goldenFormulaListApi(data: {
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
store_id?: number;
|
||||
source_id?: number;
|
||||
}) {
|
||||
return requestClient.get<any>(`${prefix}golden-formula-list`, { params: data });
|
||||
}
|
||||
|
||||
/** 金方来源下拉(导入弹窗筛选用) */
|
||||
export async function goldenFormulaSourceOptionApi(data?: { store_id?: number }) {
|
||||
return requestClient.get<any>(`${prefix}golden-formula-source-option`, {
|
||||
params: data || {},
|
||||
});
|
||||
}
|
||||
|
||||
/** 金方详情 */
|
||||
export async function goldenFormulaDetailApi(data: {
|
||||
id: number;
|
||||
@@ -526,3 +534,16 @@ export async function goldenFormulaApplyApi(data: {
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}golden-formula-apply`, data);
|
||||
}
|
||||
|
||||
/** 保存医生金方匹配选择(改选/确认导入) */
|
||||
export async function goldenFormulaSaveMatchApi(data: {
|
||||
id: number;
|
||||
store_id?: number;
|
||||
items: Array<{
|
||||
drug_name: string;
|
||||
selected_drug_id: number;
|
||||
selected_drug_name?: string;
|
||||
}>;
|
||||
}) {
|
||||
return requestClient.post<any>(`${prefix}golden-formula-save-match`, data);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* 金方导入弹窗(PC 接诊 / 在线复诊共用)
|
||||
* - VIP:golden_formula
|
||||
* - 支持来源筛选;异常方标记不可选;方名后展示绑定来源
|
||||
* - 选方后调用药名对照,确认导入时抛出与 AI 出方相同结构的 rows
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
Select,
|
||||
Spin,
|
||||
Tag,
|
||||
message,
|
||||
@@ -20,6 +22,8 @@ import {
|
||||
import {
|
||||
goldenFormulaApplyApi,
|
||||
goldenFormulaListApi,
|
||||
goldenFormulaSaveMatchApi,
|
||||
goldenFormulaSourceOptionApi,
|
||||
} from '../api';
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -35,6 +39,9 @@ const emit = defineEmits<{
|
||||
const loading = ref(false);
|
||||
const applying = ref(false);
|
||||
const keyword = ref('');
|
||||
/** 金方来源筛选(0=全部) */
|
||||
const sourceId = ref<number | undefined>(undefined);
|
||||
const sourceOptions = ref<Array<{ label: string; value: number }>>([]);
|
||||
const list = ref<any[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
@@ -57,6 +64,7 @@ const [Modal, modalApi] = useVbenModal({
|
||||
onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
keyword.value = '';
|
||||
sourceId.value = undefined;
|
||||
list.value = [];
|
||||
matchResult.value = null;
|
||||
selectedId.value = 0;
|
||||
@@ -73,10 +81,29 @@ const [Modal, modalApi] = useVbenModal({
|
||||
// activeCategory 1=中药 2=西药,与 ProductTypeEnum 一致
|
||||
prescriptionType.value = Number(data.category || 1) === 2 ? 2 : 1;
|
||||
page.value = 1;
|
||||
loadSources();
|
||||
loadList();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载金方来源下拉,供顶部筛选
|
||||
*/
|
||||
async function loadSources() {
|
||||
try {
|
||||
const res = await goldenFormulaSourceOptionApi({
|
||||
store_id: storeId.value || undefined,
|
||||
});
|
||||
const rows = Array.isArray(res) ? res : res?.items || [];
|
||||
sourceOptions.value = rows.map((r: any) => ({
|
||||
label: r.name,
|
||||
value: Number(r.id),
|
||||
}));
|
||||
} catch {
|
||||
sourceOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载金方列表 */
|
||||
async function loadList() {
|
||||
loading.value = true;
|
||||
@@ -88,6 +115,7 @@ async function loadList() {
|
||||
page: page.value,
|
||||
pageSize,
|
||||
store_id: storeId.value || undefined,
|
||||
source_id: sourceId.value || undefined,
|
||||
});
|
||||
list.value = res?.items || [];
|
||||
total.value = Number(res?.total || 0);
|
||||
@@ -103,10 +131,34 @@ function onSearch() {
|
||||
loadList();
|
||||
}
|
||||
|
||||
/** 切换来源筛选后回到第一页重载 */
|
||||
function onSourceChange() {
|
||||
page.value = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中金方并做药名对照
|
||||
* 列表展示名:方名(来源);无来源则只显示方名
|
||||
*/
|
||||
function displayName(item: any): string {
|
||||
const name = item?.name || '';
|
||||
const source = item?.source_name || item?.source || '';
|
||||
return source ? `${name}(${source})` : name;
|
||||
}
|
||||
|
||||
/** 是否异常方(不可选) */
|
||||
function isAbnormal(item: any): boolean {
|
||||
return Number(item?.is_abnormal ?? 0) === 1 || Number(item?.selectable ?? 1) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中金方并做药名对照;异常方直接提示不可选
|
||||
*/
|
||||
async function handleSelect(row: any) {
|
||||
if (isAbnormal(row)) {
|
||||
message.warning('该金方为异常方,无法选择导入');
|
||||
return;
|
||||
}
|
||||
selectedId.value = Number(row.id);
|
||||
applying.value = true;
|
||||
matchResult.value = null;
|
||||
@@ -117,7 +169,8 @@ async function handleSelect(row: any) {
|
||||
register_id: registerId.value || undefined,
|
||||
prescription_type: prescriptionType.value,
|
||||
});
|
||||
matchResult.value = res;
|
||||
// 规范化:保证每行有 candidates + selected_drug_id,便于点选切换
|
||||
matchResult.value = normalizeMatchPayload(res);
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '对照失败');
|
||||
} finally {
|
||||
@@ -126,27 +179,121 @@ async function handleSelect(row: any) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认导入:把 matched 转成 AI 导入同构 rows(候选默认取第一项)
|
||||
* 规范化对照结果:缺省 selected_drug_id 时取候选第一项
|
||||
*/
|
||||
function handleConfirmImport() {
|
||||
function normalizeMatchPayload(payload: any) {
|
||||
if (!payload) return null;
|
||||
const matched = (payload.matched || []).map((m: any) => {
|
||||
const candidates = Array.isArray(m.candidates) ? m.candidates : [];
|
||||
let sid = Number(m.selected_drug_id || 0);
|
||||
if (!sid && candidates.length) {
|
||||
sid = Number(candidates[0].drug_id || 0);
|
||||
}
|
||||
return { ...m, candidates, selected_drug_id: sid };
|
||||
});
|
||||
return { ...payload, matched };
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装当前对照行的匹配记忆 items,供落库
|
||||
*/
|
||||
function buildMatchSaveItems(matched: any[]) {
|
||||
return matched
|
||||
.map((m: any) => {
|
||||
const candidates = m.candidates || [];
|
||||
const sid = Number(m.selected_drug_id || 0);
|
||||
const picked =
|
||||
candidates.find((c: any) => Number(c.drug_id) === sid) ||
|
||||
candidates[0] ||
|
||||
{};
|
||||
const drugId = Number(picked.drug_id || sid || 0);
|
||||
const drugName = String(m.ai_name || '').trim();
|
||||
if (!drugName || !drugId) return null;
|
||||
return {
|
||||
drug_name: drugName,
|
||||
selected_drug_id: drugId,
|
||||
selected_drug_name: String(picked.drug_name || ''),
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as Array<{
|
||||
drug_name: string;
|
||||
selected_drug_id: number;
|
||||
selected_drug_name?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化医生匹配记忆(失败不阻断导入)
|
||||
*/
|
||||
async function persistDoctorMatches(items: Array<{
|
||||
drug_name: string;
|
||||
selected_drug_id: number;
|
||||
selected_drug_name?: string;
|
||||
}>) {
|
||||
if (!selectedId.value || !items.length) return;
|
||||
try {
|
||||
await goldenFormulaSaveMatchApi({
|
||||
id: selectedId.value,
|
||||
store_id: storeId.value || undefined,
|
||||
items,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('金方匹配记忆保存失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击候选药品:更新该行 selected_drug_id,并立即落库记忆
|
||||
*/
|
||||
async function onPickCandidate(mi: number, candidate: any) {
|
||||
const list = matchResult.value?.matched;
|
||||
if (!list || !list[mi] || !candidate) return;
|
||||
const drugId = Number(candidate.drug_id || 0);
|
||||
if (!drugId) return;
|
||||
if (Number(list[mi].selected_drug_id) === drugId) return;
|
||||
list[mi].selected_drug_id = drugId;
|
||||
const drugName = String(list[mi].ai_name || '').trim();
|
||||
if (!drugName) return;
|
||||
await persistDoctorMatches([
|
||||
{
|
||||
drug_name: drugName,
|
||||
selected_drug_id: drugId,
|
||||
selected_drug_name: String(candidate.drug_name || ''),
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认导入:把 matched 转成 AI 导入同构 rows(按当前选中候选)
|
||||
* 剂量:直接用后端 apply 已处理好的 m.dose(有小数已进位到整数克),禁止前端再改
|
||||
*/
|
||||
async function handleConfirmImport() {
|
||||
const matched = matchResult.value?.matched || [];
|
||||
if (!matched.length) {
|
||||
message.warning('没有可导入的已匹配药品');
|
||||
return;
|
||||
}
|
||||
// 确认导入前再整批落库,保证下次自动选中
|
||||
await persistDoctorMatches(buildMatchSaveItems(matched));
|
||||
const rows = matched.map((m: any) => {
|
||||
const candidates = m.candidates || [];
|
||||
const selectedId = Number(m.selected_drug_id || 0);
|
||||
const selectedDrugId = Number(m.selected_drug_id || 0);
|
||||
const candidate =
|
||||
candidates.find((c: any) => Number(c.drug_id) === selectedId) ||
|
||||
candidates.find((c: any) => Number(c.drug_id) === selectedDrugId) ||
|
||||
candidates[0] ||
|
||||
{};
|
||||
return {
|
||||
ai_name: m.ai_name,
|
||||
dose: m.dose,
|
||||
unit: m.unit,
|
||||
usage: m.usage,
|
||||
frequency: m.frequency,
|
||||
// 以下 || '' 兜底对齐 AI 出方协议(AiPrescriptionDrawer.vue onConfirmImport),
|
||||
// 避免 undefined 进入 handleImportAiPrescription 后:
|
||||
// - dose 为 undefined → parseFloat NaN → 回退 1,丢失真实剂量
|
||||
// - unit 为 undefined → 中药卡片单位显示异常
|
||||
// - usage 为 undefined → wayId 解析失败
|
||||
ai_name: m.ai_name || '',
|
||||
// 后端 enrich 已对原始剂量做小数进位(如 46.88→47)
|
||||
dose: m.dose || '',
|
||||
unit: m.unit || '',
|
||||
usage: m.usage || '',
|
||||
frequency: m.frequency || '',
|
||||
candidate,
|
||||
};
|
||||
});
|
||||
@@ -173,14 +320,23 @@ defineExpose({
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal title="导入金方">
|
||||
<div class="flex gap-3" style="min-height: 420px">
|
||||
<div class="w-[340px] shrink-0 border-r pr-3">
|
||||
<Modal title="导入金方" class="w-[50%] h-[80%]">
|
||||
<!-- 颜色一律用主题变量,禁止写死亮色底(暗色模式可读) -->
|
||||
<div class="gf-modal flex gap-3 text-foreground" style="min-height: 420px">
|
||||
<div class="w-[340px] shrink-0 border-r border-border pr-3">
|
||||
<div class="mb-2 flex gap-2">
|
||||
<Select
|
||||
v-model:value="sourceId"
|
||||
allow-clear
|
||||
class="w-[120px] shrink-0"
|
||||
placeholder="来源"
|
||||
:options="sourceOptions"
|
||||
@change="onSourceChange"
|
||||
/>
|
||||
<Input
|
||||
v-model:value="keyword"
|
||||
allow-clear
|
||||
placeholder="名称/首拼/来源/速览"
|
||||
placeholder="名称/首拼/全拼/来源/速览"
|
||||
@press-enter="onSearch"
|
||||
/>
|
||||
<Button type="primary" @click="onSearch">搜索</Button>
|
||||
@@ -189,24 +345,31 @@ defineExpose({
|
||||
<div v-if="!list.length" class="py-8">
|
||||
<Empty description="暂无金方" />
|
||||
</div>
|
||||
<div v-else class="max-h-[360px] space-y-2 overflow-y-auto">
|
||||
<div v-else class="max-h-[550px] space-y-2 overflow-y-auto">
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="cursor-pointer rounded border p-2 transition hover:border-primary"
|
||||
:class="selectedId === item.id ? 'border-primary bg-primary/5' : ''"
|
||||
class="gf-item rounded border border-border bg-card p-2 transition"
|
||||
:class="{
|
||||
'gf-item--active': selectedId === item.id,
|
||||
'gf-item--disabled': isAbnormal(item),
|
||||
'cursor-pointer hover:border-primary': !isAbnormal(item),
|
||||
}"
|
||||
@click="handleSelect(item)"
|
||||
>
|
||||
<div class="font-medium">{{ item.name }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500 line-clamp-2">
|
||||
{{ item.herb_overview || '—' }}
|
||||
<div class="font-medium text-foreground">
|
||||
{{ displayName(item) }}
|
||||
<Tag v-if="isAbnormal(item)" color="error" class="ml-1">异常</Tag>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap gap-1">
|
||||
<Tag v-if="item.source" color="gold">{{ item.source }}</Tag>
|
||||
<div class="mt-1 line-clamp-2 text-xs text-muted-foreground">
|
||||
{{ item.herb_overview || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="total > pageSize" class="mt-2 text-center text-xs text-gray-400">
|
||||
<div
|
||||
v-if="total > pageSize"
|
||||
class="mt-2 text-center text-xs text-muted-foreground"
|
||||
>
|
||||
共 {{ total }} 条(当前第 {{ page }} 页)
|
||||
<Button
|
||||
v-if="page * pageSize < total"
|
||||
@@ -225,15 +388,16 @@ defineExpose({
|
||||
<Empty description="请选择左侧金方进行药名对照" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="mb-2 text-base font-medium">
|
||||
{{ matchResult.formula?.name }}
|
||||
<Tag v-if="matchResult.formula?.source" class="ml-2" color="gold">
|
||||
{{ matchResult.formula.source }}
|
||||
</Tag>
|
||||
<div class="mb-2 text-base font-medium text-foreground">
|
||||
{{
|
||||
matchResult.formula?.source_name || matchResult.formula?.source
|
||||
? `${matchResult.formula?.name || ''}(${matchResult.formula.source_name || matchResult.formula.source})`
|
||||
: matchResult.formula?.name
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="matchResult.formula?.indication_translation || matchResult.formula?.indication_original"
|
||||
class="mb-2 text-xs text-gray-500"
|
||||
class="mb-2 text-xs text-muted-foreground"
|
||||
>
|
||||
主治:
|
||||
{{
|
||||
@@ -243,31 +407,57 @@ defineExpose({
|
||||
</div>
|
||||
<div
|
||||
v-if="matchResult.formula?.original_formula"
|
||||
class="mb-2 max-h-24 overflow-y-auto rounded bg-amber-50 px-2 py-1 text-xs text-gray-700 whitespace-pre-wrap"
|
||||
class="gf-original mb-2 max-h-24 overflow-y-auto rounded px-2 py-1 text-xs whitespace-pre-wrap"
|
||||
>
|
||||
<span class="font-medium text-amber-700">原方:</span>
|
||||
<span class="gf-original__label font-medium">原方:</span>
|
||||
{{ matchResult.formula.original_formula }}
|
||||
</div>
|
||||
<div class="mb-2 text-sm">
|
||||
<div class="mb-2 text-sm text-foreground">
|
||||
已匹配
|
||||
<Tag color="success">{{ (matchResult.matched || []).length }}</Tag>
|
||||
未匹配
|
||||
<Tag color="warning">{{ (matchResult.unmatched || []).length }}</Tag>
|
||||
</div>
|
||||
<div class="max-h-[280px] space-y-1 overflow-y-auto text-sm">
|
||||
<div class="max-h-[280px] space-y-2 overflow-y-auto text-sm">
|
||||
<!-- 与 AI 出方一致:一行一药 + 全部候选 Tag 可点选 -->
|
||||
<div
|
||||
v-for="(m, idx) in matchResult.matched || []"
|
||||
:key="'m' + idx"
|
||||
class="rounded bg-green-50 px-2 py-1"
|
||||
v-for="(m, mi) in matchResult.matched || []"
|
||||
:key="'m' + mi"
|
||||
class="gf-matched rounded px-2.5 py-2"
|
||||
>
|
||||
{{ m.show_dose || m.display_text || `${m.ai_name} ${m.dose || ''}${m.unit || ''}${m.usage ? `(${m.usage})` : ''}` }}
|
||||
→
|
||||
{{ (m.candidates && m.candidates[0] && m.candidates[0].drug_name) || '—' }}
|
||||
<div class="font-medium text-foreground">
|
||||
{{
|
||||
m.show_dose ||
|
||||
m.display_text ||
|
||||
`${m.ai_name} ${m.dose || ''}${m.unit || ''}${m.usage ? `(${m.usage})` : ''}`
|
||||
}}
|
||||
</div>
|
||||
<div class="mt-1.5 flex flex-wrap gap-1">
|
||||
<Tag
|
||||
v-for="c in m.candidates || []"
|
||||
:key="c.drug_id"
|
||||
class="cursor-pointer !m-0 !text-[11px]"
|
||||
:color="
|
||||
Number(m.selected_drug_id) === Number(c.drug_id)
|
||||
? 'success'
|
||||
: undefined
|
||||
"
|
||||
@click="onPickCandidate(mi, c)"
|
||||
>
|
||||
{{ c.drug_name }}
|
||||
</Tag>
|
||||
<span
|
||||
v-if="!(m.candidates || []).length"
|
||||
class="text-xs text-muted-foreground"
|
||||
>
|
||||
无候选
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(u, idx) in matchResult.unmatched || []"
|
||||
:key="'u' + idx"
|
||||
class="rounded bg-orange-50 px-2 py-1"
|
||||
class="gf-unmatched rounded px-2 py-1"
|
||||
>
|
||||
{{ u.show_dose || u.display_text || `${u.ai_name} ${u.dose || ''}${u.unit || ''}` }}
|
||||
(未匹配)
|
||||
@@ -288,3 +478,37 @@ defineExpose({
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<style scoped>
|
||||
/* 选中项:primary 透明度,暗色自动跟主题 */
|
||||
.gf-item--active {
|
||||
border-color: hsl(var(--primary)) !important;
|
||||
background-color: hsl(var(--primary) / 0.1);
|
||||
}
|
||||
/* 异常方:灰显 + 禁止指针,表达不可选 */
|
||||
.gf-item--disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
background-color: hsl(var(--muted) / 0.35);
|
||||
}
|
||||
/* 原方展示区:用 warning 变量,禁止 bg-amber-50 / text-amber-700 */
|
||||
.gf-original {
|
||||
background-color: hsl(var(--warning) / 0.12);
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--warning) / 0.35);
|
||||
}
|
||||
.gf-original__label {
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
/* 已匹配:success/primary 浅绿底;暗色下半透明 */
|
||||
.gf-matched {
|
||||
background-color: hsl(var(--primary) / 0.12);
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--primary) / 0.25);
|
||||
}
|
||||
/* 未匹配:warning 橙底 */
|
||||
.gf-unmatched {
|
||||
background-color: hsl(var(--warning) / 0.12);
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--warning) / 0.35);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1337,9 +1337,13 @@ function openCommonPrescriptionModal() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开金方导入弹窗(VIP:golden_formula)
|
||||
* 打开金方导入弹窗(VIP:golden_formula);仅中药处方可用
|
||||
*/
|
||||
function openGoldenFormulaModal() {
|
||||
if (Number(activeCategory.value) !== 1) {
|
||||
message.warning('金方仅支持中药处方');
|
||||
return;
|
||||
}
|
||||
if (!canUseGoldenFormula.value) {
|
||||
message.warning('当前门店未开通金方 VIP 功能');
|
||||
return;
|
||||
@@ -3330,7 +3334,7 @@ function onStoreSelectOpenChange(open: boolean) {
|
||||
选择常用方
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!isSpecialPrescriptionCartLocked && canUseGoldenFormula"
|
||||
v-if="activeCategory === 1 && !isSpecialPrescriptionCartLocked && canUseGoldenFormula"
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openGoldenFormulaModal"
|
||||
|
||||
@@ -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