liu
2026-09-29 982c97ab82f3beb1b4febcf014562eda34b9fc05
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<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>