feat: add user_list

This commit is contained in:
2025-12-17 13:24:32 +08:00
parent dae9a0e55a
commit 14842d989c
20 changed files with 736 additions and 130 deletions

View File

@@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sakai Vue</title>
<link href="https://fonts.cdnfonts.com/css/lato" rel="stylesheet">
<script type="module" crossorigin src="./assets/index-5TB6SaKe.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-7wg5D3fl.css">
<script type="module" crossorigin src="./assets/index-BRu67wro.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BMyA_4RT.css">
</head>
<body>

View File

@@ -10,7 +10,10 @@ const model = ref([
},
{
label: 'Super Admin',
items: [{ label: 'Tenants', icon: 'pi pi-fw pi-building', to: '/superadmin/tenants' }]
items: [
{ label: 'Tenants', icon: 'pi pi-fw pi-building', to: '/superadmin/tenants' },
{ label: 'Users', icon: 'pi pi-fw pi-users', to: '/superadmin/users' }
]
},
{
label: 'UI Components',

View File

@@ -116,6 +116,11 @@ const router = createRouter({
path: '/superadmin/tenants',
name: 'superadmin-tenants',
component: () => import('@/views/superadmin/Tenants.vue')
},
{
path: '/superadmin/users',
name: 'superadmin-users',
component: () => import('@/views/superadmin/Users.vue')
}
]
},

View File

@@ -0,0 +1,25 @@
import { requestJson } from './apiClient';
function normalizeItems(items) {
if (Array.isArray(items)) return items;
if (items && typeof items === 'object') return [items];
return [];
}
export const UserService = {
async listUsers({ page, limit, tenantID, username, sortField, sortOrder } = {}) {
const query = { page, limit, tenantID, username };
if (sortField && sortOrder) {
if (sortOrder === 1) query.asc = sortField;
if (sortOrder === -1) query.desc = sortField;
}
const data = await requestJson('/super/v1/users', { query });
return {
page: data?.page ?? page ?? 1,
limit: data?.limit ?? limit ?? 10,
total: data?.total ?? 0,
items: normalizeItems(data?.items)
};
}
};

View File

@@ -166,12 +166,27 @@ onMounted(() => {
</div>
</div>
<DataTable :value="tenants" 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="tenants"
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="code" header="Code" style="min-width: 10rem" />
<Column field="name" header="名称" sortable style="min-width: 14rem" />
@@ -184,8 +199,7 @@ onMounted(() => {
<Column field="user_balance" header="余额" sortable style="min-width: 8rem" />
<Column field="expired_at" header="过期时间" sortable style="min-width: 14rem">
<template #body="{ data }">
<span v-if="data.expired_at" v-tooltip="getExpiryDaysInfo(data.expired_at).tooltipText"
:class="getExpiryDaysInfo(data.expired_at).textClass">
<span v-if="data.expired_at" v-tooltip="getExpiryDaysInfo(data.expired_at).tooltipText" :class="getExpiryDaysInfo(data.expired_at).textClass">
{{ formatDate(data.expired_at) }}
</span>
<span v-else>-</span>
@@ -203,8 +217,7 @@ onMounted(() => {
</Column>
<Column header="操作" :exportable="false" style="min-width: 10rem">
<template #body="{ data }">
<Button label="续期" icon="pi pi-refresh" size="small" severity="secondary"
@click="openRenewDialog(data)" />
<Button label="续期" icon="pi pi-refresh" size="small" severity="secondary" @click="openRenewDialog(data)" />
</template>
</Column>
</DataTable>
@@ -219,8 +232,7 @@ onMounted(() => {
<div class="flex flex-col gap-4">
<div>
<label class="block font-medium mb-2">续期时长</label>
<Select v-model="renewDuration" :options="durationOptions" optionLabel="label" optionValue="value"
placeholder="选择续期时长" fluid />
<Select v-model="renewDuration" :options="durationOptions" optionLabel="label" optionValue="value" placeholder="选择续期时长" fluid />
</div>
</div>
<template #footer>

View File

@@ -0,0 +1,176 @@
<script setup>
import { UserService } from '@/service/UserService';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
const toast = useToast();
const users = ref([]);
const loading = ref(false);
const totalRecords = ref(0);
const page = ref(1);
const rows = ref(10);
const tenantID = ref(null);
const username = ref('');
const sortField = ref('id');
const sortOrder = ref(-1);
function formatDate(value) {
if (!value) return '-';
if (String(value).startsWith('0001-01-01')) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return String(value);
return date.toLocaleString();
}
function getStatusSeverity(status) {
switch (status) {
case 'active':
case 'verified':
return 'success';
case 'pending_verify':
case 'pending':
return 'warn';
case 'banned':
case 'disabled':
return 'danger';
default:
return 'secondary';
}
}
async function loadUsers() {
loading.value = true;
try {
const result = await UserService.listUsers({
page: page.value,
limit: rows.value,
tenantID: tenantID.value ?? undefined,
username: username.value,
sortField: sortField.value,
sortOrder: sortOrder.value
});
users.value = result.items;
totalRecords.value = result.total;
} catch (error) {
toast.add({
severity: 'error',
summary: '加载失败',
detail: error?.message || '无法加载用户列表',
life: 4000
});
} finally {
loading.value = false;
}
}
function onSearch() {
page.value = 1;
loadUsers();
}
function onReset() {
tenantID.value = null;
username.value = '';
sortField.value = 'id';
sortOrder.value = -1;
page.value = 1;
rows.value = 10;
loadUsers();
}
function onPage(event) {
page.value = (event.page ?? 0) + 1;
rows.value = event.rows ?? rows.value;
loadUsers();
}
function onSort(event) {
sortField.value = event.sortField ?? sortField.value;
sortOrder.value = event.sortOrder ?? sortOrder.value;
loadUsers();
}
onMounted(() => {
loadUsers();
});
</script>
<template>
<div>
<div class="card">
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<div class="flex items-center gap-2">
<h4 class="m-0">用户列表</h4>
<Tag :value="`总数:${totalRecords}`" severity="secondary" />
</div>
<div class="flex flex-wrap items-center gap-2">
<InputNumber v-model="tenantID" placeholder="TenantID" :useGrouping="false" class="w-40" />
<IconField>
<InputIcon>
<i class="pi pi-search" />
</InputIcon>
<InputText v-model="username" placeholder="用户名" @keyup.enter="onSearch" />
</IconField>
<Button label="查询" icon="pi pi-search" severity="secondary" @click="onSearch" />
<Button label="重置" icon="pi pi-refresh" severity="secondary" @click="onReset" />
</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"
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
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 || '-'" :severity="getStatusSeverity(data.status)" />
</template>
</Column>
<Column field="roles" header="角色" style="min-width: 16rem">
<template #body="{ data }">
<div class="flex flex-wrap gap-1">
<Tag v-for="role in data.roles || []" :key="role" :value="role" severity="secondary" />
<span v-if="!data.roles || data.roles.length === 0" class="text-muted-color">-</span>
</div>
</template>
</Column>
<Column field="verified_at" header="认证时间" sortable style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.verified_at) }}
</template>
</Column>
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.created_at) }}
</template>
</Column>
<Column field="updated_at" header="更新时间" sortable style="min-width: 14rem">
<template #body="{ data }">
{{ formatDate(data.updated_at) }}
</template>
</Column>
</DataTable>
</div>
</div>
</template>