feat: add user_list
This commit is contained in:
4
frontend/superadmin/dist/index.html
vendored
4
frontend/superadmin/dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
25
frontend/superadmin/src/service/UserService.js
Normal file
25
frontend/superadmin/src/service/UserService.js
Normal 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)
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
176
frontend/superadmin/src/views/superadmin/Users.vue
Normal file
176
frontend/superadmin/src/views/superadmin/Users.vue
Normal 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>
|
||||
Reference in New Issue
Block a user