197 lines
7.3 KiB
Vue
197 lines
7.3 KiB
Vue
<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>
|