|
|
@@ -1,7 +1,6 @@
|
|
|
<script setup lang="tsx">
|
|
|
import { Button, Popconfirm, Tag, Tooltip } from 'ant-design-vue';
|
|
|
import dayjs from 'dayjs';
|
|
|
-import { onMounted, ref } from 'vue';
|
|
|
|
|
|
import SvgIcon from '@/components/custom/svg-icon.vue';
|
|
|
import { UNIFORM_TEXT } from '@/enum';
|
|
|
@@ -22,7 +21,87 @@ import ExportModal from './modules/export-modal.vue';
|
|
|
import HeaderSearch from './modules/header-search.vue';
|
|
|
import ImportDrawer from './modules/import-drawer.vue';
|
|
|
import RefundModal from './modules/refund-modal.vue';
|
|
|
+import { computed, onMounted, ref } from 'vue';
|
|
|
+
|
|
|
+const expandStats = ref(false);
|
|
|
+
|
|
|
+// 计算统计信息
|
|
|
+const stats = computed(() => {
|
|
|
+ if (!tableInfo.value || !Array.isArray(tableInfo.value)) {
|
|
|
+ return {
|
|
|
+ totalCount: 0,
|
|
|
+ activeCount: 0,
|
|
|
+ refundCount: 0,
|
|
|
+ revokedCount: 0,
|
|
|
+ inactiveCount: 0,
|
|
|
+ // 按类型统计
|
|
|
+ typeStats: {},
|
|
|
+ // 按类型+激活状态统计
|
|
|
+ detailedStats: {},
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const info = tableInfo.value;
|
|
|
+ const typeStats: Record<string, number> = {};
|
|
|
+ const detailedStats: Record<string, {
|
|
|
+ total: number;
|
|
|
+ active: number;
|
|
|
+ refunded: number;
|
|
|
+ revoked: number;
|
|
|
+ inactive: number;
|
|
|
+ }> = {};
|
|
|
+
|
|
|
+ // 遍历 info 数组进行统计
|
|
|
+ info.forEach((item: any) => {
|
|
|
+ const typeId = item.typeId || 'unknown';
|
|
|
+ const typeName = activationCodeTypeOptions.value.find(opt => opt.id === typeId)?.label || `类型${typeId}`;
|
|
|
+
|
|
|
+ // 统计总数
|
|
|
+ typeStats[typeName] = (typeStats[typeName] || 0) + 1;
|
|
|
+
|
|
|
+ // 初始化详细统计
|
|
|
+ if (!detailedStats[typeName]) {
|
|
|
+ detailedStats[typeName] = {
|
|
|
+ total: 0,
|
|
|
+ active: 0,
|
|
|
+ refunded: 0,
|
|
|
+ revoked: 0,
|
|
|
+ inactive: 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
+ // 更新详细统计
|
|
|
+ detailedStats[typeName].total += 1;
|
|
|
+ if (item.activated) {
|
|
|
+ detailedStats[typeName].active += 1;
|
|
|
+ } else {
|
|
|
+ detailedStats[typeName].inactive += 1;
|
|
|
+ }
|
|
|
+ if (item.refunded) {
|
|
|
+ detailedStats[typeName].refunded += 1;
|
|
|
+ }
|
|
|
+ if (item.revoked) {
|
|
|
+ detailedStats[typeName].revoked += 1;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 计算总体统计
|
|
|
+ const totalCount = info.length;
|
|
|
+ const activeCount = info.filter(item => item.activated).length;
|
|
|
+ const refundCount = info.filter(item => item.refunded).length;
|
|
|
+ const revokedCount = info.filter(item => item.revoked).length;
|
|
|
+ const inactiveCount = info.filter(item => !item.activated).length;
|
|
|
+
|
|
|
+ return {
|
|
|
+ totalCount,
|
|
|
+ activeCount,
|
|
|
+ refundCount,
|
|
|
+ revokedCount,
|
|
|
+ inactiveCount,
|
|
|
+ typeStats,
|
|
|
+ detailedStats,
|
|
|
+ };
|
|
|
+});
|
|
|
const { userInfo } = useAuthStore();
|
|
|
|
|
|
// 激活码类型选项
|
|
|
@@ -107,6 +186,7 @@ const {
|
|
|
searchParams,
|
|
|
updateSearchParams,
|
|
|
resetSearchParams,
|
|
|
+ info: tableInfo, // 改为 info 字段
|
|
|
} = useTable({
|
|
|
apiFn: getActivationCodeList,
|
|
|
apiParams: {
|
|
|
@@ -276,6 +356,10 @@ const {
|
|
|
),
|
|
|
},
|
|
|
],
|
|
|
+ // 可选:通过回调接收统计信息
|
|
|
+ onStatsUpdate: (newStats) => {
|
|
|
+ console.log('统计信息更新:', newStats);
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
const { checkedRowKeys, onDeleted } = useTableOperate(data, getData);
|
|
|
@@ -475,11 +559,143 @@ onMounted(() => {
|
|
|
|
|
|
<!-- 退款弹窗 -->
|
|
|
<RefundModal v-model:visible="refundModalVisible" :record="refundingRecord" @confirm="confirmRefund" />
|
|
|
+ <!-- 统计信息显示 - 精简模式 -->
|
|
|
+ <div v-if="stats.totalCount > 0" class="stats-container mt-4 p-3 bg-blue-50 rounded-lg border border-blue-200" style="position: absolute;bottom: 10px;">
|
|
|
+ <!-- 第一行:基础统计 + 展开按钮 -->
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <div class="flex items-center flex-wrap gap-3">
|
|
|
+ <span class="text-blue-700 font-medium">搜索结果统计:</span>
|
|
|
+
|
|
|
+ <span class="px-2 py-1 bg-white rounded shadow-sm border">
|
|
|
+ 总数 <strong class="ml-1 text-lg">{{ stats.totalCount }}</strong>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <span class="px-2 py-1 bg-green-100 text-green-800 rounded shadow-sm">
|
|
|
+ 已激活 <strong class="ml-1">{{ stats.activeCount }}</strong>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded shadow-sm">
|
|
|
+ 已退款 <strong class="ml-1">{{ stats.refundCount }}</strong>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <span class="px-2 py-1 bg-red-100 text-red-800 rounded shadow-sm">
|
|
|
+ 已收回 <strong class="ml-1">{{ stats.revokedCount }}</strong>
|
|
|
+ </span>
|
|
|
+
|
|
|
+ <span class="px-2 py-1 bg-gray-100 text-gray-800 rounded shadow-sm">
|
|
|
+ 未激活 <strong class="ml-1">{{ stats.inactiveCount }}</strong>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 展开/收起按钮 -->
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ size="small"
|
|
|
+ class="flex items-center text-blue-600 hover:text-blue-800"
|
|
|
+ @click="expandStats = !expandStats"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <SvgIcon
|
|
|
+ :icon="expandStats ? 'carbon:chevron-up' : 'carbon:chevron-down'"
|
|
|
+ class="mr-1"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ {{ expandStats ? '收起详情' : '展开详情' }}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 展开的详细统计信息 -->
|
|
|
+ <div v-if="expandStats" class="mt-4 pt-4 border-t border-blue-300">
|
|
|
+ <!-- 按类型统计 -->
|
|
|
+ <div v-if="Object.keys(stats.typeStats).length > 0" class="mb-4">
|
|
|
+ <h4 class="text-blue-700 font-medium mb-3">按类型统计:</h4>
|
|
|
+ <div class="flex flex-wrap gap-3">
|
|
|
+ <div
|
|
|
+ v-for="(count, typeName) in stats.typeStats"
|
|
|
+ :key="typeName"
|
|
|
+ class="px-3 py-2 bg-blue-100 text-blue-800 rounded shadow-sm border border-blue-300 flex items-center"
|
|
|
+ >
|
|
|
+ <div class="w-2 h-2 bg-blue-500 rounded-full mr-2"></div>
|
|
|
+ {{ typeName }} <strong class="ml-2 text-lg">{{ count }}</strong>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 详细统计表格 -->
|
|
|
+ <div v-if="Object.keys(stats.detailedStats).length > 0">
|
|
|
+ <h4 class="text-blue-700 font-medium mb-3">详细统计:</h4>
|
|
|
+ <div class="overflow-x-auto rounded-lg border border-blue-200 bg-white">
|
|
|
+ <table class="min-w-full">
|
|
|
+ <thead>
|
|
|
+ <tr class="bg-blue-50">
|
|
|
+ <th class="px-4 py-3 border-b text-left font-medium text-blue-700">类型</th>
|
|
|
+ <th class="px-4 py-3 border-b text-center font-medium text-blue-700">总数</th>
|
|
|
+ <th class="px-4 py-3 border-b text-center font-medium text-blue-700">已激活</th>
|
|
|
+ <th class="px-4 py-3 border-b text-center font-medium text-blue-700">未激活</th>
|
|
|
+ <th class="px-4 py-3 border-b text-center font-medium text-blue-700">已退款</th>
|
|
|
+ <th class="px-4 py-3 border-b text-center font-medium text-blue-700">已收回</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ <tr
|
|
|
+ v-for="(typeStats, typeName) in stats.detailedStats"
|
|
|
+ :key="typeName"
|
|
|
+ class="hover:bg-blue-50 transition-colors"
|
|
|
+ >
|
|
|
+ <td class="px-4 py-3 border-b">
|
|
|
+ <div class="flex items-center">
|
|
|
+ <div class="w-3 h-3 bg-blue-500 rounded mr-2"></div>
|
|
|
+ <span class="font-medium">{{ typeName }}</span>
|
|
|
+ </div>
|
|
|
+ </td>
|
|
|
+ <td class="px-4 py-3 border-b text-center font-bold">{{ typeStats.total }}</td>
|
|
|
+ <td class="px-4 py-3 border-b text-center">
|
|
|
+ <span v-if="typeStats.active > 0" class="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">
|
|
|
+ {{ typeStats.active }}
|
|
|
+ </span>
|
|
|
+ <span v-else class="text-gray-400">0</span>
|
|
|
+ </td>
|
|
|
+ <td class="px-4 py-3 border-b text-center">
|
|
|
+ <span v-if="typeStats.inactive > 0" class="px-2 py-1 bg-gray-100 text-gray-700 rounded text-sm">
|
|
|
+ {{ typeStats.inactive }}
|
|
|
+ </span>
|
|
|
+ <span v-else class="text-gray-400">0</span>
|
|
|
+ </td>
|
|
|
+ <td class="px-4 py-3 border-b text-center">
|
|
|
+ <span v-if="typeStats.refunded > 0" class="px-2 py-1 bg-yellow-100 text-yellow-700 rounded text-sm">
|
|
|
+ {{ typeStats.refunded }}
|
|
|
+ </span>
|
|
|
+ <span v-else class="text-gray-400">0</span>
|
|
|
+ </td>
|
|
|
+ <td class="px-4 py-3 border-b text-center">
|
|
|
+ <span v-if="typeStats.revoked > 0" class="px-2 py-1 bg-red-100 text-red-700 rounded text-sm">
|
|
|
+ {{ typeStats.revoked }}
|
|
|
+ </span>
|
|
|
+ <span v-else class="text-gray-400">0</span>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </tbody>
|
|
|
+ <!-- 总计行 -->
|
|
|
+ <tfoot>
|
|
|
+ <tr class="bg-blue-50 font-bold">
|
|
|
+ <td class="px-4 py-3">总计</td>
|
|
|
+ <td class="px-4 py-3 text-center text-blue-700">{{ stats.totalCount }}</td>
|
|
|
+ <td class="px-4 py-3 text-center text-green-700">{{ stats.activeCount }}</td>
|
|
|
+ <td class="px-4 py-3 text-center text-gray-700">{{ stats.inactiveCount }}</td>
|
|
|
+ <td class="px-4 py-3 text-center text-yellow-700">{{ stats.refundCount }}</td>
|
|
|
+ <td class="px-4 py-3 text-center text-red-700">{{ stats.revokedCount }}</td>
|
|
|
+ </tr>
|
|
|
+ </tfoot>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</PageContainer>
|
|
|
</template>
|
|
|
|
|
|
<style scoped>
|
|
|
-/* 已退款,未回收的行样式 - 橙底色 */
|
|
|
+/* 保持原来的行样式 */
|
|
|
:deep(.row-refunded-only) {
|
|
|
background-color: #fff7e6 !important;
|
|
|
}
|
|
|
@@ -488,7 +704,6 @@ onMounted(() => {
|
|
|
background-color: #ffe7ba !important;
|
|
|
}
|
|
|
|
|
|
-/* 已退款,已回收的行样式 - 灰底色 */
|
|
|
:deep(.row-refunded-revoked) {
|
|
|
background-color: #f5f5f5 !important;
|
|
|
}
|
|
|
@@ -496,4 +711,47 @@ onMounted(() => {
|
|
|
:deep(.row-refunded-revoked:hover) {
|
|
|
background-color: #e8e8e8 !important;
|
|
|
}
|
|
|
+
|
|
|
+/* 统计信息展开动画 */
|
|
|
+.stats-container {
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+/* 展开内容动画 */
|
|
|
+.stats-container > div:last-child {
|
|
|
+ animation: slideDown 0.3s ease-out;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes slideDown {
|
|
|
+ from {
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(-10px);
|
|
|
+ }
|
|
|
+ to {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .stats-container {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .flex-wrap > span {
|
|
|
+ flex: 1 0 calc(50% - 0.5rem);
|
|
|
+ margin-bottom: 0.5rem;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-container > div:first-child {
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: flex-start;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .stats-container > div:first-child > div:first-child {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|