diff --git a/web/src/features/review/use-review-list.ts b/web/src/features/review/use-review-list.ts index e90575be..225e00ed 100644 --- a/web/src/features/review/use-review-list.ts +++ b/web/src/features/review/use-review-list.ts @@ -1,22 +1,26 @@ import { useQuery } from '@tanstack/react-query' import { reviewApi } from '@/api/client' -import type { ReviewTask } from '@/api/types' /** * Loads review tasks filtered by status and optional namespace scope. + * Returns paginated response with totalElements / totalPages. */ -async function getReviewList(status: string, namespaceId?: number): Promise { - const page = await reviewApi.list({ status, namespaceId }) - return page.items +async function getReviewList(status: string, namespaceId?: number, page = 0, size = 20) { + const response = await reviewApi.list({ status, namespaceId, page, size }) + return { + ...response, + totalElements: response.total, + totalPages: response.size > 0 ? Math.ceil(response.total / response.size) : 0, + } } /** * Exposes the review list query used by dashboard moderation views. */ -export function useReviewList(status: string, namespaceId?: number) { +export function useReviewList(status: string, namespaceId?: number, page = 0, size = 20) { return useQuery({ - queryKey: ['reviews', status, namespaceId], - queryFn: () => getReviewList(status, namespaceId), + queryKey: ['reviews', status, namespaceId, page, size], + queryFn: () => getReviewList(status, namespaceId, page, size), enabled: namespaceId === undefined || namespaceId > 0, }) } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index abf37f4c..9d781539 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -414,7 +414,10 @@ "colSubmitter": "Submitted By", "colSubmitTime": "Submitted At", "colReviewer": "Reviewed By", - "colReviewTime": "Reviewed At" + "colReviewTime": "Reviewed At", + "pageSummary": "Total {{total}} records, page {{page}}", + "prevPage": "Previous", + "nextPage": "Next" }, "profileReview": { "queueTitle": "Profile Review Queue", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index c5ab2755..8091741d 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -414,7 +414,10 @@ "colSubmitter": "提交者", "colSubmitTime": "提交时间", "colReviewer": "审核者", - "colReviewTime": "审核时间" + "colReviewTime": "审核时间", + "pageSummary": "共 {{total}} 条记录,第 {{page}} 页", + "prevPage": "上一页", + "nextPage": "下一页" }, "profileReview": { "queueTitle": "资料审核队列", diff --git a/web/src/pages/dashboard/namespace-reviews.tsx b/web/src/pages/dashboard/namespace-reviews.tsx index e1a9dc8f..dfc03d0f 100644 --- a/web/src/pages/dashboard/namespace-reviews.tsx +++ b/web/src/pages/dashboard/namespace-reviews.tsx @@ -15,12 +15,13 @@ function ReviewListSection({ namespaceId }: { namespaceId?: number }) { const { data: rejected } = useReviewList('REJECTED', namespaceId) const renderItems = (items?: typeof pending) => { - if (!items || items.length === 0) { + const list = items?.items + if (!list || list.length === 0) { return {t('nsReviews.empty')} } return ( - {items.map((review) => ( + {list.map((review) => (
diff --git a/web/src/pages/dashboard/reviews.tsx b/web/src/pages/dashboard/reviews.tsx index 9d90a5ee..5bdca8d1 100644 --- a/web/src/pages/dashboard/reviews.tsx +++ b/web/src/pages/dashboard/reviews.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react' import { useNavigate } from '@tanstack/react-router' import { FileCheck2 } from 'lucide-react' import { useTranslation } from 'react-i18next' +import { Button } from '@/shared/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs' import { @@ -17,6 +19,9 @@ import { DashboardPageHeader } from '@/shared/components/dashboard-page-header' import { formatLocalDateTime } from '@/shared/lib/date-time' import { ProfileReviewTable } from './profile-review-table' +type ReviewStatus = 'PENDING' | 'APPROVED' | 'REJECTED' +const PAGE_SIZE = 20 + /** * Dashboard review queue page. Each tab materializes one review status because * the moderation workflow treats pending, approved, and rejected queues as @@ -26,6 +31,11 @@ export function ReviewsPage() { const { t, i18n } = useTranslation() const navigate = useNavigate() const { hasRole } = useAuth() + const [pages, setPages] = useState>({ + PENDING: 0, + APPROVED: 0, + REJECTED: 0, + }) const isSkillAdmin = hasRole('SKILL_ADMIN') || hasRole('SUPER_ADMIN') const isUserAdmin = hasRole('USER_ADMIN') || hasRole('SUPER_ADMIN') @@ -34,9 +44,9 @@ export function ReviewsPage() { // Determine default top-level tab const defaultType = isSkillAdmin ? 'skill' : 'profile' - const { data: pendingReviews, isLoading: isPendingLoading } = useReviewList('PENDING') - const { data: approvedReviews, isLoading: isApprovedLoading } = useReviewList('APPROVED') - const { data: rejectedReviews, isLoading: isRejectedLoading } = useReviewList('REJECTED') + const pendingQuery = useReviewList('PENDING', undefined, pages.PENDING, PAGE_SIZE) + const approvedQuery = useReviewList('APPROVED', undefined, pages.APPROVED, PAGE_SIZE) + const rejectedQuery = useReviewList('REJECTED', undefined, pages.REJECTED, PAGE_SIZE) const formatDate = (dateString: string) => formatLocalDateTime(dateString, i18n.language) @@ -44,12 +54,46 @@ export function ReviewsPage() { navigate({ to: `/dashboard/reviews/${reviewId}` }) } + function changePage(status: ReviewStatus, nextPage: number) { + setPages((current) => ({ ...current, [status]: nextPage })) + } + + function renderPagination(status: ReviewStatus, totalElements: number, totalPages: number) { + if (totalPages <= 1) return null + const currentPage = pages[status] + return ( +
+

{t('reviews.pageSummary', { total: totalElements, page: currentPage + 1 })}

+
+ + +
+
+ ) + } + /** * Keeps the table rendering logic in one local helper so the per-status tabs * stay declarative while still allowing columns to differ by workflow state. */ - const renderReviewTable = (reviews: typeof pendingReviews, isLoading: boolean, status: string) => { - if (isLoading) { + const renderReviewTable = (query: typeof pendingQuery, status: ReviewStatus) => { + if (query.isLoading) { return (
{Array.from({ length: 4 }).map((_, index) => ( @@ -58,6 +102,7 @@ export function ReviewsPage() {
) } + const reviews = query.data?.items if (!reviews || reviews.length === 0) { return
{t('reviews.empty')}
} @@ -101,6 +146,7 @@ export function ReviewsPage() { ))} + {query.data && renderPagination(status, query.data.totalElements, query.data.totalPages)}
) } @@ -144,13 +190,13 @@ export function ReviewsPage() { - {renderReviewTable(pendingReviews, isPendingLoading, 'PENDING')} + {renderReviewTable(pendingQuery, 'PENDING')} - {renderReviewTable(approvedReviews, isApprovedLoading, 'APPROVED')} + {renderReviewTable(approvedQuery, 'APPROVED')} - {renderReviewTable(rejectedReviews, isRejectedLoading, 'REJECTED')} + {renderReviewTable(rejectedQuery, 'REJECTED')}