ALIEZ 3 hafta önce
ebeveyn
işleme
7c2af1a0c7

+ 1 - 1
src/app/[locale]/account/page.tsx

@@ -76,7 +76,7 @@ export default function AccountPage() {
         setWalletBalance(balance);
         setOrders(orderRes.list);
         setPurchasedCourses(courseRes);
-        setWithdrawals(withdrawRes);
+        setWithdrawals(withdrawRes.list);
       } catch (e) {
         console.error(e);
       } finally {

+ 6 - 3
src/app/[locale]/account/withdrawals/page.tsx

@@ -19,6 +19,7 @@ export default function AccountWithdrawalsPage() {
   const [loading, setLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [page, setPage] = useState(1);
+  const [total, setTotal] = useState(0);
 
   useEffect(() => {
     if (!user) return;
@@ -27,16 +28,18 @@ export default function AccountWithdrawalsPage() {
       setLoading(true);
       setError(null);
       try {
-        const list = await fetchWithdrawalList({
+        const res = await fetchWithdrawalList({
           current: page,
           row: PAGE_SIZE,
         });
         if (cancelled) return;
-        setRecords(list);
+        setRecords(res.list);
+        setTotal(res.page.total);
       } catch (e) {
         if (cancelled) return;
         setError((e as Error).message || "领取记录加载失败,请稍后重试。");
         setRecords([]);
+        setTotal(0);
       } finally {
         if (!cancelled) setLoading(false);
       }
@@ -71,7 +74,7 @@ export default function AccountWithdrawalsPage() {
   }
 
   const hasPrev = page > 1;
-  const hasNext = records.length >= PAGE_SIZE;
+  const hasNext = page * PAGE_SIZE < total;
 
   function getStatusStyle(status: number | string) {
     const s = String(status).trim();

+ 42 - 60
src/data/courses.ts

@@ -1,5 +1,10 @@
 import { apiPost } from "@/lib/api";
 import { normalizeAssetUrl, toAbsoluteFileUrl } from "@/lib/media-url";
+import {
+  applyClientPageSlice,
+  extractPageMeta,
+  type PageMetaRaw,
+} from "@/lib/page-meta";
 
 export type GoodsType = 1 | 2 | 3 | 4 | 5;
 export type CustomType = 0 | 1 | 2;
@@ -188,12 +193,7 @@ type GoodsSearchListResponse = {
       };
   list?: GoodsSearchListItem[];
   records?: GoodsSearchListItem[];
-  page?: {
-    current?: number;
-    row?: number;
-    size?: number;
-    total?: number;
-  };
+  page?: PageMetaRaw;
 };
 
 type GoodsVideoListItem = {
@@ -223,12 +223,7 @@ type GoodsVideoListResponse = {
       };
   list?: GoodsVideoListItem[];
   records?: GoodsVideoListItem[];
-  page?: {
-    current?: number;
-    row?: number;
-    size?: number;
-    total?: number;
-  };
+  page?: PageMetaRaw;
 };
 
 function toCategory(goodsType: GoodsType): CourseCategory {
@@ -396,36 +391,6 @@ function normalizeVideo(item: GoodsVideoListItem): CourseVideo | null {
   };
 }
 
-function extractVideoPage(payload: GoodsVideoListResponse): {
-  current: number;
-  row: number;
-  total: number;
-} {
-  const current = Number(payload.page?.current);
-  const row = Number(payload.page?.row ?? payload.page?.size);
-  const total = Number(payload.page?.total);
-  return {
-    current: Number.isFinite(current) && current > 0 ? current : 1,
-    row: Number.isFinite(row) && row > 0 ? row : 10,
-    total: Number.isFinite(total) && total >= 0 ? total : 0,
-  };
-}
-
-function extractSearchPage(payload: GoodsSearchListResponse): {
-  current: number;
-  row: number;
-  total: number;
-} {
-  const current = Number(payload.page?.current);
-  const row = Number(payload.page?.row ?? payload.page?.size);
-  const total = Number(payload.page?.total);
-  return {
-    current: Number.isFinite(current) && current > 0 ? current : 1,
-    row: Number.isFinite(row) && row > 0 ? row : 10,
-    total: Number.isFinite(total) && total >= 0 ? total : 0,
-  };
-}
-
 function parseDownloadUrls(rawDownload: unknown): string[] {
   if (typeof rawDownload !== "string") return [];
   const input = rawDownload.trim();
@@ -515,15 +480,21 @@ export async function fetchCoursesPaged(
         page: { current, row },
       },
     );
-    const list = extractList(payload)
-      .map(normalizeCourse)
-      .filter((c): c is Course => c !== null);
-    const serverPage = extractSearchPage(payload);
+    const serverPage = extractPageMeta(payload.page, { current, row });
+    const pageCurrent = serverPage.current || current;
+    const pageRow = serverPage.row || row;
+    const list = applyClientPageSlice(
+      extractList(payload)
+        .map(normalizeCourse)
+        .filter((c): c is Course => c !== null),
+      pageCurrent,
+      pageRow,
+    );
     return {
       list,
       page: {
-        current: serverPage.current || current,
-        row: serverPage.row || row,
+        current: pageCurrent,
+        row: pageRow,
         total: serverPage.total || list.length,
       },
     };
@@ -560,17 +531,22 @@ export async function fetchCourseVideos(
         page: { current, row },
       },
     );
-    const list = extractVideoList(payload)
-      .map(normalizeVideo)
-      .filter((v): v is CourseVideo => v !== null)
-      .slice(0, 10);
-    const serverPage = extractVideoPage(payload);
+    const serverPage = extractPageMeta(payload.page, { current, row });
+    const pageCurrent = serverPage.current || current;
+    const pageRow = serverPage.row || row;
+    const list = applyClientPageSlice(
+      extractVideoList(payload)
+        .map(normalizeVideo)
+        .filter((v): v is CourseVideo => v !== null),
+      pageCurrent,
+      pageRow,
+    );
     return {
       list,
       page: {
-        current: serverPage.current || current,
-        row: serverPage.row || row,
-        total: serverPage.total || (current - 1) * row + list.length,
+        current: pageCurrent,
+        row: pageRow,
+        total: serverPage.total || list.length,
       },
     };
   } catch {
@@ -596,13 +572,19 @@ export async function fetchCourseFiles(
       goodsId: normalizedGoodsId,
       page: { current, row },
     });
-    const finalList = extractVideoList(payload).flatMap(normalizeFileFromVideo);
-    const serverPage = extractVideoPage(payload);
+    const serverPage = extractPageMeta(payload.page, { current, row });
+    const pageCurrent = serverPage.current || current;
+    const pageRow = serverPage.row || row;
+    const finalList = applyClientPageSlice(
+      extractVideoList(payload).flatMap(normalizeFileFromVideo),
+      pageCurrent,
+      pageRow,
+    );
     return {
       list: finalList,
       page: {
-        current: serverPage.current || current,
-        row: serverPage.row || row,
+        current: pageCurrent,
+        row: pageRow,
         total: serverPage.total || finalList.length,
       },
     };

+ 13 - 33
src/lib/order-api.ts

@@ -1,4 +1,8 @@
 import { apiPost } from "@/lib/api";
+import {
+  applyClientPageSlice,
+  extractPageMetaFromPayload,
+} from "@/lib/page-meta";
 
 export type OrderStatus = 1 | 2 | 3 | 4 | 5;
 
@@ -53,37 +57,6 @@ function normalizeDetails(value: unknown): string {
   return "-";
 }
 
-function pickPage(raw: unknown, fallback: { current: number; row: number }): OrderPage {
-  if (!raw || typeof raw !== "object") {
-    return { current: fallback.current, row: fallback.row, total: 0 };
-  }
-  const o = raw as Record<string, unknown>;
-  const fromData =
-    o.data && typeof o.data === "object" && o.data !== null
-      ? (o.data as Record<string, unknown>)
-      : null;
-  const page =
-    (fromData?.page && typeof fromData.page === "object" ? fromData.page : null) ??
-    (o.page && typeof o.page === "object" ? o.page : null);
-
-  const current = Number(
-    (page as Record<string, unknown> | null)?.current ??
-      (page as Record<string, unknown> | null)?.pageNum ??
-      fallback.current,
-  );
-  const row = Number(
-    (page as Record<string, unknown> | null)?.row ??
-      (page as Record<string, unknown> | null)?.size ??
-      fallback.row,
-  );
-  const total = Number((page as Record<string, unknown> | null)?.total ?? 0);
-  return {
-    current: Number.isFinite(current) && current > 0 ? current : fallback.current,
-    row: Number.isFinite(row) && row > 0 ? row : fallback.row,
-    total: Number.isFinite(total) && total >= 0 ? total : 0,
-  };
-}
-
 export function getOrderStatusLabel(status: number): string {
   const map: Record<number, string> = {
     1: "未支付",
@@ -145,9 +118,16 @@ export async function fetchOrderList(page: {
       details,
     });
   }
+  const serverPage = extractPageMetaFromPayload(raw, { current, row });
+  const pageCurrent = serverPage.current || current;
+  const pageRow = serverPage.row || row;
   return {
-    list: out,
-    page: pickPage(raw, { current, row }),
+    list: applyClientPageSlice(out, pageCurrent, pageRow),
+    page: {
+      current: pageCurrent,
+      row: pageRow,
+      total: serverPage.total || out.length,
+    },
   };
 }
 

+ 64 - 0
src/lib/page-meta.ts

@@ -0,0 +1,64 @@
+/** 后端分页字段(含 rowTotal / rows 等别名) */
+export type PageMetaRaw = {
+  current?: number;
+  pageNum?: number;
+  row?: number;
+  rows?: number;
+  size?: number;
+  total?: number;
+  rowTotal?: number;
+  totalCount?: number;
+  pageTotal?: number;
+};
+
+export type PageMeta = {
+  current: number;
+  row: number;
+  total: number;
+};
+
+export function extractPageMeta(
+  page: PageMetaRaw | undefined | null,
+  fallback: { current: number; row: number },
+): PageMeta {
+  const current = Number(page?.current ?? page?.pageNum);
+  const row = Number(page?.row ?? page?.rows ?? page?.size);
+  const total = Number(page?.total ?? page?.rowTotal ?? page?.totalCount);
+  return {
+    current: Number.isFinite(current) && current > 0 ? current : fallback.current,
+    row: Number.isFinite(row) && row > 0 ? row : fallback.row,
+    total: Number.isFinite(total) && total >= 0 ? total : 0,
+  };
+}
+
+/** 从接口根对象或 data.page 中解析分页元数据 */
+export function resolvePageFromPayload(raw: unknown): PageMetaRaw | undefined {
+  if (!raw || typeof raw !== "object") return undefined;
+  const o = raw as Record<string, unknown>;
+  const fromData =
+    o.data && typeof o.data === "object" && o.data !== null
+      ? (o.data as Record<string, unknown>)
+      : null;
+  const page =
+    (fromData?.page && typeof fromData.page === "object" ? fromData.page : null) ??
+    (o.page && typeof o.page === "object" ? o.page : null);
+  return page as PageMetaRaw | undefined;
+}
+
+export function extractPageMetaFromPayload(
+  raw: unknown,
+  fallback: { current: number; row: number },
+): PageMeta {
+  return extractPageMeta(resolvePageFromPayload(raw), fallback);
+}
+
+/** 后端偶发一次返回超过 page.row 条时,按当前页在客户端截断 */
+export function applyClientPageSlice<T>(
+  list: T[],
+  pageCurrent: number,
+  pageRow: number,
+): T[] {
+  if (list.length <= pageRow) return list;
+  const start = (pageCurrent - 1) * pageRow;
+  return list.slice(start, start + pageRow);
+}

+ 21 - 2
src/lib/withdrawal-api.ts

@@ -1,4 +1,8 @@
 import { api } from "@/lib/api";
+import {
+  applyClientPageSlice,
+  extractPageMetaFromPayload,
+} from "@/lib/page-meta";
 import { postRemittance } from "@/lib/remittance-client";
 
 export type WithdrawalRecord = {
@@ -11,6 +15,11 @@ export type WithdrawalRecord = {
   details: string;
 };
 
+export type WithdrawalListResult = {
+  list: WithdrawalRecord[];
+  page: { current: number; row: number; total: number };
+};
+
 export function getWithdrawalStatusLabel(status: string): string {
   const normalized = String(status).trim();
   const map: Record<string, string> = {
@@ -33,7 +42,7 @@ export function getWithdrawalStatusLabel(status: string): string {
 export async function fetchWithdrawalList(page: { current: number; row: number } = {
   current: 1,
   row: 10,
-}): Promise<WithdrawalRecord[]> {
+}): Promise<WithdrawalListResult> {
   const current = Number.isFinite(page.current) ? Math.max(1, page.current) : 1;
   const row = Number.isFinite(page.row) ? Math.max(1, page.row) : 10;
   const { data: raw } = await api.post<unknown>("/finance/withdraw/searcher/list", {
@@ -42,6 +51,9 @@ export async function fetchWithdrawalList(page: { current: number; row: number }
       row,
     },
   });
+  const serverPage = extractPageMetaFromPayload(raw, { current, row });
+  const pageCurrent = serverPage.current || current;
+  const pageRow = serverPage.row || row;
   const list = pickList(raw);
   const out: WithdrawalRecord[] = [];
   for (const item of list) {
@@ -68,7 +80,14 @@ export async function fetchWithdrawalList(page: { current: number; row: number }
       details,
     });
   }
-  return out;
+  return {
+    list: applyClientPageSlice(out, pageCurrent, pageRow),
+    page: {
+      current: pageCurrent,
+      row: pageRow,
+      total: serverPage.total || out.length,
+    },
+  };
 }
 
 export type WithdrawAccount = {