2026-08-10 15:33:08 +08:00
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 门店 VIP 入口弹窗
|
|
|
|
|
|
* 先展示两张操作卡片:开通/升级、开通记录;再分别打开对应子弹窗
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { computed, ref } from 'vue';
|
|
|
|
|
|
|
|
|
|
|
|
import { useVbenModal } from '@vben/common-ui';
|
|
|
|
|
|
|
|
|
|
|
|
import VipBadgeCombo from '#/components/vip/VipBadgeCombo.vue';
|
|
|
|
|
|
import VipStoreRecordsModal from '#/components/vip/VipStoreRecordsModal.vue';
|
|
|
|
|
|
import VipUpgradeModal from '#/components/vip/VipUpgradeModal.vue';
|
2026-08-11 13:30:21 +08:00
|
|
|
|
import { formatVipExpireAt } from '#/utils/vip';
|
2026-08-10 15:33:08 +08:00
|
|
|
|
|
|
|
|
|
|
const gridApi = ref<any>();
|
|
|
|
|
|
const onSuccess = ref<null | (() => void)>(null);
|
|
|
|
|
|
const storeId = ref(0);
|
|
|
|
|
|
const storeName = ref('');
|
|
|
|
|
|
const vip = ref<Record<string, any> | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const title = computed(
|
|
|
|
|
|
() => `VIP管理 - ${storeName.value || storeId.value || ''}`,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-11 13:30:21 +08:00
|
|
|
|
/** 当前等级 + 时长类型 */
|
2026-08-10 15:33:08 +08:00
|
|
|
|
const currentLevelText = computed(() => {
|
|
|
|
|
|
const v = vip.value;
|
|
|
|
|
|
if (!v) return '当前:普通会员';
|
|
|
|
|
|
const name = String(v.level_name || '').trim();
|
|
|
|
|
|
const code = String(v.level_code || '').trim();
|
|
|
|
|
|
const duration = String(v.duration_label || '').trim();
|
|
|
|
|
|
const level =
|
|
|
|
|
|
name && code ? `${name}(${code})` : name || code || '普通会员';
|
|
|
|
|
|
return duration ? `当前:${level} · ${duration}` : `当前:${level}`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-11 13:30:21 +08:00
|
|
|
|
/** 到期时间独立一行,打开弹窗时一眼可见 */
|
|
|
|
|
|
const currentExpireText = computed(() => {
|
|
|
|
|
|
const v = vip.value;
|
|
|
|
|
|
if (!v) return '';
|
|
|
|
|
|
const code = String(v.level_code || 'V0').trim() || 'V0';
|
|
|
|
|
|
if (code === 'V0') return '';
|
|
|
|
|
|
const expire = formatVipExpireAt(v, {
|
|
|
|
|
|
lifetimeText: '终身',
|
|
|
|
|
|
withPrefix: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!expire) return '';
|
|
|
|
|
|
return expire === '终身' ? '到期时间:终身' : `到期时间:${expire}`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-10 15:33:08 +08:00
|
|
|
|
const [UpgradeModalComp, upgradeModalApi] = useVbenModal({
|
|
|
|
|
|
connectedComponent: VipUpgradeModal,
|
|
|
|
|
|
});
|
|
|
|
|
|
const [RecordsModalComp, recordsModalApi] = useVbenModal({
|
|
|
|
|
|
connectedComponent: VipStoreRecordsModal,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [Modal, modalApi] = useVbenModal({
|
|
|
|
|
|
fullscreenButton: false,
|
|
|
|
|
|
draggable: true,
|
|
|
|
|
|
class: 'w-[640px]',
|
|
|
|
|
|
showConfirmButton: false,
|
|
|
|
|
|
cancelText: '关闭',
|
|
|
|
|
|
onCancel() {
|
|
|
|
|
|
modalApi.close();
|
|
|
|
|
|
},
|
|
|
|
|
|
async onOpenChange(isOpen: boolean) {
|
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
|
storeId.value = 0;
|
|
|
|
|
|
storeName.value = '';
|
|
|
|
|
|
vip.value = null;
|
|
|
|
|
|
gridApi.value = null;
|
|
|
|
|
|
onSuccess.value = null;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const data = modalApi.getData<Record<string, any>>() || {};
|
|
|
|
|
|
storeId.value = Number(data.store_id || 0);
|
|
|
|
|
|
storeName.value = String(data.store_name || '');
|
|
|
|
|
|
vip.value = data.vip || null;
|
|
|
|
|
|
gridApi.value = data.gridApi || null;
|
|
|
|
|
|
onSuccess.value = data.onSuccess || null;
|
|
|
|
|
|
modalApi.setState({ title: title.value });
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 打开开通/升级弹窗
|
|
|
|
|
|
* 为什么不直接嵌在本弹窗:升级表单已有独立组件,复用可保持交互一致
|
|
|
|
|
|
*/
|
|
|
|
|
|
function openUpgrade() {
|
|
|
|
|
|
if (!storeId.value) return;
|
|
|
|
|
|
upgradeModalApi.setData({
|
|
|
|
|
|
store_id: storeId.value,
|
|
|
|
|
|
store_name: storeName.value,
|
|
|
|
|
|
vip: vip.value,
|
|
|
|
|
|
gridApi: gridApi.value,
|
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
|
onSuccess.value?.();
|
|
|
|
|
|
gridApi.value?.query?.();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
upgradeModalApi.open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 打开当前门店开通记录(含开通人) */
|
|
|
|
|
|
function openRecords() {
|
|
|
|
|
|
if (!storeId.value) return;
|
|
|
|
|
|
recordsModalApi.setData({
|
|
|
|
|
|
store_id: storeId.value,
|
|
|
|
|
|
store_name: storeName.value,
|
|
|
|
|
|
});
|
|
|
|
|
|
recordsModalApi.open();
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<Modal>
|
|
|
|
|
|
<div class="vip-hub-current mb-4">
|
|
|
|
|
|
<VipBadgeCombo
|
|
|
|
|
|
size="md"
|
|
|
|
|
|
:badge-url="vip?.badge_url"
|
|
|
|
|
|
:duration-badge-url="vip?.duration_badge_url"
|
|
|
|
|
|
:level-name="vip?.level_name"
|
|
|
|
|
|
:duration-label="vip?.duration_label"
|
|
|
|
|
|
/>
|
2026-08-11 13:30:21 +08:00
|
|
|
|
<div class="vip-hub-current__meta">
|
|
|
|
|
|
<div class="vip-hub-current__text">{{ currentLevelText }}</div>
|
|
|
|
|
|
<div v-if="currentExpireText" class="vip-hub-current__expire">
|
|
|
|
|
|
{{ currentExpireText }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-08-10 15:33:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="vip-hub-grid">
|
|
|
|
|
|
<button type="button" class="vip-hub-card" @click="openUpgrade">
|
|
|
|
|
|
<div class="vip-hub-card__icon">↑</div>
|
|
|
|
|
|
<div class="vip-hub-card__title">开通 / 升级</div>
|
|
|
|
|
|
<div class="vip-hub-card__desc">
|
|
|
|
|
|
为该门店开通或调整会员等级与时长
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" class="vip-hub-card" @click="openRecords">
|
|
|
|
|
|
<div class="vip-hub-card__icon">☰</div>
|
|
|
|
|
|
<div class="vip-hub-card__title">开通记录</div>
|
|
|
|
|
|
<div class="vip-hub-card__desc">
|
|
|
|
|
|
查看历史开通流水与开通人
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
<UpgradeModalComp />
|
|
|
|
|
|
<RecordsModalComp />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.vip-hub-current {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
padding: 12px 14px;
|
|
|
|
|
|
border: 1px solid hsl(var(--border));
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
background: hsl(var(--muted) / 0.25);
|
|
|
|
|
|
}
|
2026-08-11 13:30:21 +08:00
|
|
|
|
.vip-hub-current__meta {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
2026-08-10 15:33:08 +08:00
|
|
|
|
.vip-hub-current__text {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: hsl(var(--foreground));
|
|
|
|
|
|
}
|
2026-08-11 13:30:21 +08:00
|
|
|
|
.vip-hub-current__expire {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: hsl(var(--muted-foreground));
|
|
|
|
|
|
}
|
2026-08-10 15:33:08 +08:00
|
|
|
|
.vip-hub-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.vip-hub-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
min-height: 148px;
|
|
|
|
|
|
padding: 18px 16px;
|
|
|
|
|
|
border: 1px solid hsl(var(--border));
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
background: hsl(var(--card, var(--background)));
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition:
|
|
|
|
|
|
border-color 0.15s,
|
|
|
|
|
|
background 0.15s,
|
|
|
|
|
|
box-shadow 0.15s;
|
|
|
|
|
|
}
|
|
|
|
|
|
.vip-hub-card:hover {
|
|
|
|
|
|
border-color: hsl(var(--primary));
|
|
|
|
|
|
background: hsl(var(--primary) / 0.08);
|
|
|
|
|
|
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
.vip-hub-card__icon {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: hsl(var(--primary));
|
|
|
|
|
|
background: hsl(var(--primary) / 0.12);
|
|
|
|
|
|
}
|
|
|
|
|
|
.vip-hub-card__title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
|
color: hsl(var(--foreground));
|
|
|
|
|
|
}
|
|
|
|
|
|
.vip-hub-card__desc {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
color: hsl(var(--muted-foreground));
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 560px) {
|
|
|
|
|
|
.vip-hub-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|