Files
xk-admin/apps/web-antd/src/components/vip/VipHubModal.vue

234 lines
6.2 KiB
Vue
Raw Normal View History

<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';
import { formatVipExpireAt } from '#/utils/vip';
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 || ''}`,
);
/** 当前等级 + 时长类型 */
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}`;
});
/** 到期时间独立一行,打开弹窗时一眼可见 */
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}`;
});
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"
/>
<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>
</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);
}
.vip-hub-current__meta {
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.vip-hub-current__text {
font-size: 13px;
font-weight: 500;
color: hsl(var(--foreground));
}
.vip-hub-current__expire {
font-size: 12px;
color: hsl(var(--muted-foreground));
}
.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>