开方时间:{{ row.created_at || '—' }}
diff --git a/apps/web-antd/src/views/doctor/doctor-reception/config/health-history.ts b/apps/web-antd/src/views/doctor/doctor-reception/config/health-history.ts
new file mode 100644
index 00000000..823c2ea1
--- /dev/null
+++ b/apps/web-antd/src/views/doctor/doctor-reception/config/health-history.ts
@@ -0,0 +1,89 @@
+/**
+ * 就诊人健康信息预设标签(与患者小程序 myinfo-add 保持一致)
+ * 提交时 history 为字符串数组,后端 normalizeHistoryField 会用逗号拼接入库
+ */
+
+/** 肝肾功能:0 正常 / 1 异常 */
+export const LIVER_RENAL_OPTIONS = [
+ { label: '正常', value: 0 },
+ { label: '异常', value: 1 },
+];
+
+/** 各类史有无:0 无 / 1 有 */
+export const HISTORY_STATUS_OPTIONS = [
+ { label: '无', value: 0 },
+ { label: '有', value: 1 },
+];
+
+/** 过敏史预设 */
+export const ALLERGIC_HISTORY_OPTIONS = [
+ '鱼虾 海鲜产品',
+ '尘螨',
+ '花粉',
+ '动物皮屑',
+ '奶制品',
+ '鸡蛋',
+ '阿司匹林',
+].map((name) => ({ label: name, value: name }));
+
+/** 现病史预设 */
+export const PRESENT_HISTORY_OPTIONS = [
+ '起病急',
+ '病程迁延',
+ '反复发作',
+ '自行用药',
+ '症状加重',
+].map((name) => ({ label: name, value: name }));
+
+/** 既往史预设 */
+export const PERSON_HISTORY_OPTIONS = [
+ '先天性心脏病',
+ '肿瘤',
+ '动脉硬化',
+ '冠心病',
+ '糖尿病',
+ '脑血管病',
+ '哮喘',
+].map((name) => ({ label: name, value: name }));
+
+/** 家族史预设 */
+export const FAMILY_HISTORY_OPTIONS = [
+ '高血压',
+ '心脏病',
+ '糖尿病',
+ '血脂异常',
+ '白癜风',
+ '癫痫',
+ '哮喘',
+ '近视',
+ '肝炎',
+ '结核病',
+].map((name) => ({ label: name, value: name }));
+
+/** 流行病学史预设 */
+export const EPIDEMIC_HISTORY_OPTIONS = [
+ '疫区旅居',
+ '确诊病例接触',
+ '发热病例接触',
+].map((name) => ({ label: name, value: name }));
+
+/** 个人史预设 */
+export const PERSONAL_HISTORY_OPTIONS = [
+ '吸烟',
+ '饮酒',
+ '熬夜',
+].map((name) => ({ label: name, value: name }));
+
+/** 月经史预设(仅女性) */
+export const MENSTRUAL_HISTORY_OPTIONS = [
+ '月经规律',
+ '痛经',
+ '月经不调',
+].map((name) => ({ label: name, value: name }));
+
+/** 婚育史预设(男>22 或 女>20) */
+export const MARITAL_HISTORY_OPTIONS = [
+ '未婚',
+ '已婚',
+ '已育',
+].map((name) => ({ label: name, value: name }));
diff --git a/apps/web-antd/src/views/doctor/doctor-reception/index.vue b/apps/web-antd/src/views/doctor/doctor-reception/index.vue
index 939fdbe8..a37c0f1d 100644
--- a/apps/web-antd/src/views/doctor/doctor-reception/index.vue
+++ b/apps/web-antd/src/views/doctor/doctor-reception/index.vue
@@ -1387,10 +1387,47 @@ function openCommonPrescriptionModal() {
CommonPrescriptionModalApi.open();
}
-/** 打开新建就诊人弹窗 */
+/**
+ * 打开新建就诊人弹窗
+ * 成功后按 is_pay 分情况:已支付 → 切当前列表并自动选中;未支付 → 弹认领码
+ */
function openCreateUserPatientModal() {
CreateUserPatientModalApi.setData({
- onSuccess: () => refreshPatientList(),
+ onSuccess: (res: {
+ user_patient_id?: number;
+ register_id?: number;
+ is_pay?: number;
+ }) => {
+ const isPay = Number(res?.is_pay || 0) === 1;
+ const registerId = Number(res?.register_id || 0);
+ const userPatientId = Number(res?.user_patient_id || 0);
+ if (isPay) {
+ // 新人 status=1,落在「当前」列表
+ listType.value = 1;
+ getPatientList({ type: listType.value })
+ .then((list) => {
+ patients.value = Array.isArray(list) ? list : [];
+ const hit = patients.value.find(
+ (p) => Number(p.id) === registerId,
+ );
+ if (hit) {
+ selectPatient(hit);
+ } else {
+ message.warning('已创建,请在列表中手动选择');
+ }
+ })
+ .catch((e: any) => {
+ message.error(e?.message || '刷新患者列表失败');
+ });
+ return;
+ }
+ // 未支付进不了左侧列表:直接弹认领码并选中刚创建的就诊人
+ ClaimQrcodeModalApi.setData({
+ user_patient_id: userPatientId || undefined,
+ });
+ ClaimQrcodeModalApi.open();
+ refreshPatientList();
+ },
});
CreateUserPatientModalApi.open();
}
diff --git a/apps/web-antd/src/views/system/golden-formula/api/index.ts b/apps/web-antd/src/views/system/golden-formula/api/index.ts
index 04e9bb8c..e2e6f433 100644
--- a/apps/web-antd/src/views/system/golden-formula/api/index.ts
+++ b/apps/web-antd/src/views/system/golden-formula/api/index.ts
@@ -170,6 +170,12 @@ export async function getFormulasByDrugName(drugName: string) {
has_unmatched: number;
status: number;
drug_count: number;
+ /** 命中味克数(拆分后每味继承) */
+ match_dose: string;
+ match_unit: string;
+ match_ancient_dose: string;
+ /** 展示用:如 9g(三两) */
+ match_dose_text: string;
}[]
>(`${missingPrefix}formulas-by-drug-name`, {
params: { drug_name: drugName },
@@ -177,20 +183,24 @@ export async function getFormulasByDrugName(drugName: string) {
}
/**
- * 替换为药材表已有药材:金方 name 改为目标药名,旧名保留 origin_name 并挂别名
+ * 替换为药材表已有药材(支持 1→N 拆分)
* 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[];
+ /** 目标药材列表;多味时把错误合并名拆成多行 */
+ targets: Array<{ target_drug_id: number; dose?: string }>;
+ /** 兼容旧单目标(可选) */
+ target_drug_id?: number;
}) {
return requestClient.post<{
drug_id: number;
old_name: string;
new_name: string;
+ new_names: string[];
formula_count: number;
refreshed: number;
}>(`${missingPrefix}replace-with-drug`, data);
diff --git a/apps/web-antd/src/views/system/golden-formula/components/ResolveMissingDrugPanel.vue b/apps/web-antd/src/views/system/golden-formula/components/ResolveMissingDrugPanel.vue
index cecd599b..3b9257da 100644
--- a/apps/web-antd/src/views/system/golden-formula/components/ResolveMissingDrugPanel.vue
+++ b/apps/web-antd/src/views/system/golden-formula/components/ResolveMissingDrugPanel.vue
@@ -3,7 +3,7 @@
* 缺失药处理面板
* - 添加药材:打开中药管理「新增中药」模态框
* - 挂别名:ChineseDrugNameBubble 选目标药
- * - 替换已有:选目标药 → 预览勾选含同名药的金方 → 确认
+ * - 替换已有:可 1→N(错误合并名拆多味)→ 预览勾选金方 → 确认
*/
import { computed, ref, watch } from 'vue';
@@ -25,6 +25,13 @@ import {
resolveMissingByName,
} from '../api';
+/** 替换目标行:只选药名,克数各方继承原味(不在此填写) */
+interface ReplaceTargetRow {
+ key: string;
+ displayName: string;
+ drugId: number;
+}
+
const props = defineProps<{
open: boolean;
missingId?: number;
@@ -46,26 +53,71 @@ const submitting = ref(false);
const selectedDrug = ref
(null);
const selectedDisplayName = ref('');
+/** 替换目标列表(可多味) */
+const replaceTargets = ref([]);
+/** 当前正在搜索选择的那一行 */
+const activeReplaceKey = ref('');
+
/** 替换预览:含该药名的金方 */
const previewLoading = ref(false);
const previewFormulas = ref([]);
const selectedFormulaIds = ref([]);
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 },
+ { title: 'ID', dataIndex: 'id', key: 'id', width: 60 },
+ { title: '药方名字', dataIndex: 'name', key: 'name', width: 120, ellipsis: true },
+ { title: '条文', dataIndex: 'clause_number', key: 'clause_number', width: 70 },
+ { title: '原克数', dataIndex: 'match_dose_text', key: 'match_dose_text', width: 110 },
+ { title: '拆分后预览', key: 'split_preview', ellipsis: true },
+ { title: '标记', key: 'flags', width: 110 },
];
const selectedFormulaCount = computed(() => selectedFormulaIds.value.length);
+const validReplaceTargets = computed(() =>
+ replaceTargets.value.filter((r) => r.drugId > 0 && r.displayName),
+);
+const replaceTargetNamesText = computed(() =>
+ validReplaceTargets.value.map((r) => r.displayName).join('、'),
+);
+
+/**
+ * 按该方原味克数生成拆分预览文案(各方克数可能不同,统一继承本方原味)
+ */
+function buildSplitPreview(record: {
+ match_dose_text?: string;
+ match_dose?: string;
+ match_unit?: string;
+ match_ancient_dose?: string;
+}): string {
+ const names = validReplaceTargets.value.map((r) => r.displayName);
+ if (!names.length) return '请先选择目标药材';
+ const doseText =
+ String(record?.match_dose_text || '').trim() ||
+ (() => {
+ const d = String(record?.match_dose || '').trim();
+ const u = String(record?.match_unit || 'g').trim() || 'g';
+ const a = String(record?.match_ancient_dose || '').trim();
+ let t = d ? `${d}${u}` : '';
+ if (a) t = t ? `${t}(${a})` : a;
+ return t;
+ })();
+ return names
+ .map((n) => (doseText ? `${n} ${doseText}` : n))
+ .join('、');
+}
const [ChinaMedicineModal, chinaMedicineModalApi] = useVbenModal({
connectedComponent: ChinaMedicineFormModal,
});
+function newReplaceRow(): ReplaceTargetRow {
+ return {
+ key: `rt-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
+ displayName: '',
+ drugId: 0,
+ };
+}
+
watch(
() => props.open,
(v) => {
@@ -73,6 +125,8 @@ watch(
step.value = 'choose';
selectedDrug.value = null;
selectedDisplayName.value = '';
+ replaceTargets.value = [newReplaceRow()];
+ activeReplaceKey.value = replaceTargets.value[0]?.key || '';
previewFormulas.value = [];
selectedFormulaIds.value = [];
}
@@ -145,6 +199,57 @@ function resolveTargetDrugId(): number {
return Number(item.drug_id || item.drug?.id || 0);
}
+/**
+ * 替换行选中药材(必须带 key,避免多行时写错位)
+ * 克数不在此填写:确认后由后端按各方原味 dose 继承
+ */
+function onSelectReplaceTarget(key: string, item: ChineseDrugBubbleItem) {
+ activeReplaceKey.value = key;
+ const row = replaceTargets.value.find((r) => r.key === key);
+ if (!row) return;
+ const drugId = Number(item.drug_id || item.drug?.id || 0);
+ const displayName = String(
+ item?.drug?.drug_name || item?.drug_name || '',
+ ).trim();
+ if (!drugId || !displayName) {
+ message.warning('未识别到有效药材');
+ return;
+ }
+ // 同一列表禁止重复选同一味
+ const dup = replaceTargets.value.some(
+ (r) => r.key !== key && r.drugId === drugId,
+ );
+ if (dup) {
+ message.warning(`「${displayName}」已在目标列表中`);
+ return;
+ }
+ row.drugId = drugId;
+ row.displayName = displayName;
+}
+
+function focusReplaceRow(key: string) {
+ activeReplaceKey.value = key;
+}
+
+function addReplaceRow() {
+ const row = newReplaceRow();
+ replaceTargets.value.push(row);
+ activeReplaceKey.value = row.key;
+}
+
+function removeReplaceRow(key: string) {
+ if (replaceTargets.value.length <= 1) {
+ // 至少保留一行空位
+ replaceTargets.value = [newReplaceRow()];
+ activeReplaceKey.value = replaceTargets.value[0]!.key;
+ return;
+ }
+ replaceTargets.value = replaceTargets.value.filter((r) => r.key !== key);
+ if (!replaceTargets.value.some((r) => r.key === activeReplaceKey.value)) {
+ activeReplaceKey.value = replaceTargets.value[0]?.key || '';
+ }
+}
+
/**
* 加载含该缺失药名的金方预览,并默认勾选
* - 有 formulaId:默认只勾当前方(可再加选)
@@ -162,11 +267,10 @@ async function loadFormulaPreview() {
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]]
+ ? [allIds[0]!]
: [];
} else {
selectedFormulaIds.value = allIds;
@@ -185,6 +289,8 @@ async function goPick(next: 'alias' | 'replace') {
selectedDrug.value = null;
selectedDisplayName.value = '';
if (next === 'replace') {
+ replaceTargets.value = [newReplaceRow()];
+ activeReplaceKey.value = replaceTargets.value[0]!.key;
await loadFormulaPreview();
} else {
previewFormulas.value = [];
@@ -229,12 +335,12 @@ async function confirmAlias() {
}
/**
- * 确认替换:只改预览勾选的金方
+ * 确认替换:支持多味拆分,只改预览勾选的金方
*/
async function confirmReplace() {
- const targetDrugId = resolveTargetDrugId();
- if (!targetDrugId) {
- message.warning('请先搜索并选择目标药材');
+ const targets = validReplaceTargets.value;
+ if (!targets.length) {
+ message.warning('请至少选择一味目标药材');
return;
}
if (!selectedFormulaIds.value.length) {
@@ -243,14 +349,23 @@ async function confirmReplace() {
}
submitting.value = true;
try {
+ // 不传 dose:后端按各方原味克数继承到拆出的每一味
const res = await replaceMissingWithDrug({
...buildBasePayload(),
- target_drug_id: targetDrugId,
formula_ids: selectedFormulaIds.value,
+ targets: targets.map((t) => ({
+ target_drug_id: t.drugId,
+ })),
});
- message.success(
- `已将「${res?.old_name || props.drugName}」替换为「${res?.new_name || selectedDisplayName.value}」(共 ${Number(res?.formula_count || 0)} 个金方,旧名已保留)`,
- );
+ const names =
+ Array.isArray(res?.new_names) && res.new_names.length
+ ? res.new_names.join('、')
+ : res?.new_name || replaceTargetNamesText.value;
+ const tip =
+ targets.length > 1
+ ? `已将「${res?.old_name || props.drugName}」拆分为「${names}」(共 ${Number(res?.formula_count || 0)} 个金方)`
+ : `已将「${res?.old_name || props.drugName}」替换为「${names}」(共 ${Number(res?.formula_count || 0)} 个金方,旧名已保留)`;
+ message.success(tip);
emit('done');
close();
} catch (e: any) {
@@ -267,7 +382,7 @@ async function confirmReplace() {
:title="`处理缺失药:${drugName || ''}`"
:footer="null"
destroy-on-close
- :width="step === 'replace' ? 860 : 520"
+ :width="step === 'replace' ? 960 : 520"
@cancel="close"
>
@@ -282,7 +397,7 @@ async function confirmReplace() {
放入药材别名表(挂到已有药,不改金方药名)
@@ -313,23 +428,42 @@ async function confirmReplace() {
- 1)搜索并选择目标药材;2)预览勾选要一起修改的金方;3)确认后把「{{ drugName }}」替换为目标药名(旧名写入
- origin_name 并挂别名)。
+ 1)添加目标药材(可多味);2)下方预览各方原克数,拆分后每味都继承该方原味克数(各方可能不同,无需手填);3)勾选金方后确认。
-
-
搜索中药
-
+
+
+
{{ idx + 1 }}
+
+ onSelectReplaceTarget(row.key, item)"
+ />
+
+
+
+
+
+ 将拆分为:{{ replaceTargetNamesText }}(共 {{ validReplaceTargets.length }} 味,克数各方继承原味)
+
-
- 已选目标:{{ selectedDisplayName }}
- (ID {{ resolveTargetDrugId() }})
-
受影响金方预览(已勾选 {{ selectedFormulaCount }} / {{ previewFormulas.length }})
@@ -357,7 +491,13 @@ async function confirmReplace() {
class="gf-preview-table"
>
-
+
+ {{ record.match_dose_text || '—' }}
+
+
+ {{ buildSplitPreview(record) }}
+
+
异常
未匹配
- 确认替换({{ selectedFormulaCount }} 个金方)
+ 确认{{ validReplaceTargets.length > 1 ? '拆分' : '替换' }}({{
+ selectedFormulaCount
+ }}
+ 个金方)
@@ -401,9 +544,11 @@ async function confirmReplace() {
font-size: 13px;
}
.gf-resolve-search :deep(.chinese-drug-name-bubble),
-.gf-resolve-search :deep(.chinese-drug-name-bubble--compact) {
+.gf-resolve-search :deep(.chinese-drug-name-bubble--compact),
+.gf-replace-search :deep(.chinese-drug-name-bubble),
+.gf-replace-search :deep(.chinese-drug-name-bubble--compact) {
width: 100% !important;
- min-width: 220px !important;
+ min-width: 180px !important;
max-width: none !important;
flex: 1 1 auto !important;
}
@@ -412,6 +557,43 @@ async function confirmReplace() {
color: hsl(var(--primary));
font-size: 13px;
}
+.gf-replace-targets {
+ margin-bottom: 16px;
+}
+.gf-replace-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 8px;
+ padding: 8px 10px;
+ border: 1px solid hsl(var(--border));
+ border-radius: 8px;
+ background: hsl(var(--card, var(--background)));
+}
+.gf-replace-row.is-active {
+ border-color: hsl(var(--primary) / 0.55);
+ background: hsl(var(--primary) / 0.06);
+}
+.gf-replace-idx {
+ flex-shrink: 0;
+ width: 20px;
+ color: hsl(var(--muted-foreground));
+ font-size: 12px;
+ text-align: center;
+}
+.gf-replace-search {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+.gf-replace-add {
+ margin-top: 4px;
+ margin-bottom: 8px;
+}
+.gf-split-preview {
+ color: hsl(var(--primary));
+ font-size: 12px;
+ line-height: 1.5;
+}
.gf-preview-toolbar {
display: flex;
align-items: center;
diff --git a/apps/web-antd/src/views/system/golden-formula/index.vue b/apps/web-antd/src/views/system/golden-formula/index.vue
index dd4443f9..d1c88e7d 100644
--- a/apps/web-antd/src/views/system/golden-formula/index.vue
+++ b/apps/web-antd/src/views/system/golden-formula/index.vue
@@ -272,6 +272,7 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
-
+
-
{{ opt.name }}
-
@@ -267,11 +270,21 @@ const [Modal, modalApi] = useVbenModal({
font-weight: 600;
}
.vip-level-card__name {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: baseline;
+ justify-content: center;
+ gap: 4px 6px;
font-size: 14px;
font-weight: 600;
color: hsl(var(--foreground));
text-align: center;
}
+.vip-level-card__code {
+ font-size: 12px;
+ font-weight: 500;
+ color: hsl(var(--primary));
+}
.vip-level-card__meta {
margin-top: 4px;
font-size: 11px;
diff --git a/apps/web-antd/src/views/system/vip/feature/index.vue b/apps/web-antd/src/views/system/vip/feature/index.vue
index 85af2c67..f3589ebc 100644
--- a/apps/web-antd/src/views/system/vip/feature/index.vue
+++ b/apps/web-antd/src/views/system/vip/feature/index.vue
@@ -81,6 +81,18 @@ const showModal = (data: any = {}, isUpdate = false) => {
formModalApi.open();
};
+/**
+ * 已绑等级展示文案:同名等级(如多档「黄金会员」)需同时带上 code(V1/V2…)才能区分
+ */
+function formatBoundLevelLabel(lv: Record