Compare commits
3 Commits
9efd188628
...
920bbc4c5a
| Author | SHA1 | Date | |
|---|---|---|---|
| 920bbc4c5a | |||
| 2f03dcf8d8 | |||
| e4c8deaacf |
@@ -13,7 +13,8 @@ type TenantFilter struct {
|
||||
requests.Pagination
|
||||
requests.SortQueryFilter
|
||||
|
||||
Name *string `json:"name,omitempty" query:"name"`
|
||||
Name *string `json:"name,omitempty" query:"name"`
|
||||
Status *string `json:"status,omitempty" query:"status"`
|
||||
}
|
||||
|
||||
type TenantItem struct {
|
||||
|
||||
@@ -11,6 +11,7 @@ type UserPageFilter struct {
|
||||
requests.SortQueryFilter
|
||||
|
||||
Username *string `query:"username"`
|
||||
Status *string `query:"status"`
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
}
|
||||
|
||||
@@ -23,3 +24,9 @@ type UserItem struct {
|
||||
type UserStatusUpdateForm struct {
|
||||
Status consts.UserStatus `json:"status" validate:"required,oneof=normal disabled"`
|
||||
}
|
||||
|
||||
type UserStatistics struct {
|
||||
Status consts.UserStatus `json:"status"`
|
||||
StatusDescription string `json:"status_description"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
r.user.list,
|
||||
Query[dto.UserPageFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/users/statistics -> user.statistics")
|
||||
router.Get("/super/v1/users/statistics", DataFunc0(
|
||||
r.user.statistics,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/users/statuses -> user.statusList")
|
||||
router.Get("/super/v1/users/statuses", DataFunc0(
|
||||
r.user.statusList,
|
||||
|
||||
@@ -61,3 +61,18 @@ func (*user) statusList(ctx fiber.Ctx) ([]requests.KV, error) {
|
||||
return requests.NewKV(item.String(), item.Description())
|
||||
}), nil
|
||||
}
|
||||
|
||||
// statistics
|
||||
//
|
||||
// @Summary 用户统计信息
|
||||
// @Tags Super
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.UserStatistics
|
||||
//
|
||||
// @Router /super/v1/users/statistics [get]
|
||||
// @Bind userID path
|
||||
// @Bind form body
|
||||
func (*user) statistics(ctx fiber.Ctx) ([]*dto.UserStatistics, error) {
|
||||
return services.User.Statistics(ctx)
|
||||
}
|
||||
|
||||
@@ -107,3 +107,19 @@ func (t *user) UpdateStatus(ctx context.Context, userID int64, status consts.Use
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Statistics
|
||||
func (t *user) Statistics(ctx context.Context) ([]*dto.UserStatistics, error) {
|
||||
tbl, query := models.UserQuery.QueryContext(ctx)
|
||||
|
||||
var statistics []*dto.UserStatistics
|
||||
err := query.Select(tbl.Status, tbl.ID.Count().As("count")).Group(tbl.Status).Scan(&statistics)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lo.Map(statistics, func(item *dto.UserStatistics, _ int) *dto.UserStatistics {
|
||||
item.StatusDescription = item.Status.Description()
|
||||
return item
|
||||
}), nil
|
||||
}
|
||||
|
||||
@@ -261,3 +261,15 @@ func (t *UserTestSuite) Test_Relations() {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (t *UserTestSuite) Test_Statistics() {
|
||||
Convey("test page", t.T(), func() {
|
||||
Convey("filter tenant users", func() {
|
||||
m, err := User.Statistics(t.T().Context())
|
||||
So(err, ShouldBeNil)
|
||||
// So(m.OwnedTenant, ShouldNotBeNil)
|
||||
// So(m.Tenants, ShouldHaveLength, 10)
|
||||
t.T().Logf("%s", utils.MustJsonString(m))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ const docTemplate = `{
|
||||
"type": "integer",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -243,6 +248,11 @@ const docTemplate = `{
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tenantID",
|
||||
@@ -276,6 +286,31 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/users/statistics": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Super"
|
||||
],
|
||||
"summary": "用户统计信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.UserStatistics"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/users/statuses": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -536,6 +571,20 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UserStatistics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/consts.UserStatus"
|
||||
},
|
||||
"status_description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UserStatusUpdateForm": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
"type": "integer",
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -237,6 +242,11 @@
|
||||
"name": "page",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "tenantID",
|
||||
@@ -270,6 +280,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/users/statistics": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Super"
|
||||
],
|
||||
"summary": "用户统计信息",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/dto.UserStatistics"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/super/v1/users/statuses": {
|
||||
"get": {
|
||||
"consumes": [
|
||||
@@ -530,6 +565,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UserStatistics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/consts.UserStatus"
|
||||
},
|
||||
"status_description": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dto.UserStatusUpdateForm": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
||||
@@ -134,6 +134,15 @@ definitions:
|
||||
verified_at:
|
||||
type: string
|
||||
type: object
|
||||
dto.UserStatistics:
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
status:
|
||||
$ref: '#/definitions/consts.UserStatus'
|
||||
status_description:
|
||||
type: string
|
||||
type: object
|
||||
dto.UserStatusUpdateForm:
|
||||
properties:
|
||||
status:
|
||||
@@ -288,6 +297,9 @@ paths:
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: status
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
@@ -382,6 +394,9 @@ paths:
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
- in: query
|
||||
name: status
|
||||
type: string
|
||||
- in: query
|
||||
name: tenantID
|
||||
type: integer
|
||||
@@ -426,6 +441,22 @@ paths:
|
||||
summary: 更新用户状态
|
||||
tags:
|
||||
- Super
|
||||
/super/v1/users/statistics:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/dto.UserStatistics'
|
||||
type: array
|
||||
summary: 用户统计信息
|
||||
tags:
|
||||
- Super
|
||||
/super/v1/users/statuses:
|
||||
get:
|
||||
consumes:
|
||||
|
||||
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-VWI_AnCM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-B0E0aOXs.css">
|
||||
<script type="module" crossorigin src="./assets/index-PcBKlZrK.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BDK8BdlV.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
24
frontend/superadmin/src/components/SearchField.vue
Normal file
24
frontend/superadmin/src/components/SearchField.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
label: { type: String, required: true },
|
||||
forId: { type: String, default: '' },
|
||||
colClass: { type: String, default: 'col-span-12 md:col-span-6 lg:col-span-4' },
|
||||
labelClass: { type: String, default: 'w-28 text-right' }
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="props.colClass">
|
||||
<div class="flex items-center gap-3">
|
||||
<label v-if="props.forId" :for="props.forId"
|
||||
class="text-sm font-medium text-surface-900 dark:text-surface-0" :class="props.labelClass">
|
||||
{{ props.label }}
|
||||
</label>
|
||||
<span v-else class="text-sm font-medium text-surface-900 dark:text-surface-0" :class="props.labelClass">{{
|
||||
props.label }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
44
frontend/superadmin/src/components/SearchPanel.vue
Normal file
44
frontend/superadmin/src/components/SearchPanel.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { Comment, computed, ref, useSlots } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
collapsedCount: { type: Number, default: 3 },
|
||||
initialExpanded: { type: Boolean, default: false },
|
||||
loading: { type: Boolean, default: false }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['search', 'reset']);
|
||||
|
||||
const slots = useSlots();
|
||||
const expanded = ref(props.initialExpanded);
|
||||
|
||||
const fieldNodes = computed(() => {
|
||||
const nodes = slots.default ? slots.default() : [];
|
||||
return (nodes || []).filter((n) => n && n.type !== Comment);
|
||||
});
|
||||
|
||||
const canToggle = computed(() => fieldNodes.value.length > props.collapsedCount);
|
||||
const visibleFields = computed(() => (expanded.value ? fieldNodes.value : fieldNodes.value.slice(0, props.collapsedCount)));
|
||||
|
||||
function onToggle() {
|
||||
expanded.value = !expanded.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mb-4 p-4 rounded border border-surface-200 dark:border-surface-700 bg-surface-0 dark:bg-surface-900">
|
||||
<div class="flex flex-col lg:flex-row lg:items-end gap-4 lg:gap-6">
|
||||
<div class="flex-1 grid grid-cols-12 gap-x-6 gap-y-4 items-end">
|
||||
<template v-for="(node, idx) in visibleFields" :key="idx">
|
||||
<component :is="node" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 lg:flex-none">
|
||||
<Button label="查询" icon="pi pi-search" :loading="props.loading" @click="emit('search')" />
|
||||
<Button label="重置" icon="pi pi-refresh" severity="secondary" outlined :disabled="props.loading" @click="emit('reset')" />
|
||||
<Button v-if="canToggle" :label="expanded ? '收起' : '展开'" :icon="expanded ? 'pi pi-angle-up' : 'pi pi-angle-down'" iconPos="right" severity="secondary" text :disabled="props.loading" @click="onToggle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
29
frontend/superadmin/src/components/StatisticsStrip.vue
Normal file
29
frontend/superadmin/src/components/StatisticsStrip.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: 'card'
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="props.containerClass">
|
||||
<div class="flex flex-wrap items-center justify-between gap-6">
|
||||
<div v-for="(item, idx) in props.items" :key="item.key ?? idx" class="flex items-center gap-4 flex-1 min-w-[220px]">
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center bg-surface-100 dark:bg-surface-800">
|
||||
<i class="pi text-primary text-xl" :class="item.icon" />
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-muted-color">{{ item.label }}</span>
|
||||
<span class="text-surface-900 dark:text-surface-0 text-xl font-semibold" :class="item.valueClass">{{ item.value }}</span>
|
||||
</div>
|
||||
<div v-if="idx !== props.items.length - 1" class="hidden xl:block w-px self-stretch bg-surface-200 dark:bg-surface-700 ml-auto"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,13 +23,37 @@ export const UserService = {
|
||||
};
|
||||
},
|
||||
async getUserStatuses() {
|
||||
const data = await requestJson('/super/v1/users/statuses');
|
||||
return Array.isArray(data) ? data : [];
|
||||
try {
|
||||
const data = await requestJson('/super/v1/user/statuses');
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
if (error?.status === 404) {
|
||||
const data = await requestJson('/super/v1/users/statuses');
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async updateUserStatus({ userID, status }) {
|
||||
return requestJson(`/super/v1/users/${userID}/status`, {
|
||||
method: 'PATCH',
|
||||
body: { status }
|
||||
});
|
||||
try {
|
||||
return await requestJson(`/super/v1/user/${userID}/status`, { method: 'PATCH', body: { status } });
|
||||
} catch (error) {
|
||||
if (error?.status === 404) {
|
||||
return requestJson(`/super/v1/users/${userID}/status`, { method: 'PATCH', body: { status } });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async getUserStatistics() {
|
||||
try {
|
||||
const data = await requestJson('/super/v1/users/statistics');
|
||||
return Array.isArray(data) ? data : [];
|
||||
} catch (error) {
|
||||
if (error?.status === 404) {
|
||||
const data = await requestJson('/super/v1/users/statistic');
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup>
|
||||
import { TenantService } from '@/service/TenantService';
|
||||
import SearchField from '@/components/SearchField.vue';
|
||||
import SearchPanel from '@/components/SearchPanel.vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
@@ -198,21 +200,20 @@ onMounted(() => {
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">租户列表</h4>
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||
<SearchField label="名称 / Code">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="keyword" placeholder="名称 / Code" @keyup.enter="onSearch" />
|
||||
<InputText v-model="keyword" placeholder="请输入" class="w-full" @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>
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable
|
||||
:value="tenants"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup>
|
||||
import SearchField from '@/components/SearchField.vue';
|
||||
import SearchPanel from '@/components/SearchPanel.vue';
|
||||
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 +49,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 +136,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,27 +196,28 @@ 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">
|
||||
<h4 class="m-0">用户列表</h4>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="m-0">用户列表</h4>
|
||||
</div>
|
||||
|
||||
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||
<SearchField label="用户名">
|
||||
<IconField>
|
||||
<InputIcon>
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText v-model="username" placeholder="用户名" @keyup.enter="onSearch" />
|
||||
<InputText v-model="username" placeholder="请输入" class="w-full" @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>
|
||||
</SearchField>
|
||||
</SearchPanel>
|
||||
|
||||
<DataTable :value="users" dataKey="id" :loading="loading" lazy :paginator="true" :rows="rows"
|
||||
:totalRecords="totalRecords" :first="(page - 1) * rows" :rowsPerPageOptions="[10, 20, 50, 100]"
|
||||
|
||||
Reference in New Issue
Block a user