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()