feat:
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
1. 创建就诊人功能增强 2. 优化交互细节
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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<ChineseDrugBubbleItem | null>(null);
|
||||
const selectedDisplayName = ref('');
|
||||
|
||||
/** 替换目标列表(可多味) */
|
||||
const replaceTargets = ref<ReplaceTargetRow[]>([]);
|
||||
/** 当前正在搜索选择的那一行 */
|
||||
const activeReplaceKey = 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 },
|
||||
{ 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"
|
||||
>
|
||||
<div v-if="step === 'choose'" class="gf-resolve-choose">
|
||||
@@ -282,7 +397,7 @@ async function confirmReplace() {
|
||||
放入药材别名表(挂到已有药,不改金方药名)
|
||||
</Button>
|
||||
<Button block @click="goPick('replace')">
|
||||
替换为已有药材(预览勾选金方后确认)
|
||||
替换为已有药材(可拆成多味,预览勾选金方后确认)
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -313,23 +428,42 @@ async function confirmReplace() {
|
||||
</div>
|
||||
<div v-else class="gf-resolve-replace">
|
||||
<p class="gf-resolve-tip">
|
||||
1)搜索并选择目标药材;2)预览勾选要一起修改的金方;3)确认后把「{{ drugName }}」替换为目标药名(旧名写入
|
||||
origin_name 并挂别名)。
|
||||
1)添加目标药材(可多味);2)下方预览各方原克数,拆分后每味都继承该方原味克数(各方可能不同,无需手填);3)勾选金方后确认。
|
||||
</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 class="gf-replace-targets">
|
||||
<div
|
||||
v-for="(row, idx) in replaceTargets"
|
||||
:key="row.key"
|
||||
class="gf-replace-row"
|
||||
:class="{ 'is-active': activeReplaceKey === row.key }"
|
||||
@click="focusReplaceRow(row.key)"
|
||||
>
|
||||
<span class="gf-replace-idx">{{ idx + 1 }}</span>
|
||||
<div class="gf-replace-search" @mousedown="focusReplaceRow(row.key)">
|
||||
<ChineseDrugNameBubble
|
||||
:display-name="row.displayName"
|
||||
:type="1"
|
||||
:store-id="searchStoreId"
|
||||
placeholder="搜索并选择目标药"
|
||||
@select="(item) => onSelectReplaceTarget(row.key, item)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
@click.stop="removeReplaceRow(row.key)"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="dashed" block class="gf-replace-add" @click="addReplaceRow">
|
||||
+ 再加一味目标药
|
||||
</Button>
|
||||
<p v-if="validReplaceTargets.length" class="gf-resolve-selected">
|
||||
将拆分为:{{ replaceTargetNamesText }}(共 {{ validReplaceTargets.length }} 味,克数各方继承原味)
|
||||
</p>
|
||||
</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 }})
|
||||
@@ -357,7 +491,13 @@ async function confirmReplace() {
|
||||
class="gf-preview-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'flags'">
|
||||
<template v-if="column.key === 'match_dose_text'">
|
||||
{{ record.match_dose_text || '—' }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'split_preview'">
|
||||
<span class="gf-split-preview">{{ buildSplitPreview(record) }}</span>
|
||||
</template>
|
||||
<template v-else-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
|
||||
@@ -374,10 +514,13 @@ async function confirmReplace() {
|
||||
<Button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!selectedDisplayName || !selectedFormulaCount"
|
||||
:disabled="!validReplaceTargets.length || !selectedFormulaCount"
|
||||
@click="confirmReplace"
|
||||
>
|
||||
确认替换({{ selectedFormulaCount }} 个金方)
|
||||
确认{{ validReplaceTargets.length > 1 ? '拆分' : '替换' }}({{
|
||||
selectedFormulaCount
|
||||
}}
|
||||
个金方)
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -272,6 +272,7 @@ async function handleConfirmImport(payload: { rows: any[]; sourceId: number }) {
|
||||
<Grid>
|
||||
<template #toolbar-buttons>
|
||||
<TableAction
|
||||
:flex="false"
|
||||
:actions="[
|
||||
{
|
||||
label: '新增',
|
||||
|
||||
@@ -43,7 +43,7 @@ import SalespersonCommissionDrawer from './components/SalespersonCommissionDrawe
|
||||
import AddDoctorFromStoreModal from './components/AddDoctorFromStoreModal.vue';
|
||||
import StoreCardModal from '#/components/store-card/StoreCardModal.vue';
|
||||
import StoreVipCell from '#/components/vip/StoreVipCell.vue';
|
||||
import VipUpgradeModal from '#/components/vip/VipUpgradeModal.vue';
|
||||
import VipHubModal from '#/components/vip/VipHubModal.vue';
|
||||
import BankCardReportModal from '#/views/system/store-bank-card/components/BankCardReportModal.vue';
|
||||
import BankCardStatusModal from '#/views/system/store-bank-card/components/BankCardStatusModal.vue';
|
||||
import BankCardStoreEditModal from '#/views/system/store-bank-card/components/BankCardStoreEditModal.vue';
|
||||
@@ -105,20 +105,23 @@ const [StoreCardModalComp, StoreCardModalApi] = useVbenModal({
|
||||
connectedComponent: StoreCardModal,
|
||||
});
|
||||
|
||||
const [VipUpgradeModalComp, vipUpgradeModalApi] = useVbenModal({
|
||||
connectedComponent: VipUpgradeModal,
|
||||
const [VipHubModalComp, vipHubModalApi] = useVbenModal({
|
||||
connectedComponent: VipHubModal,
|
||||
});
|
||||
|
||||
/** 打开门店 VIP 升级弹窗(列表徽标点击) */
|
||||
function openVipUpgrade(row: Record<string, any>) {
|
||||
/**
|
||||
* 打开门店 VIP 入口弹窗(两张卡片:开通/升级、开通记录)
|
||||
* 为什么先走 Hub:需要同时支持升级操作与查看开通人/流水,避免直接进升级页看不到记录
|
||||
*/
|
||||
function openVipHub(row: Record<string, any>) {
|
||||
if (!row?.id) return;
|
||||
vipUpgradeModalApi.setData({
|
||||
vipHubModalApi.setData({
|
||||
store_id: Number(row.id),
|
||||
store_name: row.name || '',
|
||||
vip: row.vip || null,
|
||||
gridApi,
|
||||
});
|
||||
vipUpgradeModalApi.open();
|
||||
vipHubModalApi.open();
|
||||
}
|
||||
|
||||
/** 添加医生弹窗:诊所列表行操作和详情医生团队共用 */
|
||||
@@ -491,7 +494,7 @@ const handleSwitchClinicType = (row: any) => {
|
||||
<BankCardStatusModalComponent />
|
||||
<BankCardStoreEditModalComponent />
|
||||
<StoreCardModalComp />
|
||||
<VipUpgradeModalComp />
|
||||
<VipHubModalComp />
|
||||
<AddDoctorModalComp />
|
||||
<QrCodePreviewModal />
|
||||
<DrugPriceModalComponent />
|
||||
@@ -561,7 +564,7 @@ const handleSwitchClinicType = (row: any) => {
|
||||
/>
|
||||
</template>
|
||||
<template #vip="{ row }">
|
||||
<StoreVipCell :vip="row.vip" @click="openVipUpgrade(row)" />
|
||||
<StoreVipCell :vip="row.vip" @click="openVipHub(row)" />
|
||||
</template>
|
||||
<template #store_config_1="{ row }">
|
||||
<StoreConfigTogglesCell
|
||||
|
||||
@@ -187,10 +187,13 @@ const [Modal, modalApi] = useVbenModal({
|
||||
{{ (opt.name || opt.code || '?').slice(0, 1) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="vip-level-card__name">{{ opt.name }}</div>
|
||||
<div class="vip-level-card__meta">
|
||||
{{ opt.code }} · 权重 {{ opt.weight }}
|
||||
<div class="vip-level-card__name">
|
||||
{{ opt.name || opt.code || '—' }}
|
||||
<span v-if="opt.code" class="vip-level-card__code">{{
|
||||
opt.code
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="vip-level-card__meta">权重 {{ opt.weight }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -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;
|
||||
|
||||
@@ -81,6 +81,18 @@ const showModal = (data: any = {}, isUpdate = false) => {
|
||||
formModalApi.open();
|
||||
};
|
||||
|
||||
/**
|
||||
* 已绑等级展示文案:同名等级(如多档「黄金会员」)需同时带上 code(V1/V2…)才能区分
|
||||
*/
|
||||
function formatBoundLevelLabel(lv: Record<string, any>) {
|
||||
const name = String(lv?.name || '').trim();
|
||||
const code = String(lv?.code || '').trim();
|
||||
if (name && code) {
|
||||
return `${name}(${code})`;
|
||||
}
|
||||
return name || code || '—';
|
||||
}
|
||||
|
||||
/** 打开反绑等级弹窗 */
|
||||
function openBindLevels(item: Record<string, any>) {
|
||||
bindLevelsModalApi.setData({
|
||||
@@ -176,20 +188,25 @@ onMounted(loadList);
|
||||
v-for="lv in item.bound_levels"
|
||||
:key="lv.id"
|
||||
class="bound-vip-chip"
|
||||
:title="lv.name || lv.code"
|
||||
:title="formatBoundLevelLabel(lv)"
|
||||
>
|
||||
<img
|
||||
v-if="lv.badge_url"
|
||||
:src="lv.badge_url"
|
||||
:alt="lv.name || lv.code"
|
||||
:alt="formatBoundLevelLabel(lv)"
|
||||
class="bound-vip-chip__img"
|
||||
/>
|
||||
<span v-else class="bound-vip-chip__fallback">{{
|
||||
(lv.name || lv.code || '?').slice(0, 1)
|
||||
}}</span>
|
||||
<span class="bound-vip-chip__name">{{
|
||||
lv.name || lv.code
|
||||
(lv.code || lv.name || '?').slice(0, 1)
|
||||
}}</span>
|
||||
<span class="bound-vip-chip__text">
|
||||
<span class="bound-vip-chip__name">{{
|
||||
lv.name || lv.code || '—'
|
||||
}}</span>
|
||||
<span v-if="lv.code" class="bound-vip-chip__code">{{
|
||||
lv.code
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="text-xs">未绑定任何会员等级</span>
|
||||
@@ -238,12 +255,22 @@ onMounted(loadList);
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.bound-vip-chip__text {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.bound-vip-chip__name {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--foreground));
|
||||
max-width: 88px;
|
||||
max-width: 96px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bound-vip-chip__code {
|
||||
font-size: 11px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 门店 VIP 开通页:复用全局 VipUpgradeModal
|
||||
* 门店 VIP 开通页:点击徽标先进入 VIP 入口(升级 / 开通记录)
|
||||
*/
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { TableAction } from '#/components/table-action';
|
||||
import VipBadgeCombo from '#/components/vip/VipBadgeCombo.vue';
|
||||
import VipUpgradeModal from '#/components/vip/VipUpgradeModal.vue';
|
||||
import VipHubModal from '#/components/vip/VipHubModal.vue';
|
||||
|
||||
import { formOptions } from './config/search';
|
||||
import { gridOptions } from './config/table';
|
||||
@@ -15,23 +15,24 @@ import { gridOptions } from './config/table';
|
||||
defineOptions({ name: 'VipStore' });
|
||||
|
||||
const [Grid, gridApi] = useVbenVxeGrid({ formOptions, gridOptions });
|
||||
const [UpgradeModalComp, upgradeModalApi] = useVbenModal({
|
||||
connectedComponent: VipUpgradeModal,
|
||||
const [HubModalComp, hubModalApi] = useVbenModal({
|
||||
connectedComponent: VipHubModal,
|
||||
});
|
||||
|
||||
/** 打开 VIP 入口弹窗 */
|
||||
const openVip = (row: any) => {
|
||||
upgradeModalApi.setData({
|
||||
hubModalApi.setData({
|
||||
store_id: row.store_id,
|
||||
store_name: row.store_name,
|
||||
vip: row.vip,
|
||||
gridApi,
|
||||
});
|
||||
upgradeModalApi.open();
|
||||
hubModalApi.open();
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<Page auto-content-height title="门店VIP开通">
|
||||
<UpgradeModalComp />
|
||||
<HubModalComp />
|
||||
<div class="p-4">
|
||||
<Grid>
|
||||
<template #vip_badge="{ row }">
|
||||
@@ -50,7 +51,7 @@ const openVip = (row: any) => {
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<TableAction
|
||||
:actions="[{ label: '开通/升级', onClick: () => openVip(row) }]"
|
||||
:actions="[{ label: 'VIP管理', onClick: () => openVip(row) }]"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
|
||||
Reference in New Issue
Block a user