diff --git a/apps/web-antd/src/components/form/components/store-multi-search.vue b/apps/web-antd/src/components/form/components/store-multi-search.vue index 8890a9fc..5dd49b80 100644 --- a/apps/web-antd/src/components/form/components/store-multi-search.vue +++ b/apps/web-antd/src/components/form/components/store-multi-search.vue @@ -1,12 +1,18 @@ - -
- - -
-
- {{ item.name }}【{{ item.id }}】 -
-
- 首拼:{{ item.shouzimu }} -
+ +
+
+ ↑↓ 循环切换 · Enter 选用 · Esc 关闭 · 可连续多选 + + 剩 {{ visibleOptions.length }} 条可选 +
- -
+
+ + +
+ +
+
+
+
+ -
+ + diff --git a/apps/web-antd/src/components/vip/VipUpgradeModal.vue b/apps/web-antd/src/components/vip/VipUpgradeModal.vue index 8224eb73..4370e968 100644 --- a/apps/web-antd/src/components/vip/VipUpgradeModal.vue +++ b/apps/web-antd/src/components/vip/VipUpgradeModal.vue @@ -17,7 +17,7 @@ import { openVipStore, } from '#/views/system/vip/store/api'; -/** 时长预设 → 时效类型 code(与后端 VipDurationHelper 对齐) */ +/** 时长预设 → 时效类型 code(与后端 VipDurationHelper 对齐;keep 走当前会员 duration_type) */ const PRESET_TO_DURATION: Record = { '7d': 'trial', '1w': 'week', @@ -33,6 +33,8 @@ const gridApi = ref(); const onSuccess = ref void)>(null); const storeId = ref(0); const storeName = ref(''); +/** 打开弹窗时带入的当前门店 VIP,用于判断能否「继承剩余时间」 */ +const currentVip = ref | null>(null); const levelId = ref(); const durationPreset = ref(); const customYears = ref(); @@ -46,8 +48,28 @@ const selectedLevel = computed(() => levels.value.find((i) => i.id === levelId.value), ); +/** + * 是否可继承剩余时间:非普通会员(非 V0)且未过期 + * 普通会员没有可继承的到期时间,不展示/不默认 keep + */ +const canKeepDuration = computed(() => { + const v = currentVip.value; + if (!v) return false; + const code = String(v.level_code || 'V0').trim() || 'V0'; + if (code === 'V0') return false; + const expireAt = Number(v.expire_at || 0); + // expire_at=0 表示终身,视为可继承;>0 须未过期 + if (expireAt > 0 && expireAt * 1000 <= Date.now()) return false; + return true; +}); + /** 按时长预设解析对应丝带资源 */ function ribbonByPreset(preset?: string) { + if (preset === 'keep') { + const code = String(currentVip.value?.duration_type || '').trim(); + if (!code) return null; + return durationTypes.value.find((i) => i.code === code) || null; + } const code = PRESET_TO_DURATION[preset || ''] || ''; if (!code) return null; return durationTypes.value.find((i) => i.code === code) || null; @@ -57,16 +79,18 @@ const selectedDurationType = computed(() => ribbonByPreset(durationPreset.value), ); -/** 时长卡片数据:附带丝带图,避免模板重复查找 */ +/** 时长卡片:普通会员隐藏 keep;其它预设照常 */ const durationCards = computed(() => - presets.value.map((p) => { - const ribbon = ribbonByPreset(p.value); - return { - ...p, - ribbonUrl: ribbon?.badge_url || '', - ribbonName: ribbon?.name || '', - }; - }), + presets.value + .filter((p) => p.value !== 'keep' || canKeepDuration.value) + .map((p) => { + const ribbon = ribbonByPreset(p.value); + return { + ...p, + ribbonUrl: ribbon?.badge_url || '', + ribbonName: ribbon?.name || '', + }; + }), ); const previewBadgeUrl = computed(() => selectedLevel.value?.badge_url || ''); @@ -78,10 +102,36 @@ const previewLevelName = computed( ); const previewDurationLabel = computed(() => { if (!durationPreset.value) return '请选择时长'; + if (durationPreset.value === 'keep') { + const label = String(currentVip.value?.duration_label || '').trim(); + const expireHint = formatVipExpireHint(currentVip.value); + if (label && expireHint) return `继承剩余:${label}(${expireHint})`; + if (label) return `继承剩余:${label}`; + return '继承剩余时间(不改动)'; + } const preset = presets.value.find((p) => p.value === durationPreset.value); return preset?.label || selectedDurationType.value?.name || '请选择时长'; }); +/** + * 格式化当前会员到期提示(终身 / 已格式化字符串 / 时间戳) + */ +function formatVipExpireHint(v: Record | null | undefined): string { + if (!v) return ''; + if (v.is_lifetime) return '终身'; + const raw = v.expire_at; + if (raw == null || raw === '' || raw === 0) return ''; + if (typeof raw === 'string' && Number.isNaN(Number(raw))) { + return `至 ${raw}`; + } + const n = Number(raw); + if (!(n > 0)) return ''; + const d = new Date(n * 1000); + if (Number.isNaN(d.getTime())) return ''; + const pad = (x: number) => String(x).padStart(2, '0'); + return `至 ${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + const title = computed(() => `升级VIP - ${storeName.value || storeId.value}`); /** 选择会员等级 */ @@ -139,6 +189,7 @@ const [Modal, modalApi] = useVbenModal({ durationPreset.value = undefined; customYears.value = undefined; remark.value = ''; + currentVip.value = null; return; } const data = modalApi.getData>() || {}; @@ -146,6 +197,7 @@ const [Modal, modalApi] = useVbenModal({ onSuccess.value = data.onSuccess || null; storeId.value = Number(data.store_id || 0); storeName.value = data.store_name || ''; + currentVip.value = data.vip || null; const [levelList, presetList, durationList] = await Promise.all([ getVipLevelOption(), getVipDurationPresets(), @@ -157,10 +209,14 @@ const [Modal, modalApi] = useVbenModal({ value: i.value, })); durationTypes.value = durationList || []; - // 回填当前 VIP 等级(便于续期) + // 回填当前 VIP 等级(便于续期/改档) if (data.vip?.level_id) { levelId.value = Number(data.vip.level_id); } + // 非普通会员:默认选「继承剩余时间」,到期日不改动 + if (canKeepDuration.value) { + durationPreset.value = 'keep'; + } }, }); diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 9e688b71..16c5ff93 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -1,15 +1,15 @@ @@ -321,13 +275,54 @@ watch( - +