Files
quyun/frontend/admin/src/pages/UserPage.vue
2025-05-15 10:25:18 +08:00

197 lines
7.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { userService } from '@/api/userService';
import { formatDate } from '@/utils/date';
import Badge from 'primevue/badge';
import Button from 'primevue/button';
import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import InputText from 'primevue/inputtext';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
const toast = useToast();
const confirm = useConfirm();
const globalFilterValue = ref('');
const loading = ref(false);
const searchTimeout = ref(null);
const filters = ref({
global: { value: null, matchMode: 'contains' },
status: { value: null, matchMode: 'equals' }
});
// Table state
const users = ref({
items: [],
total: 0,
page: 1,
limit: 10
});
// DataTable pagination state
const first = ref(0);
const rows = ref(10);
const fetchUsers = async () => {
loading.value = true;
try {
// Calculate current page from first and rows
const currentPage = (first.value / rows.value) + 1;
const response = await userService.getUsers({
page: currentPage,
limit: rows.value,
keyword: globalFilterValue.value
});
users.value = response.data;
} catch (error) {
console.error('Failed to fetch users:', error);
toast.add({ severity: 'error', summary: '错误', detail: '加载用户数据失败', life: 3000 });
} finally {
loading.value = false;
}
};
const onPage = (event) => {
first.value = event.first;
rows.value = event.rows;
fetchUsers();
};
const onSearch = (event) => {
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
}
searchTimeout.value = setTimeout(() => {
first.value = 0; // Reset to first page when searching
fetchUsers();
}, 300);
};
const handleRecharge = (user) => {
confirm.require({
message: `确定要为用户ID ${user.id} "${user.username}" 充值吗?`,
header: '确认充值',
icon: 'pi pi-exclamation-triangle',
acceptClass: 'p-button-success',
accept: async () => {
try {
const amount = prompt('请输入充值金额(元):');
if (!amount) return;
const amountInCents = Math.floor(parseFloat(amount) * 100);
if (isNaN(amountInCents) || amountInCents <= 0) {
toast.add({ severity: 'error', summary: '错误', detail: '请输入有效的金额', life: 3000 });
return;
}
await userService.userBalance(user.id, amountInCents);
toast.add({ severity: 'success', summary: '成功', detail: '用户已充值', life: 3000 });
fetchUsers();
} catch (error) {
console.error('Failed to recharge user:', error);
toast.add({ severity: 'error', summary: '错误', detail: '充值用户失败', life: 3000 });
}
}
});
};
onMounted(() => {
fetchUsers();
});
</script>
<template>
<Toast />
<ConfirmDialog />
<div class="w-full">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-800">用户列表</h1>
</div>
<div class="card">
<div class="pb-10 flex">
<InputText v-model="globalFilterValue" placeholder="搜索用户..." class="flex-1" @input="onSearch" />
</div>
<DataTable v-model:filters="filters" :value="users.items" :paginator="true" :rows="rows"
:totalRecords="users.total" :loading="loading" :lazy="true" :first="first" @page="onPage"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
:rowsPerPageOptions="[10, 25, 50]"
currentPageReportTemplate="显示第 {first} 到 {last} 条,共 {totalRecords} 条结果" dataKey="id"
:globalFilterFields="['username', 'open_id']" stripedRows removableSort class="p-datatable-sm"
responsiveLayout="scroll">
<template #empty>
<div class="text-center p-4">未找到用户</div>
</template>
<template #loading>
<div class="flex flex-col items-center justify-center p-4">
<ProgressSpinner style="width:50px;height:50px" />
<span class="mt-2">加载用户数据...</span>
</div>
</template>
<Column field="id" header="ID" sortable></Column>
<Column field="balance" header="余额" sortable>
<template #body="{ data }">
¥{{ (data.balance / 100).toFixed(2) }}
</template>
</Column>
<Column field="username" header="用户名" sortable>
<template #body="{ data }">
<div class="flex items-center space-x-3">
<div class="avatar">
<div class="mask mask-squircle w-12 h-12">
<img :src="data.avatar" :alt="data.username" />
</div>
</div>
<div>
<router-link :to="`/users/${data.id}`"
class="font-bold text-blue-600 hover:text-blue-800">
{{ data.username }}
</router-link>
</div>
</div>
</template>
</Column>
<Column field="open_id" header="OpenID" sortable></Column>
<Column field="status" header="状态" sortable>
<template #body="{ data }">
<Badge :value="data.status === 0 ? '活跃' : '禁用'"
:severity="data.status === 0 ? 'success' : 'danger'" />
</template>
</Column>
<Column field="updated_at" header="时间信息" sortable>
<template #body="{ data }">
<div class="flex flex-col">
<span class="text-gray-500">更新: {{ formatDate(data.updated_at) }}</span>
<span class="text-gray-400">创建: {{ formatDate(data.created_at) }}</span>
</div>
</template>
</Column>
<Column header="操作" :exportable="false" style="min-width:8rem">
<template #body="{ data }">
<div class="flex justify-center space-x-2">
<Button icon="pi pi-credit-card" rounded text severity="success"
@click="handleRecharge(data)" label="充值" />
</div>
</template>
</Column>
</DataTable>
</div>
</div>
</template>