<script lang="ts" setup>
|
import type { WxCpActivityApi } from '#/api/wxcp/activity';
|
|
import { computed, ref } from 'vue';
|
|
import { useVbenModal } from '@vben/common-ui';
|
import { Progress, Spin } from 'ant-design-vue';
|
|
import { getActivityStats } from '#/api/wxcp/activity';
|
|
defineOptions({ name: 'WxCpActivityStatsModal' });
|
|
const activity = ref<WxCpActivityApi.Activity>({});
|
const stats = ref<null | WxCpActivityApi.ActivityStats>(null);
|
const loading = ref(false);
|
const loadError = ref('');
|
|
/** 不限名额时不展示进度条 */
|
const hasLimit = computed(
|
() => !!stats.value?.signupLimit && stats.value.signupLimit > 0,
|
);
|
|
async function loadStats(id: number) {
|
loading.value = true;
|
loadError.value = '';
|
try {
|
stats.value = await getActivityStats(id);
|
} catch (error: unknown) {
|
loadError.value = (error as Error)?.message || '统计加载失败';
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
const [Modal, modalApi] = useVbenModal({
|
onOpenChange(isOpen: boolean) {
|
if (!isOpen) {
|
stats.value = null;
|
activity.value = {};
|
loadError.value = '';
|
return;
|
}
|
const data = modalApi.getData<{ activity: WxCpActivityApi.Activity }>();
|
activity.value = data.activity ?? {};
|
if (activity.value.id) {
|
loadStats(activity.value.id);
|
}
|
},
|
});
|
</script>
|
|
<template>
|
<Modal title="报名统计" class="w-3/5">
|
<Spin :spinning="loading">
|
<div class="px-4">
|
<div class="mb-3 text-sm text-gray-500">
|
{{ stats?.activityTitle || activity.title }}
|
</div>
|
|
<div v-if="loadError" class="py-6 text-center text-sm text-red-500">
|
{{ loadError }}
|
</div>
|
|
<template v-else-if="stats">
|
<!-- 关键指标 -->
|
<div class="mb-4 grid grid-cols-4 gap-3">
|
<div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800">
|
<div class="text-xl font-semibold">
|
{{ stats.totalSignup }}
|
</div>
|
<div class="mt-1 text-xs text-gray-500">有效报名</div>
|
</div>
|
<div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800">
|
<div class="text-xl font-semibold">
|
{{ stats.signedUpCount }}
|
</div>
|
<div class="mt-1 text-xs text-gray-500">未签到</div>
|
</div>
|
<div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800">
|
<div class="text-xl font-semibold">
|
{{ stats.checkedInCount }}
|
</div>
|
<div class="mt-1 text-xs text-gray-500">已签到</div>
|
</div>
|
<div class="rounded bg-gray-50 p-3 text-center dark:bg-gray-800">
|
<div class="text-xl font-semibold">
|
{{ stats.canceledCount }}
|
</div>
|
<div class="mt-1 text-xs text-gray-500">已取消</div>
|
</div>
|
</div>
|
|
<!-- 进度 -->
|
<div v-if="hasLimit" class="mb-5">
|
<div class="mb-1 flex justify-between text-xs text-gray-500">
|
<span>报名进度</span>
|
<span>
|
{{ stats.totalSignup }} / {{ stats.signupLimit }}
|
(剩余 {{ stats.remaining ?? 0 }})
|
</span>
|
</div>
|
<Progress :percent="stats.signupPercent ?? 0" />
|
</div>
|
<div v-else class="mb-5 text-xs text-gray-500">
|
本次活动不限名额,当前已报名 {{ stats.totalSignup }} 人
|
</div>
|
|
<!-- 分布 -->
|
<div class="grid grid-cols-3 gap-4 text-sm">
|
<div>
|
<div class="mb-2 font-medium">按部门</div>
|
<div v-if="stats.deptStats?.length" class="space-y-1">
|
<div
|
v-for="item in stats.deptStats"
|
:key="item.name"
|
class="flex justify-between text-gray-600"
|
>
|
<span class="truncate">{{ item.name }}</span>
|
<span>{{ item.count }}</span>
|
</div>
|
</div>
|
<div v-else class="text-xs text-gray-400">暂无数据</div>
|
</div>
|
|
<div>
|
<div class="mb-2 font-medium">按报名日期</div>
|
<div v-if="stats.dateStats?.length" class="space-y-1">
|
<div
|
v-for="item in stats.dateStats"
|
:key="item.name"
|
class="flex justify-between text-gray-600"
|
>
|
<span>{{ item.name }}</span>
|
<span>{{ item.count }}</span>
|
</div>
|
</div>
|
<div v-else class="text-xs text-gray-400">暂无数据</div>
|
</div>
|
|
<div>
|
<div class="mb-2 font-medium">按来源</div>
|
<div v-if="stats.sourceStats?.length" class="space-y-1">
|
<div
|
v-for="item in stats.sourceStats"
|
:key="item.name"
|
class="flex justify-between text-gray-600"
|
>
|
<span>{{ item.name }}</span>
|
<span>{{ item.count }}</span>
|
</div>
|
</div>
|
<div v-else class="text-xs text-gray-400">暂无数据</div>
|
</div>
|
</div>
|
|
<div class="mt-4 text-xs text-gray-400">
|
签到率 {{ stats.checkInPercent }}%(按有效报名数计算,不含已取消)
|
</div>
|
</template>
|
</div>
|
</Spin>
|
</Modal>
|
</template>
|