From b82754e36be25093f4954191f61fa1832ec08e95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Mon, 10 Aug 2026 20:08:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=201.=20=E6=9B=B4=E6=96=B0=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=202.=20=E4=BF=AE=E5=A4=8D=E6=A1=86=E6=9E=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=90=8E=E5=AF=BC=E8=87=B4=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=203.=20=E6=B6=88=E6=81=AF=E7=B3=BB=E7=BB=9F=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../form/components/store-multi-search.vue | 368 +++-- .../src/components/vip/VipUpgradeModal.vue | 78 +- apps/web-antd/src/layouts/basic.vue | 347 ++-- .../log/queue-job/components/QueueJobList.vue | 6 +- .../log/queue-job/components/detail-modal.vue | 77 +- .../views/notice/action/NoticeActionHost.vue | 122 ++ apps/web-antd/src/views/notice/action/bus.ts | 40 + .../web-antd/src/views/notice/action/types.ts | 143 ++ apps/web-antd/src/views/notice/api/index.ts | 43 +- .../src/views/notice/compoents/send.vue | 30 +- apps/web-antd/src/views/notice/index.vue | 1413 +++++++++++------ .../views/notice/scenes/NoticeSceneHost.vue | 128 ++ .../views/notice/scenes/NoticeSceneShell.vue | 260 +++ apps/web-antd/src/views/notice/scenes/meta.ts | 113 ++ .../notice/views/DoctorInputNoticeView.vue | 434 +++++ .../views/notice/views/InvoiceNoticeView.vue | 135 ++ .../views/notice/views/PeriodReportView.vue | 147 ++ .../notice/views/PriceChangeNoticeView.vue | 89 ++ .../views/notice/views/QueueFailureView.vue | 234 +++ .../views/notice/views/RichTextNoticeView.vue | 103 ++ .../notice/views/StoreInputNoticeView.vue | 422 +++++ .../src/views/notice/views/VipNoticeView.vue | 340 ++++ .../views/WarehouseProductNoticeView.vue | 197 +++ .../views/notice/views/WithdrawNoticeView.vue | 136 ++ .../src/views/notice/views/registry.ts | 43 + .../system/admin-notice-type/api/index.ts | 24 + .../admin-notice-type/components/modal.vue | 69 + .../admin-notice-type/config/constants.ts | 22 + .../system/admin-notice-type/config/form.ts | 75 + .../views/system/admin-notice-type/index.vue | 178 +++ .../doctor-input/components/AuditModal.vue | 220 ++- .../vip/store/components/open-modal.vue | 26 +- .../src/widgets/notification/notification.vue | 26 +- 33 files changed, 5202 insertions(+), 886 deletions(-) create mode 100644 apps/web-antd/src/views/notice/action/NoticeActionHost.vue create mode 100644 apps/web-antd/src/views/notice/action/bus.ts create mode 100644 apps/web-antd/src/views/notice/action/types.ts create mode 100644 apps/web-antd/src/views/notice/scenes/NoticeSceneHost.vue create mode 100644 apps/web-antd/src/views/notice/scenes/NoticeSceneShell.vue create mode 100644 apps/web-antd/src/views/notice/scenes/meta.ts create mode 100644 apps/web-antd/src/views/notice/views/DoctorInputNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/InvoiceNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/PeriodReportView.vue create mode 100644 apps/web-antd/src/views/notice/views/PriceChangeNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/QueueFailureView.vue create mode 100644 apps/web-antd/src/views/notice/views/RichTextNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/StoreInputNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/VipNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/WarehouseProductNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/WithdrawNoticeView.vue create mode 100644 apps/web-antd/src/views/notice/views/registry.ts create mode 100644 apps/web-antd/src/views/system/admin-notice-type/api/index.ts create mode 100644 apps/web-antd/src/views/system/admin-notice-type/components/modal.vue create mode 100644 apps/web-antd/src/views/system/admin-notice-type/config/constants.ts create mode 100644 apps/web-antd/src/views/system/admin-notice-type/config/form.ts create mode 100644 apps/web-antd/src/views/system/admin-notice-type/index.vue 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( - +