feat: 添加用户统计功能,更新用户状态接口,优化统计组件

This commit is contained in:
2025-12-17 15:09:52 +08:00
parent e4c8deaacf
commit 2f03dcf8d8
4 changed files with 140 additions and 22 deletions

View File

@@ -1,7 +1,8 @@
<script setup>
import StatisticsStrip from '@/components/StatisticsStrip.vue';
import { UserService } from '@/service/UserService';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
const toast = useToast();
@@ -46,6 +47,57 @@ const statusOptions = ref([]);
const statusUser = ref(null);
const statusValue = ref(null);
const statistics = ref([]);
const statisticsLoading = ref(false);
const statisticsItems = computed(() => {
const total = (statistics.value || []).reduce((sum, row) => sum + (Number(row?.count) || 0), 0);
const statusIcon = (status) => {
switch (status) {
case 'verified':
return 'pi-check-circle';
case 'pending_verify':
return 'pi-clock';
case 'banned':
return 'pi-ban';
default:
return 'pi-tag';
}
};
const valueClass = (status) => {
switch (status) {
case 'banned':
return 'text-red-500';
case 'pending_verify':
return 'text-orange-500';
default:
return '';
}
};
return [
{ key: 'total', label: '用户总数:', value: statisticsLoading.value ? '-' : total, icon: 'pi-users' },
...(statistics.value || []).map((row) => ({
key: row?.status ?? row?.status_description,
label: `${row?.status_description || row?.status || '-'}`,
value: statisticsLoading.value ? '-' : (row?.count ?? 0),
icon: statusIcon(row?.status),
valueClass: valueClass(row?.status)
}))
];
});
async function loadStatistics() {
statisticsLoading.value = true;
try {
statistics.value = await UserService.getUserStatistics();
} catch (error) {
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载用户统计信息', life: 4000 });
} finally {
statisticsLoading.value = false;
}
}
async function ensureStatusOptionsLoaded() {
if (statusOptions.value.length > 0) return;
const list = await UserService.getUserStatuses();
@@ -82,6 +134,7 @@ async function confirmUpdateStatus() {
toast.add({ severity: 'success', summary: '更新成功', detail: `用户ID: ${userID}`, life: 3000 });
statusDialogVisible.value = false;
await loadUsers();
await loadStatistics();
} catch (error) {
toast.add({ severity: 'error', summary: '更新失败', detail: error?.message || '无法更新用户状态', life: 4000 });
} finally {
@@ -141,11 +194,13 @@ function onSort(event) {
onMounted(() => {
loadUsers();
loadStatistics();
});
</script>
<template>
<div>
<StatisticsStrip :items="statisticsItems" containerClass="card mb-4" />
<div class="card">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div class="flex items-center gap-2">
@@ -163,19 +218,32 @@ onMounted(() => {
</div>
</div>
<DataTable :value="users" dataKey="id" :loading="loading" lazy :paginator="true" :rows="rows"
:totalRecords="totalRecords" :first="(page - 1) * rows" :rowsPerPageOptions="[10, 20, 50, 100]"
sortMode="single" :sortField="sortField" :sortOrder="sortOrder" @page="onPage" @sort="onSort"
<DataTable
:value="users"
dataKey="id"
:loading="loading"
lazy
:paginator="true"
:rows="rows"
:totalRecords="totalRecords"
:first="(page - 1) * rows"
:rowsPerPageOptions="[10, 20, 50, 100]"
sortMode="single"
:sortField="sortField"
:sortOrder="sortOrder"
@page="onPage"
@sort="onSort"
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
scrollable scrollHeight="flex" responsiveLayout="scroll">
scrollable
scrollHeight="flex"
responsiveLayout="scroll"
>
<Column field="id" header="ID" sortable style="min-width: 6rem" />
<Column field="username" header="用户名" sortable style="min-width: 14rem" />
<Column field="status" header="状态" sortable style="min-width: 10rem">
<template #body="{ data }">
<Tag :value="data.status_description || data.status || '-'"
:severity="getStatusSeverity(data.status)" class="cursor-pointer"
@click="openStatusDialog(data)" />
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" class="cursor-pointer" @click="openStatusDialog(data)" />
</template>
</Column>
<Column field="roles" header="角色" style="min-width: 16rem">
@@ -214,15 +282,12 @@ onMounted(() => {
<div class="flex flex-col gap-4">
<div>
<label class="block font-medium mb-2">用户状态</label>
<Select v-model="statusValue" :options="statusOptions" optionLabel="label" optionValue="value"
placeholder="选择状态" :disabled="statusLoading" fluid />
<Select v-model="statusValue" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="选择状态" :disabled="statusLoading" fluid />
</div>
</div>
<template #footer>
<Button label="取消" icon="pi pi-times" text @click="statusDialogVisible = false"
:disabled="statusLoading" />
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusLoading"
:disabled="!statusValue" />
<Button label="取消" icon="pi pi-times" text @click="statusDialogVisible = false" :disabled="statusLoading" />
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusLoading" :disabled="!statusValue" />
</template>
</Dialog>
</div>