From c8b96b2e8c80d50ac50b9674033a718944b12ba1 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Fri, 24 Apr 2026 21:49:59 +0800 Subject: [PATCH] feat(pagination): implement pagination for users and posts management - Added pagination functionality to the users and posts management pages, allowing for better navigation through large datasets. - Updated API endpoints to support pagination parameters and return total counts for users and posts. - Introduced a reusable pagination component to enhance user experience and streamline page transitions. These changes improve the overall usability of the admin interface by enabling efficient data handling and display. --- app/pages/me/admin/users/index.vue | 61 +++++++++++++++++++++++++++++++++++++- app/pages/me/posts/index.vue | 59 ++++++++++++++++++++++++++++++++++-- server/api/admin/users.get.ts | 57 +++++++++++++++++++---------------- server/api/me/posts.get.ts | 7 +++-- server/service/posts/index.ts | 24 +++++++++++++++ 5 files changed, 176 insertions(+), 32 deletions(-) diff --git a/app/pages/me/admin/users/index.vue b/app/pages/me/admin/users/index.vue index 6b67954..a785fae 100644 --- a/app/pages/me/admin/users/index.vue +++ b/app/pages/me/admin/users/index.vue @@ -7,6 +7,8 @@ usePageTitle('用户管理') const { user, refresh } = useAuthSession() const { fetchData } = useClientApi() const toast = useToast() +const route = useRoute() +const router = useRouter() const rows = ref< { @@ -21,9 +23,25 @@ const rows = ref< }[] >([]) const loading = ref(true) +const page = ref(1) +const total = ref(0) +const pageSize = ref(50) const form = reactive({ username: '', password: '', email: '' }) const creating = ref(false) +function parsePage(raw: unknown): number { + const n = + typeof raw === 'string' + ? Number.parseInt(raw, 10) + : typeof raw === 'number' + ? raw + : Number.NaN + if (!Number.isFinite(n) || n < 1) { + return 1 + } + return Math.floor(n) +} + async function copyPublicUrl(publicSlug: string) { const href = buildPublicProfileAbsoluteUrl(window.location.origin, publicSlug) try { @@ -44,14 +62,46 @@ async function ensureAdmin() { async function load() { loading.value = true try { - const { users } = await fetchData<{ users: typeof rows.value }>('/api/admin/users') + const url = page.value > 1 ? `/api/admin/users?page=${page.value}` : '/api/admin/users' + const { users, total: count, pageSize: size } = await fetchData<{ + users: typeof rows.value + total: number + pageSize: number + }>(url) rows.value = users + total.value = count + pageSize.value = size } finally { loading.value = false } } +function onPageChange(nextPage: number) { + page.value = nextPage + const query = { ...route.query } + if (nextPage > 1) { + query.page = String(nextPage) + } else { + delete query.page + } + void router.replace({ query }) + void load() +} + +watch( + () => route.query.page, + () => { + const next = parsePage(route.query.page) + if (next === page.value) { + return + } + page.value = next + void load() + }, +) + onMounted(async () => { + page.value = parsePage(route.query.page) await ensureAdmin() await load() }) @@ -205,6 +255,15 @@ async function setStatus(id: number, status: 'active' | 'disabled') { +
+ +
diff --git a/app/pages/me/posts/index.vue b/app/pages/me/posts/index.vue index 6b403cd..80cd33a 100644 --- a/app/pages/me/posts/index.vue +++ b/app/pages/me/posts/index.vue @@ -6,24 +6,70 @@ usePageTitle('我的文章') type Visibility = 'private' | 'unlisted' | 'public' type Row = { id: number; title: string; slug: string; visibility: Visibility } type ViewMode = 'list' | 'card' +type Payload = { items: Row[]; total: number; page: number; pageSize: number } const posts = ref([]) const loading = ref(true) const viewMode = ref('card') const { user } = useAuthSession() const { fetchData } = useClientApi() +const route = useRoute() +const router = useRouter() +const page = ref(1) +const total = ref(0) +const pageSize = ref(20) + +function parsePage(raw: unknown): number { + const n = + typeof raw === 'string' + ? Number.parseInt(raw, 10) + : typeof raw === 'number' + ? raw + : Number.NaN + if (!Number.isFinite(n) || n < 1) { + return 1 + } + return Math.floor(n) +} async function load() { loading.value = true try { - const { posts: list } = await fetchData<{ posts: Row[] }>('/api/me/posts') - posts.value = list + const url = page.value > 1 ? `/api/me/posts?page=${page.value}` : '/api/me/posts' + const data = await fetchData(url) + posts.value = data.items + total.value = data.total + pageSize.value = data.pageSize } finally { loading.value = false } } +function onPageChange(nextPage: number) { + page.value = nextPage + const query = { ...route.query } + if (nextPage > 1) { + query.page = String(nextPage) + } else { + delete query.page + } + void router.replace({ query }) +} + +watch( + () => route.query.page, + () => { + const next = parsePage(route.query.page) + if (next === page.value) { + return + } + page.value = next + void load() + }, +) + onMounted(() => { + page.value = parsePage(route.query.page) void load() }) @@ -153,5 +199,14 @@ function visibilityLabel(visibility: string) { +
+ +
diff --git a/server/api/admin/users.get.ts b/server/api/admin/users.get.ts index 4625c5e..97fd7e3 100644 --- a/server/api/admin/users.get.ts +++ b/server/api/admin/users.get.ts @@ -4,37 +4,42 @@ import { posts, timelineEvents } from "drizzle-pkg/lib/schema/content"; import { rssFeeds } from "drizzle-pkg/lib/schema/rss"; import { desc, sql } from "drizzle-orm"; import { requireAdmin } from "#server/utils/admin-guard"; +import { normalizePublicListPage } from "#server/utils/public-pagination"; export default defineWrappedResponseHandler(async (event) => { await requireAdmin(event); const q = getQuery(event); - const limit = Math.min(Number(q.limit ?? 50) || 50, 100); - const offset = Math.max(Number(q.offset ?? 0) || 0, 0); + const pageSize = Math.min(Number(q.pageSize ?? q.limit ?? 50) || 50, 100); + const page = normalizePublicListPage(q.page); + const offset = Math.max(Number(q.offset ?? (page - 1) * pageSize) || 0, 0); - const rows = await dbGlobal - .select({ - id: users.id, - username: users.username, - email: users.email, - role: users.role, - status: users.status, - publicSlug: users.publicSlug, - createdAt: users.createdAt, - postCount: sql`(select count(*) from ${posts} where ${posts.userId} = ${users.id})`.mapWith( - Number, - ), - timelineEventCount: sql`(select count(*) from ${timelineEvents} where ${timelineEvents.userId} = ${users.id})`.mapWith( - Number, - ), - rssFeedCount: sql`(select count(*) from ${rssFeeds} where ${rssFeeds.userId} = ${users.id})`.mapWith( - Number, - ), - }) - .from(users) - .orderBy(desc(users.id)) - .limit(limit) - .offset(offset); + const [countRows, rows] = await Promise.all([ + dbGlobal.select({ total: sql`count(*)`.mapWith(Number) }).from(users), + dbGlobal + .select({ + id: users.id, + username: users.username, + email: users.email, + role: users.role, + status: users.status, + publicSlug: users.publicSlug, + createdAt: users.createdAt, + postCount: sql`(select count(*) from ${posts} where ${posts.userId} = ${users.id})`.mapWith( + Number, + ), + timelineEventCount: sql`(select count(*) from ${timelineEvents} where ${timelineEvents.userId} = ${users.id})`.mapWith( + Number, + ), + rssFeedCount: sql`(select count(*) from ${rssFeeds} where ${rssFeeds.userId} = ${users.id})`.mapWith( + Number, + ), + }) + .from(users) + .orderBy(desc(users.id)) + .limit(pageSize) + .offset(offset), + ]); - return R.success({ users: rows }); + return R.success({ users: rows, total: countRows[0]?.total ?? 0, page, pageSize }); }); diff --git a/server/api/me/posts.get.ts b/server/api/me/posts.get.ts index 3c94b34..58b578e 100644 --- a/server/api/me/posts.get.ts +++ b/server/api/me/posts.get.ts @@ -1,7 +1,8 @@ -import { listPostsForUser } from "#server/service/posts"; +import { listPostsPageForUser } from "#server/service/posts"; export default defineWrappedResponseHandler(async (event) => { const user = await event.context.auth.requireUser(); - const rows = await listPostsForUser(user.id); - return R.success({ posts: rows }); + const q = getQuery(event); + const payload = await listPostsPageForUser(user.id, q.page); + return R.success(payload); }); diff --git a/server/service/posts/index.ts b/server/service/posts/index.ts index f7c5415..6debd48 100644 --- a/server/service/posts/index.ts +++ b/server/service/posts/index.ts @@ -27,6 +27,30 @@ export async function listPostsForUser(userId: number) { .orderBy(desc(posts.publishedAt), desc(posts.id)); } +export async function listPostsPageForUser(userId: number, pageRaw: unknown, pageSize = 20) { + const page = normalizePublicListPage(pageRaw); + const offset = (page - 1) * pageSize; + const [countRows, items] = await Promise.all([ + dbGlobal + .select({ total: count() }) + .from(posts) + .where(eq(posts.userId, userId)), + dbGlobal + .select() + .from(posts) + .where(eq(posts.userId, userId)) + .orderBy(desc(posts.publishedAt), desc(posts.id)) + .limit(pageSize) + .offset(offset), + ]); + return { + items, + total: countRows[0]?.total ?? 0, + page, + pageSize, + }; +} + export async function getPostForUser(userId: number, id: number) { const [row] = await dbGlobal .select()