From c297c2330ebf185f9f62bea38e96435d84dc7c87 Mon Sep 17 00:00:00 2001 From: Eamon-meng <17516219072@163.com> Date: Thu, 18 Jun 2026 15:11:28 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=B4=AD=E4=B9=B0=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=BB=B7=E6=A0=BC=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/balance/page.tsx | 2 +- src/components/composites/purchase/index.tsx | 1 + .../composites/purchase/long/center.tsx | 11 ++++- .../composites/purchase/long/form.tsx | 4 +- src/components/composites/purchase/option.tsx | 44 ++++++++++++++++++- .../purchase/shared/billing-method-field.tsx | 19 ++++---- .../composites/purchase/shared/side-panel.tsx | 23 +++++++--- .../composites/purchase/shared/sku.ts | 15 +++++++ .../composites/purchase/short/center.tsx | 11 ++++- .../composites/purchase/short/form.tsx | 5 ++- src/lib/models/product-sku.ts | 1 + 11 files changed, 111 insertions(+), 25 deletions(-) diff --git a/src/app/admin/balance/page.tsx b/src/app/admin/balance/page.tsx index 27c168c..dcc6472 100644 --- a/src/app/admin/balance/page.tsx +++ b/src/app/admin/balance/page.tsx @@ -197,7 +197,7 @@ export default function BalancePage(props: BalancePageProps) {
{isPositive ? '+' : ''} 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 ffcbf6e..68ce1fd 100644 --- a/src/components/composites/purchase/long/center.tsx +++ b/src/components/composites/purchase/long/center.tsx @@ -9,7 +9,7 @@ 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, getPurchaseSkuCountMin, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' +import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuCountMin, getPurchaseSkuDiscount, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' export default function Center({skuData}: { skuData: PurchaseSkuData @@ -18,7 +18,7 @@ export default function Center({skuData}: { const type = useWatch({name: 'type'}) as Schema['type'] const live = useWatch({name: 'live'}) as Schema['live'] const expire = useWatch({name: 'expire'}) as Schema['expire'] - const {modeList, priceMap} = skuData + const {modeList, priceMap, discountMap} = skuData const liveList = type === '1' ? getAvailablePurchaseLives(skuData, {mode: type, expire}) : getAvailablePurchaseLives(skuData, {mode: type}) @@ -144,6 +144,11 @@ export default function Center({skuData}: { live, expire: priceExpire, }) + const discount = getPurchaseSkuDiscount(discountMap, { + mode: type, + live, + expire: priceExpire, + }) return ( ) diff --git a/src/components/composites/purchase/long/form.tsx b/src/components/composites/purchase/long/form.tsx index 6004dc0..cf1111b 100644 --- a/src/components/composites/purchase/long/form.tsx +++ b/src/components/composites/purchase/long/form.tsx @@ -20,7 +20,7 @@ export type Schema = z.infer export default function LongForm({skuList}: {skuList: ProductItem['skus']}) { const skuData = parsePurchaseSkuList('long', skuList) - const defaultMode = skuData.modeList.includes('1') ? '1' : '2' + const defaultMode = skuData.modeList.includes('2') ? '2' : '1' const defaultLive = getAvailablePurchaseLives(skuData, {mode: defaultMode})[0] || '' const defaultExpire = defaultMode === '1' ? getAvailablePurchaseExpires(skuData, {mode: defaultMode, live: defaultLive})[0] || '0' @@ -46,7 +46,7 @@ export default function LongForm({skuList}: {skuList: ProductItem['skus']}) { return (
- + ) } diff --git a/src/components/composites/purchase/option.tsx b/src/components/composites/purchase/option.tsx index 6e14db7..5259e1a 100644 --- a/src/components/composites/purchase/option.tsx +++ b/src/components/composites/purchase/option.tsx @@ -9,12 +9,37 @@ export type FormOptionProps = { value: string label?: string description?: string + price?: string + discount?: number compare: string className?: string children?: ReactNode } export default function FormOption(props: FormOptionProps) { + // 安全地解析价格 + const priceNum = props.price ? parseFloat(props.price) : NaN + const isValidPrice = !isNaN(priceNum) && priceNum > 0 + + const discount = typeof props.discount === 'number' ? props.discount : undefined + const hasDiscount = isValidPrice && discount !== undefined && discount < 100 + const discountedPrice = hasDiscount ? priceNum * discount / 100 : null + + const formatPrice = (price: number | string): string => { + const num = typeof price === 'string' ? parseFloat(price) : price + // 如果是 NaN 或无效值,返回空字符串 + if (isNaN(num) || !isFinite(num)) return '' + + const str = num.toString() + if (!str.includes('.')) return str + + const decimal = str.split('.')[1] + if (/^0+$/.test(decimal)) return Math.floor(num).toString() + if (decimal.length <= 2) return str + + return num.toFixed(4).replace(/\.?0+$/, '') + } + return ( <> {props.label} - {props.description &&

{props.description}

} + {props.description && !isValidPrice && ( +

{props.description}

+ )} + {isValidPrice && ( +
+ {hasDiscount ? ( + <> +

¥{formatPrice(discountedPrice!)}/IP

+

原价:{formatPrice(props.price!)}/IP

+ {/* + {props.discount}折 + */} + + ) : ( +

¥{formatPrice(props.price!)}/IP

+ )} +
+ )} )}
diff --git a/src/components/composites/purchase/shared/billing-method-field.tsx b/src/components/composites/purchase/shared/billing-method-field.tsx index 003aeeb..b20f8ed 100644 --- a/src/components/composites/purchase/shared/billing-method-field.tsx +++ b/src/components/composites/purchase/shared/billing-method-field.tsx @@ -34,16 +34,6 @@ export function BillingMethodField(props: { }} className="flex gap-4 max-md:flex-col" > - - {props.modeList.includes('1') && ( - - )} {props.modeList.includes('2') && ( )} + {props.modeList.includes('1') && ( + + )} )} diff --git a/src/components/composites/purchase/shared/side-panel.tsx b/src/components/composites/purchase/shared/side-panel.tsx index b18bebf..30c7d58 100644 --- a/src/components/composites/purchase/shared/side-panel.tsx +++ b/src/components/composites/purchase/shared/side-panel.tsx @@ -11,7 +11,7 @@ import {FieldPayment} from './field-payment' import {buildPurchaseResource, PurchaseKind, PurchaseSelection} from './resource' import {getPrice, getPriceHome} from '@/actions/resource' import {ExtraResp} from '@/lib/api' -import {formatPurchaseLiveLabel} from './sku' +import {formatPurchaseLiveLabel, getPurchaseSkuCountMin, PurchaseSkuData} from './sku' import {User} from '@/lib/models' import {PurchaseFormValues} from './form-values' import {IdCard} from 'lucide-react' @@ -25,6 +25,7 @@ const emptyPrice: ExtraResp = { export type PurchaseSidePanelProps = { kind: PurchaseKind + skuData: PurchaseSkuData } export function PurchaseSidePanel(props: PurchaseSidePanelProps) { @@ -44,7 +45,7 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) { expire, dailyLimit, } - const {priceData, isLoading, isError} = usePurchasePrice(profile, selection) + const {priceData, isLoading, isError} = usePurchasePrice(profile, selection, props.skuData) const {price, actual: discountedPrice = '0.00'} = priceData const totalDiscount = getTotalDiscount(price, discountedPrice) const hasDiscount = Number(totalDiscount) > 0 @@ -154,7 +155,7 @@ export function PurchaseSidePanel(props: PurchaseSidePanelProps) { ) } -function usePurchasePrice(profile: User | null, selection: PurchaseSelection) { +function usePurchasePrice(profile: User | null, selection: PurchaseSelection, skuData: PurchaseSkuData) { const [priceData, setPriceData] = useState>(emptyPrice) const [isLoading, setIsLoading] = useState(true) const [isError, setIsError] = useState(false) @@ -164,6 +165,15 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) { useEffect(() => { const requestId = ++requestIdRef.current + const expireValue = mode === '1' ? expire : '0' + const countMin = getPurchaseSkuCountMin(skuData, {mode, live, expire: expireValue}) + const quantity = mode === '1' ? dailyLimit : quota + if (countMin > 0 && quantity < countMin) { + setIsLoading(false) + setIsError(false) + return + } + const loadPrice = async () => { setIsLoading(true) setIsError(false) @@ -177,6 +187,7 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) { expire, dailyLimit, }) + const response = profile ? await getPrice(resource) : await getPriceHome(resource) @@ -184,7 +195,9 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) { if (requestId !== requestIdRef.current) { return } - + if (!response.success) { + throw new Error(response.message) + } if (response.success) { setPriceData({ price: response.data.price, @@ -211,7 +224,7 @@ function usePurchasePrice(profile: User | null, selection: PurchaseSelection) { } loadPrice() - }, [dailyLimit, expire, kind, live, mode, profile, quota]) + }, [dailyLimit, expire, kind, live, mode, profile, quota, skuData]) return {priceData, isLoading, isError} } diff --git a/src/components/composites/purchase/shared/sku.ts b/src/components/composites/purchase/shared/sku.ts index 1bca87a..4f432e4 100644 --- a/src/components/composites/purchase/shared/sku.ts +++ b/src/components/composites/purchase/shared/sku.ts @@ -8,12 +8,14 @@ export type PurchaseSkuItem = { expire: string price: string count_min: number + discount: number } export type PurchaseSkuData = { items: PurchaseSkuItem[] priceMap: Map countMinMap: Map + discountMap: Map modeList: PurchaseMode[] liveList: string[] expireList: string[] @@ -27,6 +29,7 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s const items: PurchaseSkuItem[] = [] const priceMap = new Map() const countMinMap = new Map() + const discountMap = new Map() const modeSet = new Set() const liveSet = new Set() const expireSet = new Set() @@ -51,6 +54,7 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s const countMin = typeof sku.count_min === 'number' ? sku.count_min : Number(sku.count_min) || 0 countMinMap.set(code, countMin) + const skuDiscount = sku.discount ?? 100 items.push({ code, mode, @@ -58,8 +62,10 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s expire: expireValue, price: sku.price, count_min: countMin, + discount: skuDiscount, }) priceMap.set(code, sku.price) + discountMap.set(code, skuDiscount) modeSet.add(mode) liveSet.add(live) @@ -82,6 +88,7 @@ export function parsePurchaseSkuList(kind: PurchaseKind, skuList: ProductItem['s items, priceMap, countMinMap, + discountMap, modeList: (['2', '1'] as const).filter(mode => modeSet.has(mode)), liveList: sortNumericValues(liveSet), expireList: sortNumericValues(expireSet), @@ -157,6 +164,14 @@ export function getPurchaseSkuPrice(priceMap: Map, props: { return priceMap.get(getPurchaseSkuKey(props)) } +export function getPurchaseSkuDiscount(discountMap: Map, props: { + mode: PurchaseMode + live: string + expire: string +}) { + return discountMap.get(getPurchaseSkuKey(props)) +} + export function formatPurchaseLiveLabel(live: string, kind: PurchaseKind) { const minutes = Number(live) diff --git a/src/components/composites/purchase/short/center.tsx b/src/components/composites/purchase/short/center.tsx index 6bbdea6..3f7660f 100644 --- a/src/components/composites/purchase/short/center.tsx +++ b/src/components/composites/purchase/short/center.tsx @@ -9,7 +9,7 @@ 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, getPurchaseSkuCountMin, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' +import {getAvailablePurchaseExpires, getAvailablePurchaseLives, getPurchaseSkuCountMin, getPurchaseSkuDiscount, getPurchaseSkuPrice, hasPurchaseSku, PurchaseSkuData} from '../shared/sku' export default function Center({ skuData, @@ -20,7 +20,7 @@ export default function Center({ const type = useWatch({name: 'type'}) as Schema['type'] const live = useWatch({name: 'live'}) as Schema['live'] const expire = useWatch({name: 'expire'}) as Schema['expire'] - const {modeList, priceMap} = skuData + const {modeList, priceMap, discountMap} = skuData const liveList = type === '1' ? getAvailablePurchaseLives(skuData, {mode: type, expire}) : getAvailablePurchaseLives(skuData, {mode: type}) @@ -145,6 +145,11 @@ export default function Center({ live, expire: priceExpire, }) + const discount = getPurchaseSkuDiscount(discountMap, { + mode: type, + live, + expire: priceExpire, + }) const minutes = Number(live) const hours = minutes / 60 const label = minutes % 60 === 0 ? `${hours} 小时` : `${minutes} 分钟` @@ -155,6 +160,8 @@ export default function Center({ value={live} label={label} description={price && `¥${price}/IP`} + price={price} + discount={discount} compare={field.value} /> ) diff --git a/src/components/composites/purchase/short/form.tsx b/src/components/composites/purchase/short/form.tsx index 4f0dd43..f27002c 100644 --- a/src/components/composites/purchase/short/form.tsx +++ b/src/components/composites/purchase/short/form.tsx @@ -20,7 +20,7 @@ export type Schema = z.infer export default function ShortForm({skuList}: {skuList: ProductItem['skus']}) { const skuData = parsePurchaseSkuList('short', skuList) - const defaultMode = skuData.modeList.includes('1') ? '1' : '2' + const defaultMode = skuData.modeList.includes('2') ? '2' : '1' const defaultLive = getAvailablePurchaseLives(skuData, {mode: defaultMode})[0] || '' const defaultExpire = defaultMode === '1' ? getAvailablePurchaseExpires(skuData, {mode: defaultMode, live: defaultLive})[0] || '0' @@ -42,11 +42,12 @@ export default function ShortForm({skuList}: {skuList: ProductItem['skus']}) { pay_type: 'balance', // 余额支付 }, }) + console.log(skuData, 'skuData') return (
- + ) } diff --git a/src/lib/models/product-sku.ts b/src/lib/models/product-sku.ts index 6c81846..71f256d 100644 --- a/src/lib/models/product-sku.ts +++ b/src/lib/models/product-sku.ts @@ -7,5 +7,6 @@ export type ProductSku = { price_min: string product_id: number discount_id: number + discount?: number status: number }