mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
fix: submit recharge card form
This commit is contained in:
@@ -22,6 +22,7 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [type, setType] = useState<"BALANCE" | "PLAN">("BALANCE");
|
const [type, setType] = useState<"BALANCE" | "PLAN">("BALANCE");
|
||||||
const [planId, setPlanId] = useState(plans[0]?.id ?? "");
|
const [planId, setPlanId] = useState(plans[0]?.id ?? "");
|
||||||
|
const [hasExpiry, setHasExpiry] = useState(false);
|
||||||
const [pending, startTransition] = useTransition();
|
const [pending, startTransition] = useTransition();
|
||||||
const selectedPlan = plans.find((plan) => plan.id === planId) ?? null;
|
const selectedPlan = plans.find((plan) => plan.id === planId) ?? null;
|
||||||
const planSoldOut = type === "PLAN" && selectedPlan?.remainingCount === 0;
|
const planSoldOut = type === "PLAN" && selectedPlan?.remainingCount === 0;
|
||||||
@@ -40,6 +41,7 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) {
|
|||||||
await createAdminRechargeCards(formData);
|
await createAdminRechargeCards(formData);
|
||||||
toast.success("充值卡已生成");
|
toast.success("充值卡已生成");
|
||||||
form.reset();
|
form.reset();
|
||||||
|
setHasExpiry(false);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(getErrorMessage(error, "生成充值卡失败"));
|
toast.error(getErrorMessage(error, "生成充值卡失败"));
|
||||||
@@ -93,12 +95,19 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) {
|
|||||||
<Label>绑定套餐</Label>
|
<Label>绑定套餐</Label>
|
||||||
<Select value={planId} onValueChange={(value) => setPlanId(value ?? "")}>
|
<Select value={planId} onValueChange={(value) => setPlanId(value ?? "")}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="选择套餐" />
|
<SelectValue placeholder="选择套餐">
|
||||||
|
{(value) => {
|
||||||
|
const plan = plans.find((item) => item.id === value);
|
||||||
|
return plan
|
||||||
|
? `${plan.name} · ${plan.type === "PROXY" ? "代理套餐" : "流媒体套餐"}`
|
||||||
|
: "选择套餐";
|
||||||
|
}}
|
||||||
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{plans.map((plan) => (
|
{plans.map((plan) => (
|
||||||
<SelectItem key={plan.id} value={plan.id}>
|
<SelectItem key={plan.id} value={plan.id}>
|
||||||
{plan.name} · {plan.type === "PROXY" ? "代理" : "流媒体"} · 余 {plan.remainingCount ?? "不限"}
|
{plan.name} · {plan.type === "PROXY" ? "代理套餐" : "流媒体套餐"} · 余 {plan.remainingCount ?? "不限"}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -112,11 +121,35 @@ export function RechargeCardForm({ plans }: { plans: PlanOption[] }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="recharge-card-expires">过期时间</Label>
|
<Label htmlFor="recharge-card-expiry-mode">有效期</Label>
|
||||||
<Input id="recharge-card-expires" name="expiresAt" type="datetime-local" />
|
<BooleanToggle
|
||||||
|
id="recharge-card-expiry-mode"
|
||||||
|
value={hasExpiry}
|
||||||
|
onChange={setHasExpiry}
|
||||||
|
trueLabel="设置到期"
|
||||||
|
falseLabel="永不过期"
|
||||||
|
ariaLabel="充值卡有效期"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className="w-full" disabled={pending || (type === "PLAN" && (!planId || planSoldOut))}>
|
{hasExpiry && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="recharge-card-expires">到期时间</Label>
|
||||||
|
<Input id="recharge-card-expires" name="expiresAt" type="datetime-local" required />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hasExpiry && (
|
||||||
|
<input type="hidden" name="expiresAt" value="" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "PLAN" && plans.length === 0 && (
|
||||||
|
<div className="rounded-lg border border-destructive/15 bg-destructive/10 px-3 py-2 text-xs font-medium text-destructive">
|
||||||
|
暂无可用套餐,请先上架套餐。
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={pending || (type === "PLAN" && (!planId || planSoldOut))}>
|
||||||
{pending ? "生成中..." : planSoldOut ? "套餐已售罄" : "生成充值卡"}
|
{pending ? "生成中..." : planSoldOut ? "套餐已售罄" : "生成充值卡"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import { parsePage } from "@/lib/utils";
|
||||||
import { getPlanAvailability } from "@/services/plan-availability";
|
import { getPlanAvailability } from "@/services/plan-availability";
|
||||||
|
|
||||||
function getRechargeCardPlanRemaining(
|
function getRechargeCardPlanRemaining(
|
||||||
@@ -15,8 +16,11 @@ function getRechargeCardPlanRemaining(
|
|||||||
return limits.length > 0 ? Math.min(...limits) : null;
|
return limits.length > 0 ? Math.min(...limits) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCommerceData() {
|
export async function getCommerceData(
|
||||||
const [coupons, promotions, rechargeCards, planRows] = await Promise.all([
|
searchParams: Record<string, string | string[] | undefined> = {},
|
||||||
|
) {
|
||||||
|
const { page, skip, pageSize } = parsePage(searchParams, 20);
|
||||||
|
const [coupons, promotions, rechargeCards, rechargeCardTotal, planRows] = await Promise.all([
|
||||||
prisma.coupon.findMany({
|
prisma.coupon.findMany({
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
include: { _count: { select: { orders: true, grants: true } } },
|
include: { _count: { select: { orders: true, grants: true } } },
|
||||||
@@ -32,8 +36,10 @@ export async function getCommerceData() {
|
|||||||
plan: { select: { name: true, type: true } },
|
plan: { select: { name: true, type: true } },
|
||||||
redeemedBy: { select: { email: true } },
|
redeemedBy: { select: { email: true } },
|
||||||
},
|
},
|
||||||
take: 50,
|
skip,
|
||||||
|
take: pageSize,
|
||||||
}),
|
}),
|
||||||
|
prisma.rechargeCard.count(),
|
||||||
prisma.subscriptionPlan.findMany({
|
prisma.subscriptionPlan.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true },
|
||||||
orderBy: [{ type: "asc" }, { sortOrder: "asc" }, { createdAt: "desc" }],
|
orderBy: [{ type: "asc" }, { sortOrder: "asc" }, { createdAt: "desc" }],
|
||||||
@@ -60,5 +66,11 @@ export async function getCommerceData() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return { coupons, promotions, rechargeCards, plans };
|
return {
|
||||||
|
coupons,
|
||||||
|
promotions,
|
||||||
|
rechargeCards,
|
||||||
|
rechargeCardPagination: { total: rechargeCardTotal, page, pageSize },
|
||||||
|
plans,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { createCoupon, createPromotionRule } from "@/actions/admin/commerce";
|
|||||||
import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge";
|
import { ActiveStatusBadge, StatusBadge } from "@/components/admin/status-badge";
|
||||||
import { CopyButton } from "@/components/shared/copy-button";
|
import { CopyButton } from "@/components/shared/copy-button";
|
||||||
import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell";
|
import { PageHeader, PageShell, SectionHeader } from "@/components/shared/page-shell";
|
||||||
|
import { Pagination } from "@/components/shared/pagination";
|
||||||
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
|
import { PendingSubmitButton } from "@/components/shared/pending-submit-button";
|
||||||
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
import { BooleanToggle } from "@/components/ui/boolean-toggle";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
@@ -28,8 +29,25 @@ export const metadata: Metadata = {
|
|||||||
description: "管理优惠券与满减规则。",
|
description: "管理优惠券与满减规则。",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function AdminCommercePage() {
|
const rechargeCardStatusLabels: Record<string, string> = {
|
||||||
const { coupons, promotions, rechargeCards, plans } = await getCommerceData();
|
UNUSED: "未使用",
|
||||||
|
REDEEMED: "已兑换",
|
||||||
|
EXPIRED: "已过期",
|
||||||
|
DISABLED: "已停用",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeCommerceTab(value: string | string[] | undefined) {
|
||||||
|
return value === "manage" || value === "cards" ? value : "create";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function AdminCommercePage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
|
}) {
|
||||||
|
const params = await searchParams;
|
||||||
|
const activeTab = normalizeCommerceTab(params.tab);
|
||||||
|
const { coupons, promotions, rechargeCards, rechargeCardPagination, plans } = await getCommerceData(params);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell>
|
<PageShell>
|
||||||
@@ -38,7 +56,7 @@ export default async function AdminCommercePage() {
|
|||||||
title="优惠与奖励"
|
title="优惠与奖励"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs defaultValue="create" className="space-y-6">
|
<Tabs defaultValue={activeTab} className="space-y-6">
|
||||||
<TabsList variant="line" className="surface-card p-1">
|
<TabsList variant="line" className="surface-card p-1">
|
||||||
<TabsTrigger value="create">新建规则</TabsTrigger>
|
<TabsTrigger value="create">新建规则</TabsTrigger>
|
||||||
<TabsTrigger value="manage">规则列表</TabsTrigger>
|
<TabsTrigger value="manage">规则列表</TabsTrigger>
|
||||||
@@ -185,7 +203,7 @@ export default async function AdminCommercePage() {
|
|||||||
<RechargeCardForm plans={plans} />
|
<RechargeCardForm plans={plans} />
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SectionHeader title="最近充值卡" />
|
<SectionHeader title="充值卡列表" />
|
||||||
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
|
<div className="surface-card divide-y divide-border/60 overflow-hidden rounded-xl">
|
||||||
{rechargeCards.map((card) => (
|
{rechargeCards.map((card) => (
|
||||||
<article key={card.id} className="grid gap-4 px-4 py-4 lg:grid-cols-[minmax(0,1fr)_minmax(14rem,0.6fr)_auto] lg:items-center">
|
<article key={card.id} className="grid gap-4 px-4 py-4 lg:grid-cols-[minmax(0,1fr)_minmax(14rem,0.6fr)_auto] lg:items-center">
|
||||||
@@ -197,13 +215,7 @@ export default async function AdminCommercePage() {
|
|||||||
<div className="flex min-h-6 flex-wrap items-center gap-2">
|
<div className="flex min-h-6 flex-wrap items-center gap-2">
|
||||||
<h3 className="min-w-0 truncate font-mono font-semibold">{card.code}</h3>
|
<h3 className="min-w-0 truncate font-mono font-semibold">{card.code}</h3>
|
||||||
<StatusBadge tone={card.status === "UNUSED" ? "success" : "neutral"}>
|
<StatusBadge tone={card.status === "UNUSED" ? "success" : "neutral"}>
|
||||||
{card.status === "UNUSED"
|
{rechargeCardStatusLabels[card.status] ?? "未知状态"}
|
||||||
? "未使用"
|
|
||||||
: card.status === "REDEEMED"
|
|
||||||
? "已兑换"
|
|
||||||
: card.status === "EXPIRED"
|
|
||||||
? "已过期"
|
|
||||||
: "已停用"}
|
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
@@ -216,7 +228,7 @@ export default async function AdminCommercePage() {
|
|||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{card.batchName && <StatusBadge>{card.batchName}</StatusBadge>}
|
{card.batchName && <StatusBadge>{card.batchName}</StatusBadge>}
|
||||||
{card.redeemedBy && <StatusBadge tone="info">{card.redeemedBy.email}</StatusBadge>}
|
{card.redeemedBy && <StatusBadge tone="info">{card.redeemedBy.email}</StatusBadge>}
|
||||||
{card.expiresAt && <StatusBadge>到期 {formatDate(card.expiresAt)}</StatusBadge>}
|
<StatusBadge>{card.expiresAt ? `到期 ${formatDate(card.expiresAt)}` : "永不过期"}</StatusBadge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start lg:justify-end">
|
<div className="flex justify-start lg:justify-end">
|
||||||
<CopyButton text={card.code} />
|
<CopyButton text={card.code} />
|
||||||
@@ -227,6 +239,12 @@ export default async function AdminCommercePage() {
|
|||||||
<p className="px-4 py-8 text-center text-sm text-muted-foreground">暂无充值卡</p>
|
<p className="px-4 py-8 text-center text-sm text-muted-foreground">暂无充值卡</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Pagination
|
||||||
|
total={rechargeCardPagination.total}
|
||||||
|
pageSize={rechargeCardPagination.pageSize}
|
||||||
|
page={rechargeCardPagination.page}
|
||||||
|
fixedParams={{ tab: "cards" }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function WalletActions() {
|
|||||||
<Label htmlFor="wallet-recharge-amount">充值金额</Label>
|
<Label htmlFor="wallet-recharge-amount">充值金额</Label>
|
||||||
<Input id="wallet-recharge-amount" name="amount" type="number" min="1" step="0.01" placeholder="100.00" required />
|
<Input id="wallet-recharge-amount" name="amount" type="number" min="1" step="0.01" placeholder="100.00" required />
|
||||||
</div>
|
</div>
|
||||||
<Button className="w-full" disabled={rechargePending}>
|
<Button type="submit" className="w-full" disabled={rechargePending}>
|
||||||
{rechargePending ? "创建中..." : "去支付"}
|
{rechargePending ? "创建中..." : "去支付"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -82,7 +82,7 @@ export function WalletActions() {
|
|||||||
<Label htmlFor="wallet-redeem-code">卡密</Label>
|
<Label htmlFor="wallet-redeem-code">卡密</Label>
|
||||||
<Input id="wallet-redeem-code" name="code" placeholder="JB-XXXXXX-XXXXXX-XXXXXX" required />
|
<Input id="wallet-redeem-code" name="code" placeholder="JB-XXXXXX-XXXXXX-XXXXXX" required />
|
||||||
</div>
|
</div>
|
||||||
<Button className="w-full" variant="outline" disabled={redeemPending}>
|
<Button type="submit" className="w-full" variant="outline" disabled={redeemPending}>
|
||||||
{redeemPending ? "兑换中..." : "立即兑换"}
|
{redeemPending ? "兑换中..." : "立即兑换"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -3,32 +3,72 @@
|
|||||||
import { usePathname, useSearchParams, useRouter } from "next/navigation";
|
import { usePathname, useSearchParams, useRouter } from "next/navigation";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
||||||
interface PaginationProps {
|
interface PaginationProps {
|
||||||
total: number;
|
total: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
page: number;
|
page: number;
|
||||||
|
pageSizeOptions?: number[];
|
||||||
|
fixedParams?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pagination({ total, pageSize, page }: PaginationProps) {
|
const defaultPageSizeOptions = [10, 20, 30, 50, 100];
|
||||||
|
|
||||||
|
export function Pagination({
|
||||||
|
total,
|
||||||
|
pageSize,
|
||||||
|
page,
|
||||||
|
pageSizeOptions = defaultPageSizeOptions,
|
||||||
|
fixedParams,
|
||||||
|
}: PaginationProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
|
|
||||||
if (totalPages <= 1) return null;
|
if (total === 0) return null;
|
||||||
|
|
||||||
function go(p: number) {
|
function go(p: number) {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
for (const [key, value] of Object.entries(fixedParams ?? {})) {
|
||||||
|
params.set(key, value);
|
||||||
|
}
|
||||||
params.set("page", String(p));
|
params.set("page", String(p));
|
||||||
router.push(`${pathname}?${params.toString()}`);
|
router.push(`${pathname}?${params.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changePageSize(nextPageSize: string | null) {
|
||||||
|
if (!nextPageSize) return;
|
||||||
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
for (const [key, value] of Object.entries(fixedParams ?? {})) {
|
||||||
|
params.set(key, value);
|
||||||
|
}
|
||||||
|
params.set("page", "1");
|
||||||
|
params.set("pageSize", nextPageSize);
|
||||||
|
router.push(`${pathname}?${params.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="surface-card flex flex-col gap-3 rounded-xl px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
<div className="surface-card flex flex-col gap-3 rounded-xl px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
共 {total} 条,第 <span className="font-semibold text-foreground">{page}</span>/{totalPages} 页
|
共 {total} 条,第 <span className="font-semibold text-foreground">{page}</span>/{totalPages || 1} 页
|
||||||
</p>
|
</p>
|
||||||
|
<Select value={String(pageSize)} onValueChange={changePageSize}>
|
||||||
|
<SelectTrigger size="sm" className="h-8 w-28 rounded-lg text-xs">
|
||||||
|
<SelectValue>{(value) => `每页 ${value} 条`}</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent align="start">
|
||||||
|
{pageSizeOptions.map((option) => (
|
||||||
|
<SelectItem key={option} value={String(option)}>
|
||||||
|
每页 {option} 条
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{totalPages > 1 && (
|
||||||
<div className="flex flex-wrap items-center gap-1.5">
|
<div className="flex flex-wrap items-center gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -72,6 +112,7 @@ export function Pagination({ total, pageSize, page }: PaginationProps) {
|
|||||||
<ChevronRight className="h-4 w-4" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,15 @@ export function formatDateShort(date: Date | string): string {
|
|||||||
return format(new Date(date), "yyyy-MM-dd", { locale: zhCN });
|
return format(new Date(date), "yyyy-MM-dd", { locale: zhCN });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePage(searchParams: Record<string, string | string[] | undefined>, pageSize = 20) {
|
const PAGE_SIZE_OPTIONS = [10, 20, 30, 50, 100] as const;
|
||||||
|
|
||||||
|
export function parsePage(searchParams: Record<string, string | string[] | undefined>, defaultPageSize = 20) {
|
||||||
const raw = searchParams.page;
|
const raw = searchParams.page;
|
||||||
|
const rawPageSize = searchParams.pageSize;
|
||||||
|
const requestedPageSize = parseInt(typeof rawPageSize === "string" ? rawPageSize : "", 10);
|
||||||
|
const pageSize = PAGE_SIZE_OPTIONS.includes(requestedPageSize as (typeof PAGE_SIZE_OPTIONS)[number])
|
||||||
|
? requestedPageSize
|
||||||
|
: defaultPageSize;
|
||||||
const page = Math.max(1, parseInt(typeof raw === "string" ? raw : "1", 10) || 1);
|
const page = Math.max(1, parseInt(typeof raw === "string" ? raw : "1", 10) || 1);
|
||||||
const skip = (page - 1) * pageSize;
|
const skip = (page - 1) * pageSize;
|
||||||
return { page, skip, pageSize };
|
return { page, skip, pageSize };
|
||||||
|
|||||||
Reference in New Issue
Block a user