From 74d53c619d3209b158d6263f0fdf9ff113a89609 Mon Sep 17 00:00:00 2001 From: Eamon-meng <17516219072@163.com> Date: Mon, 20 Apr 2026 15:36:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=AD=E4=B9=B0=E5=A5=97=E9=A4=90=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0count=5Fmin=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/_components/addr.tsx | 6 ++-- src/app/admin/channels/page.tsx | 4 +-- src/components/composites/purchase/index.tsx | 1 + .../composites/purchase/long/center.tsx | 31 ++++++++++++++++--- .../composites/purchase/long/form.tsx | 11 +++++-- .../purchase/shared/number-stepper-field.tsx | 3 ++ .../composites/purchase/shared/sku.ts | 15 +++++++++ .../composites/purchase/short/center.tsx | 30 +++++++++++++++--- .../composites/purchase/short/form.tsx | 12 ++++--- src/lib/models/product-sku.ts | 1 + 10 files changed, 92 insertions(+), 22 deletions(-) diff --git a/src/app/admin/_components/addr.tsx b/src/app/admin/_components/addr.tsx index fba0804..b504e39 100644 --- a/src/app/admin/_components/addr.tsx +++ b/src/app/admin/_components/addr.tsx @@ -10,13 +10,13 @@ export default function Addr({channel}: { const expired = isBefore(channel.expired_at, new Date()) return ( -
+ <> {ip}:{port} {expired && ( - + 已过期 )} -
+ ) } diff --git a/src/app/admin/channels/page.tsx b/src/app/admin/channels/page.tsx index 60b16e8..c56e185 100644 --- a/src/app/admin/channels/page.tsx +++ b/src/app/admin/channels/page.tsx @@ -192,7 +192,7 @@ export default function ChannelsPage(props: ChannelsPageProps) { 白名单
{channel.whitelists.split(',').map((ip, index) => ( - + {ip.trim()} ))} @@ -201,7 +201,7 @@ export default function ChannelsPage(props: ChannelsPageProps) { ) : hasAuth ? (
账号密码 - + {channel.username}:{channel.password}
diff --git a/src/components/composites/purchase/index.tsx b/src/components/composites/purchase/index.tsx index 47931fb..4add2cb 100644 --- a/src/components/composites/purchase/index.tsx +++ b/src/components/composites/purchase/index.tsx @@ -30,6 +30,7 @@ export default function Purchase() { const res = profile ? await listProduct({}) : await listProductHome({}) + console.log(res, 'res') if (res.success) { setProductList(res.data) diff --git a/src/components/composites/purchase/long/center.tsx b/src/components/composites/purchase/long/center.tsx index 1073b7d..5c76b27 100644 --- a/src/components/composites/purchase/long/center.tsx +++ b/src/components/composites/purchase/long/center.tsx @@ -3,18 +3,18 @@ import {FormField} from '@/components/ui/form' import {RadioGroup} from '@/components/ui/radio-group' import FormOption from '@/components/composites/purchase/option' import {Schema} from '@/components/composites/purchase/long/form' -import {useEffect} from 'react' +import {useEffect, useMemo} from 'react' import {useFormContext, useWatch} from 'react-hook-form' import {Card} from '@/components/ui/card' import {BillingMethodField} from '../shared/billing-method-field' import {FeatureList} from '../shared/feature-list' import {NumberStepperField} from '../shared/number-stepper-field' -import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' +import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuCountMin, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' export default function Center({skuData}: { skuData: PurchaseSkuData }) { - const {setValue} = useFormContext() + const {setValue, getValues} = useFormContext() const type = useWatch({name: 'type'}) as Schema['type'] const live = useWatch({name: 'live'}) as Schema['live'] const expire = useWatch({name: 'expire'}) as Schema['expire'] @@ -26,6 +26,27 @@ export default function Center({skuData}: { ? getAvailablePurchaseExpires(skuData, {mode: type, live}) : [] + const currentCountMin = useMemo(() => { + if (!type || !live) return 0 + const expireValue = type === '1' ? expire : '0' + return getPurchaseSkuCountMin(skuData, {mode: type, live, expire: expireValue}) + }, [type, live, expire, skuData]) + + useEffect(() => { + if (type === '1') { + const current = getValues('daily_limit') + if (current < currentCountMin) { + setValue('daily_limit', currentCountMin) + } + } + else { + const current = getValues('quota') + if (current < currentCountMin) { + setValue('quota', currentCountMin) + } + } + }, [currentCountMin, type, setValue, getValues]) + useEffect(() => { const nextType = modeList.includes(type) ? type : modeList[0] @@ -146,14 +167,14 @@ export default function Center({skuData}: { ) : ( )} diff --git a/src/components/composites/purchase/long/form.tsx b/src/components/composites/purchase/long/form.tsx index fa5b308..e37daff 100644 --- a/src/components/composites/purchase/long/form.tsx +++ b/src/components/composites/purchase/long/form.tsx @@ -5,7 +5,7 @@ import {Form} from '@/components/ui/form' import * as z from 'zod' import {zodResolver} from '@hookform/resolvers/zod' import {ProductItem} from '@/actions/product' -import {getAvailablePurchaseExpires, getAvailablePurchaseLives, parsePurchaseSkuList} from '../shared/sku' +import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuCountMin, parsePurchaseSkuList} from '../shared/sku' import {PurchaseSidePanel} from '../shared/side-panel' const schema = z.object({ @@ -25,6 +25,11 @@ export default function LongForm({skuList}: {skuList: ProductItem['skus']}) { const defaultExpire = defaultMode === '1' ? getAvailablePurchaseExpires(skuData, {mode: defaultMode, live: defaultLive})[0] || '0' : '0' + const defaultCountMin = getPurchaseSkuCountMin(skuData, { + mode: defaultMode, + live: defaultLive, + expire: defaultExpire, + }) const form = useForm({ resolver: zodResolver(schema), @@ -32,8 +37,8 @@ export default function LongForm({skuList}: {skuList: ProductItem['skus']}) { type: defaultMode, live: defaultLive, expire: defaultExpire, - quota: 500, - daily_limit: 100, + quota: defaultMode === '2' ? Math.max(defaultCountMin, 500) : 500, + daily_limit: defaultMode === '1' ? Math.max(defaultCountMin, 100) : 100, pay_type: 'balance', // 余额支付 }, }) diff --git a/src/components/composites/purchase/shared/number-stepper-field.tsx b/src/components/composites/purchase/shared/number-stepper-field.tsx index e3f390b..e18b101 100644 --- a/src/components/composites/purchase/shared/number-stepper-field.tsx +++ b/src/components/composites/purchase/shared/number-stepper-field.tsx @@ -49,6 +49,9 @@ export function NumberStepperField(props: NumberStepperFieldProps) { className="w-40 h-10 border border-gray-200 rounded-sm text-center" min={props.min} step={props.step} + onInvalid={(e) => { + e.preventDefault() + }} onBlur={(event) => { field.onBlur() const nextValue = Number(event.target.value) diff --git a/src/components/composites/purchase/shared/sku.ts b/src/components/composites/purchase/shared/sku.ts index 0240b6e..1bca87a 100644 --- a/src/components/composites/purchase/shared/sku.ts +++ b/src/components/composites/purchase/shared/sku.ts @@ -7,11 +7,13 @@ export type PurchaseSkuItem = { live: string expire: string price: string + count_min: number } export type PurchaseSkuData = { items: PurchaseSkuItem[] priceMap: Map + countMinMap: Map modeList: PurchaseMode[] liveList: string[] expireList: string[] @@ -24,6 +26,7 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s const items: PurchaseSkuItem[] = [] const priceMap = new Map() + const countMinMap = new Map() const modeSet = new Set() const liveSet = new Set() const expireSet = new Set() @@ -45,6 +48,8 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s live: liveValue, expire: expireValue, }) + const countMin = typeof sku.count_min === 'number' ? sku.count_min : Number(sku.count_min) || 0 + countMinMap.set(code, countMin) items.push({ code, @@ -52,6 +57,7 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s live: liveValue, expire: expireValue, price: sku.price, + count_min: countMin, }) priceMap.set(code, sku.price) modeSet.add(mode) @@ -75,6 +81,7 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s return { items, priceMap, + countMinMap, modeList: (['2', '1'] as const).filter(mode => modeSet.has(mode)), liveList: sortNumericValues(liveSet), expireList: sortNumericValues(expireSet), @@ -163,3 +170,11 @@ export function formatPurchaseLiveLabel(live: string, kind: PurchaseKind) { return `${minutes} 分钟` } + +export function getPurchaseSkuCountMin( + skuData: PurchaseSkuData, + props: {mode: PurchaseMode, live: string, expire: string}, +): number { + const key = getPurchaseSkuKey(props) + return skuData.countMinMap.get(key) ?? 0 +} diff --git a/src/components/composites/purchase/short/center.tsx b/src/components/composites/purchase/short/center.tsx index 1b5533c..90f2c68 100644 --- a/src/components/composites/purchase/short/center.tsx +++ b/src/components/composites/purchase/short/center.tsx @@ -2,21 +2,22 @@ import {FormField} from '@/components/ui/form' import {RadioGroup} from '@/components/ui/radio-group' import FormOption from '@/components/composites/purchase/option' -import {useEffect} from 'react' +import {useEffect, useMemo} from 'react' import {useFormContext, useWatch} from 'react-hook-form' import {Schema} from '@/components/composites/purchase/short/form' import {Card} from '@/components/ui/card' import {BillingMethodField} from '../shared/billing-method-field' import {FeatureList} from '../shared/feature-list' import {NumberStepperField} from '../shared/number-stepper-field' -import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' +import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuCountMin, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' export default function Center({ skuData, }: { skuData: PurchaseSkuData }) { - const {setValue} = useFormContext() + // const {setValue} = useFormContext() + const {setValue, getValues} = useFormContext() const type = useWatch({name: 'type'}) as Schema['type'] const live = useWatch({name: 'live'}) as Schema['live'] const expire = useWatch({name: 'expire'}) as Schema['expire'] @@ -28,6 +29,25 @@ export default function Center({ ? getAvailablePurchaseExpires(skuData, {mode: type, live}) : [] + const currentCountMin = useMemo(() => { + if (!type || !live) return 0 + const expireValue = type === '1' ? expire : '0' + return getPurchaseSkuCountMin(skuData, {mode: type, live, expire: expireValue}) + }, [type, live, expire, skuData]) + useEffect(() => { + if (type === '1') { + const current = getValues('daily_limit') + if (current < currentCountMin) { + setValue('daily_limit', currentCountMin) + } + } + else { + const current = getValues('quota') + if (current < currentCountMin) { + setValue('quota', currentCountMin) + } + } + }, [currentCountMin, type, setValue, getValues]) useEffect(() => { const nextType = modeList.includes(type) ? type : modeList[0] @@ -151,14 +171,14 @@ export default function Center({ ) : ( )} diff --git a/src/components/composites/purchase/short/form.tsx b/src/components/composites/purchase/short/form.tsx index 9bb97e7..69ed764 100644 --- a/src/components/composites/purchase/short/form.tsx +++ b/src/components/composites/purchase/short/form.tsx @@ -5,7 +5,7 @@ import {Form} from '@/components/ui/form' import * as z from 'zod' import {zodResolver} from '@hookform/resolvers/zod' import {ProductItem} from '@/actions/product' -import {getAvailablePurchaseExpires, getAvailablePurchaseLives, parsePurchaseSkuList} from '../shared/sku' +import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuCountMin, parsePurchaseSkuList} from '../shared/sku' import {PurchaseSidePanel} from '../shared/side-panel' const schema = z.object({ @@ -25,15 +25,19 @@ export default function ShortForm({skuList}: {skuList: ProductItem['skus']}) { const defaultExpire = defaultMode === '1' ? getAvailablePurchaseExpires(skuData, {mode: defaultMode, live: defaultLive})[0] || '0' : '0' - + const defaultCountMin = getPurchaseSkuCountMin(skuData, { + mode: defaultMode, + live: defaultLive, + expire: defaultExpire, + }) const form = useForm({ resolver: zodResolver(schema), defaultValues: { type: defaultMode, live: defaultLive, expire: defaultExpire, - quota: 10_000, // >= 10000, - daily_limit: 2_000, // >= 2000 + quota: defaultMode === '2' ? defaultCountMin || 10000 : 10000, + daily_limit: defaultMode === '1' ? defaultCountMin || 2000 : 2000, pay_type: 'balance', // 余额支付 }, }) diff --git a/src/lib/models/product-sku.ts b/src/lib/models/product-sku.ts index 43c63b7..6c81846 100644 --- a/src/lib/models/product-sku.ts +++ b/src/lib/models/product-sku.ts @@ -1,6 +1,7 @@ export type ProductSku = { id: number code: string + count_min: number name: string price: string price_min: string