diff --git a/package.json b/package.json index e738bf4..978f51e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lanhu-web", - "version": "1.4.0", + "version": "1.5.0", "private": true, "scripts": { "dev": "next dev -H 0.0.0.0 --turbopack", diff --git a/src/actions/product.ts b/src/actions/product.ts new file mode 100644 index 0000000..e1ca0d4 --- /dev/null +++ b/src/actions/product.ts @@ -0,0 +1,12 @@ +import {callByUser, callPublic} from './base' +import {Product} from '@/lib/models/product' + +export type ProductItem = Product + +export async function listProduct(props: {}) { + return callByUser('/api/product/list', props) +} + +export async function listProductHome(props: {}) { + return callPublic('/api/product/list', props) +} diff --git a/src/actions/resource.ts b/src/actions/resource.ts index d2c9319..03fbab4 100644 --- a/src/actions/resource.ts +++ b/src/actions/resource.ts @@ -91,7 +91,7 @@ export async function payClose(props: { export async function getPrice(props: CreateResourceReq) { return callByDevice<{ price: string - discounted_price?: string - discounted?: number + actual?: string + discounted?: string }>('/api/resource/price', props) } diff --git a/src/actions/user.ts b/src/actions/user.ts index 4c1acff..bf54c33 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -38,16 +38,16 @@ export async function Identify(props: { } export async function update(props: { - username: string - email: string - contact_qq: string - contact_wechat: string + username?: string + email?: string + contact_qq?: string + contact_wechat?: string }) { return await callByUser('/api/user/update', props) } export async function updatePassword(props: { - phone: string + // phone: string code: string password: string }) { diff --git a/src/actions/verify.ts b/src/actions/verify.ts index 0ec2dbf..473380d 100644 --- a/src/actions/verify.ts +++ b/src/actions/verify.ts @@ -1,6 +1,6 @@ 'use server' import {ApiResponse} from '@/lib/api' -import {callByDevice} from '@/actions/base' +import {callByDevice, callByUser} from '@/actions/base' import {getCap} from '@/lib/cap' export async function sendSMS(props: { @@ -38,3 +38,35 @@ export async function sendSMS(props: { throw new Error('验证码验证失败', {cause: error}) } } + +export async function updateSendSMS(props: { + captcha: string +}): Promise { + try { + // 人机验证 + if (!props.captcha?.length) { + return { + success: false, + status: 400, + message: '请输入验证码', + } + } + + const cap = await getCap() + const valid = await cap.validateToken(props.captcha) + if (!valid) { + return { + success: false, + status: 400, + message: '验证码错误或已过期', + } + } + + // 请求发送短信 + return await callByUser('/api/verify/sms/password', {}) + } + catch (error) { + console.error('验证码验证失败:', error) + throw new Error('验证码验证失败', {cause: error}) + } +} diff --git a/src/app/(auth)/login/login-card.tsx b/src/app/(auth)/login/login-card.tsx index 863144d..fd454c1 100644 --- a/src/app/(auth)/login/login-card.tsx +++ b/src/app/(auth)/login/login-card.tsx @@ -34,20 +34,12 @@ export type LoginSchema = zod.infer export default function LoginCard() { const router = useRouter() const refreshProfile = useProfileStore(store => store.refreshProfile) - const [mode, setMode] = useState('phone_code') + const [mode, setMode] = useState('password') const [submitting, setSubmitting] = useState(false) const updateLoginMode = (mode: LoginMode) => { sessionStorage.setItem('login_mode', mode) } - - useEffect(() => { - const mode = sessionStorage.getItem('login_mode') - if (mode) { - setMode(mode as LoginMode) - } - }, []) - const form = useForm({ resolver: zodResolver(mode === 'phone_code' ? smsSchema : pwdSchema), defaultValues: { @@ -55,7 +47,16 @@ export default function LoginCard() { password: '', remember: false, }, + mode: 'onChange', }) + + useEffect(() => { + const savedMode = sessionStorage.getItem('login_mode') as LoginMode + if (savedMode && savedMode === 'phone_code') { + setMode(savedMode) + } + }, []) + const handler = form.handleSubmit(async (data) => { setSubmitting(true) try { @@ -93,13 +94,14 @@ export default function LoginCard() { { - setMode(val as typeof mode) - form.reset({username: form.getValues('username'), password: '', remember: false}) + setMode(val as LoginMode) + form.reset({username: '', password: '', remember: false}) + form.clearErrors() }} className="mb-6"> 密码登录 - 验证码登录 + 验证码登录/注册 className="space-y-6" form={form} handler={handler}> @@ -124,6 +126,7 @@ export default function LoginCard() { className="h-10" placeholder="请输入验证码" autoComplete="one-time-code" + disabled={submitting} /> @@ -137,6 +140,7 @@ export default function LoginCard() { placeholder="至少6位密码,需包含字母和数字" autoComplete="current-password" minLength={6} + disabled={submitting} /> + ) : ( diff --git a/src/app/admin/resources/_components/list.tsx b/src/app/admin/resources/_components/list.tsx index dc15ab0..634d933 100644 --- a/src/app/admin/resources/_components/list.tsx +++ b/src/app/admin/resources/_components/list.tsx @@ -219,6 +219,17 @@ export default function ResourceList({resourceType}: ResourceListProps) { header: '开通时间', cell: ({row}) => formatDateTime(row.original.created_at), }, + { + header: '状态', + cell: ({row}) => { + const isActive = row.original.active + return ( + + {isActive ? '启用' : '禁用'} + + ) + }, + }, ] // 短效资源增加到期时间列 diff --git a/src/components/composites/dialogs/change-password-dialog.tsx b/src/components/composites/dialogs/change-password-dialog.tsx index ff9af17..7067201 100644 --- a/src/components/composites/dialogs/change-password-dialog.tsx +++ b/src/components/composites/dialogs/change-password-dialog.tsx @@ -4,7 +4,7 @@ import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTr import {Button} from '@/components/ui/button' import {Form, FormField} from '@/components/ui/form' import {Input} from '@/components/ui/input' -import {useForm, useFormContext, useWatch} from 'react-hook-form' +import {useForm, useFormContext} from 'react-hook-form' import {zodResolver} from '@hookform/resolvers/zod' import * as z from 'zod' import {toast} from 'sonner' @@ -14,7 +14,6 @@ import dynamic from 'next/dynamic' // 表单验证规则 const schema = z.object({ - phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`), captcha: z.string().nonempty('请输入验证码'), code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`), password: z.string().min(6, `密码至少6位`), @@ -32,6 +31,7 @@ interface ChangePasswordDialogProps { defaultOpen?: boolean onOpenChange?: (open: boolean) => void onSuccess?: () => void + phone?: string } export function ChangePasswordDialog({ @@ -40,12 +40,28 @@ export function ChangePasswordDialog({ defaultOpen, onOpenChange, onSuccess, + phone, }: ChangePasswordDialogProps) { const [internalOpen, setInternalOpen] = useState(defaultOpen || false) const router = useRouter() const actualOpen = open !== undefined ? open : internalOpen - const actualOnOpenChange = onOpenChange || setInternalOpen + const actualOnOpenChange = (open: boolean) => { + if (!open) { + form.reset({ + captcha: '', + code: '', + password: '', + confirm_password: '', + }) + } + if (onOpenChange) { + onOpenChange(open) + } + else { + setInternalOpen(open) + } + } // 表单初始化 const form = useForm({ @@ -59,7 +75,6 @@ export function ChangePasswordDialog({ ), ), defaultValues: { - phone: '', captcha: '', code: '', password: '', @@ -71,7 +86,6 @@ export function ChangePasswordDialog({ const handler = async (value: Schema) => { try { const resp = await updatePassword({ - phone: value.phone, code: value.code, password: value.password, }) @@ -108,14 +122,12 @@ export function ChangePasswordDialog({ 修改密码 - {/* 手机号输入 */} - name="phone" label="手机号" className="flex-auto"> - {({field}) => ( - - )} - - - {/* 短信验证码 */} + {phone && ( +
+ 当前用户手机号: + {phone} +
+ )}
name="code" label="验证码" className="flex-auto"> {({field}) => ( @@ -124,15 +136,11 @@ export function ChangePasswordDialog({
- - {/* 新密码 */} name="password" label="新密码" className="flex-auto"> {({field}) => ( )} - - {/* 确认密码 */} name="confirm_password" label="确认密码" className="flex-auto"> {({field}) => ( @@ -144,8 +152,13 @@ export function ChangePasswordDialog({ theme="outline" type="button" onClick={() => { + form.reset({ + captcha: '', + code: '', + password: '', + confirm_password: '', + }) actualOnOpenChange(false) - form.reset() }}> 关闭 @@ -159,8 +172,7 @@ export function ChangePasswordDialog({ function SendMsgByPhone() { const {control} = useFormContext() - const phone = useWatch({control, name: 'phone'}) - return + return } -const SendMsg = dynamic(() => import('@/components/send-msg'), {ssr: false}) +const SendMsg = dynamic(() => import('@/components/updateSend-msg'), {ssr: false}) diff --git a/src/components/composites/extract/index.tsx b/src/components/composites/extract/index.tsx index 1998f98..61f4fa7 100644 --- a/src/components/composites/extract/index.tsx +++ b/src/components/composites/extract/index.tsx @@ -337,8 +337,6 @@ function SelectResource() { setStatus('load') try { const resp = await allResource() - console.log(resp, '/api/resource/all') - if (!resp.success) { throw new Error('获取套餐失败,请稍后再试') } diff --git a/src/components/composites/purchase/index.tsx b/src/components/composites/purchase/index.tsx index 9508b95..0c10d79 100644 --- a/src/components/composites/purchase/index.tsx +++ b/src/components/composites/purchase/index.tsx @@ -1,11 +1,12 @@ 'use client' -import {ReactNode} from 'react' +import {ReactNode, useEffect, useState} from 'react' import {merge} from '@/lib/utils' import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs' import LongForm from '@/components/composites/purchase/long/form' import ShortForm from '@/components/composites/purchase/short/form' import {usePathname, useRouter, useSearchParams} from 'next/navigation' import SelfDesc from '@/components/features/self-desc' +import {listProduct, ProductItem} from '@/actions/product' export type TabType = 'short' | 'long' | 'fixed' | 'custom' export default function Purchase() { @@ -13,7 +14,8 @@ export default function Purchase() { const path = usePathname() const params = useSearchParams() - const tab = params.get('type') as TabType || 'short' + const [productList, setProductList] = useState([]) + const tab = (params.get('type') as TabType) || productList[0]?.code || 'short' const updateTab = (tab: string) => { const newParams = new URLSearchParams(params) @@ -21,27 +23,47 @@ export default function Purchase() { router.push(`${path}?${newParams.toString()}`) } + useEffect(() => { + const fetchProducts = async () => { + const res = await listProduct({}) + if (res.success) { + setProductList(res.data) + } + } + fetchProducts() + }, []) + + const currentProduct = productList.find(item => item.code === tab) + const currentSkuList = currentProduct?.skus || [] + const componentMap: Record> = { + short: ShortForm, + long: LongForm, + // static: StaticForm + } return (
- 短效动态 - 长效静态 - 固定套餐 + {productList.map(item => ( + + {item.name} + + ))} + {/* 固定的定制套餐tab */} 定制套餐 - - - - - - - - + {productList.map((item) => { + const Component = componentMap[item.code] + const skuList = item.skus || [] + return ( + + {Component ? :
页面待开发中
} +
+ ) + })} + - { - router.push('/custom') - }}/> + router.push('/custom')}/>
diff --git a/src/components/composites/purchase/long/center.tsx b/src/components/composites/purchase/long/center.tsx index 701452e..1ff6fa7 100644 --- a/src/components/composites/purchase/long/center.tsx +++ b/src/components/composites/purchase/long/center.tsx @@ -10,72 +10,22 @@ import check from '../_assets/check.svg' import {Schema} from '@/components/composites/purchase/long/form' import {useFormContext, useWatch} from 'react-hook-form' import {Card} from '@/components/ui/card' -import {useEffect} from 'react' -export default function Center() { +export default function Center({map, expireList, liveList}: { + map: Map + liveList: string[] + expireList: string[] +}) { const form = useFormContext() const type = useWatch({name: 'type'}) - useEffect(() => { - if (type === '1') { - form.setValue('daily_limit', 100) - } - else { - form.setValue('quota', 500) - } - }, [type, form]) + return ( {/* 计费方式 */} - - {({id, field}) => ( - - - - - - - - )} - - + {/* IP 时效 */} - - {({id, field}) => ( - - - - - - - - - )} - + {/* 根据套餐类型显示不同表单项 */} {type === '2' ? ( @@ -126,26 +76,7 @@ export default function Center() { ) : ( <> {/* 包时:套餐时效 */} - - {({id, field}) => ( - - - - - - - - - - )} - + {/* 包时:每日提取上限 */} ) } + +function BillingMethod(props: { + expireList: string[] +}) { + const {setValue} = useFormContext() + return ( + + {({id, field}) => ( + { + field.onChange(v) + if (v === '2') { + setValue('expire', '0') + } + else if (props.expireList.length > 0) { + setValue('expire', props.expireList[0]) + } + }} + className="flex gap-4 max-md:flex-col"> + + + + + + + )} + + ) +} + +function IpTime({map, liveList}: { + map: Map + liveList: string[]}) { + const {control, getValues} = useFormContext() + const values = useWatch({control}) + + return ( + + {({id, field}) => ( + + {liveList.map((live) => { + const params = new URLSearchParams() + params.set('mode', { + 1: 'time', + 2: 'quota', + }[values.type || '2']) + params.set('live', live || '0') + params.set('expire', values.expire || '0') + const price = map.get(params.toString()) + return ( + + ) + })} + + + )} + + ) +} + +function ComboValidity({expireList}: {expireList: string[]}) { + return ( + + {({id, field}) => ( + { + field.onChange(val) + }} + className="flex gap-4 flex-wrap" + > + {expireList.map(item => ( + + ))} + + )} + + ) +} diff --git a/src/components/composites/purchase/long/form.tsx b/src/components/composites/purchase/long/form.tsx index ce9d86a..03e5c63 100644 --- a/src/components/composites/purchase/long/form.tsx +++ b/src/components/composites/purchase/long/form.tsx @@ -5,13 +5,14 @@ import Right from '@/components/composites/purchase/long/right' import {Form} from '@/components/ui/form' import * as z from 'zod' import {zodResolver} from '@hookform/resolvers/zod' +import {ProductItem} from '@/actions/product' // 定义表单验证架构 const schema = z.object({ type: z.enum(['1', '2']).default('2'), - live: z.enum(['1', '4', '8', '12', '24']), + live: z.string(), quota: z.number().min(500, '购买数量不能少于 500 个'), - expire: z.enum(['7', '15', '30', '90', '180', '365']), + expire: z.string(), daily_limit: z.number().min(100, '每日限额不能少于 100 个'), pay_type: z.enum(['wechat', 'alipay', 'balance']), }) @@ -19,14 +20,31 @@ const schema = z.object({ // 从架构中推断类型 export type Schema = z.infer -export default function LongForm() { +export default function LongForm({skuList}: {skuList: ProductItem['skus']}) { + if (!skuList) throw new Error('没有套餐数据') + + const map = new Map() + // const _modeList = new Set() + const _liveList = new Set() + const _expireList = new Set() + for (const sku of skuList) { + const params = new URLSearchParams(sku.code) + // _modeList.add(params.get('mode') || '') + _liveList.add(Number(params.get('live'))) + _expireList.add(Number(params.get('expire'))) + map.set(sku.code, sku.price) + } + // const modeList = Array.from(_modeList).filter(Boolean) + const liveList = Array.from(_liveList).filter(Boolean).map(String) + const expireList = Array.from(_expireList).filter(Boolean).map(String) + const form = useForm({ resolver: zodResolver(schema), defaultValues: { type: '2', // 默认为包量套餐 - live: '1', // 小时 + live: liveList[0], // 分钟 + expire: '0', // 天 quota: 500, - expire: '30', // 天 daily_limit: 100, pay_type: 'balance', // 余额支付 }, @@ -34,7 +52,7 @@ export default function LongForm() { return (
-
+
) diff --git a/src/components/composites/purchase/long/right.tsx b/src/components/composites/purchase/long/right.tsx index 64a2780..97538f7 100644 --- a/src/components/composites/purchase/long/right.tsx +++ b/src/components/composites/purchase/long/right.tsx @@ -22,8 +22,8 @@ export default function Right() { const dailyLimit = useWatch({control, name: 'daily_limit'}) const [priceData, setPriceData] = useState>({ price: '0.00', - discounted_price: '0.00', - discounted: 0, + actual: '0.00', + discounted: '0.00', }) useEffect(() => { @@ -32,20 +32,18 @@ export default function Right() { const resp = await getPrice({ type: 2, long: { - live: Number(live) * 60, + live: Number(live), mode: Number(mode), quota: mode === '1' ? Number(dailyLimit) : Number(quota), expire: mode === '1' ? Number(expire) : undefined, }, }) - if (!resp.success) { throw new Error('获取价格失败') } - setPriceData({ price: resp.data.price, - discounted_price: resp.data.discounted_price ?? resp.data.price ?? '', + actual: resp.data.actual ?? resp.data.price ?? '', discounted: resp.data.discounted, }) } @@ -53,16 +51,28 @@ export default function Right() { console.error('获取价格失败:', error) setPriceData({ price: '0.00', - discounted_price: '0.00', - discounted: 0, + actual: '0.00', + discounted: '0.00', }) } } price() }, [dailyLimit, expire, live, quota, mode]) - const {price, discounted_price: discountedPrice = '', discounted} = priceData + const {price, actual: discountedPrice = ''} = priceData + // 计算总折扣价(原价 - 实付价格) + const calculateTotalDiscount = () => { + const originalPrice = parseFloat(price) + const actualPrice = parseFloat(discountedPrice) + if (isNaN(originalPrice) || isNaN(actualPrice)) { + return '0.00' + } + const discount = originalPrice - actualPrice + return discount.toFixed(2) + } + const totalDiscount = calculateTotalDiscount() + const hasDiscount = parseFloat(totalDiscount) > 0 return ( {live} {' '} - 小时 + 分钟 {mode === '2' ? ( @@ -93,11 +103,19 @@ export default function Right() {
  • - 实价 + 原价 ¥{price}
  • + {hasDiscount && ( +
  • + 总折扣 + + -¥{totalDiscount} + +
  • + )} ) : ( <> @@ -116,19 +134,19 @@ export default function Right() {
  • - 实价 + 原价 ¥{price}
  • - {/* {discounted === 1 ? '' : ( + {hasDiscount && (
  • 总折扣 - -¥{discounted} + -¥{totalDiscount}
  • - )} */} + )} )} @@ -167,7 +185,7 @@ function BalanceOrLogin(props: { type: 2, long: { mode: Number(props.mode), - live: Number(props.live) * 60, + live: Number(props.live), expire: props.mode === '1' ? Number(props.expire) : undefined, quota: props.mode === '1' ? Number(props.dailyLimit) : Number(props.quota), }, diff --git a/src/components/composites/purchase/pay.tsx b/src/components/composites/purchase/pay.tsx index c15e628..b779b48 100644 --- a/src/components/composites/purchase/pay.tsx +++ b/src/components/composites/purchase/pay.tsx @@ -47,7 +47,6 @@ export default function Pay(props: PayProps) { payment_method: method, payment_platform: TradePlatform.Desktop, } - console.log(req, 'req') const resp = await prepareResource(req) diff --git a/src/components/composites/purchase/short/center.tsx b/src/components/composites/purchase/short/center.tsx index 31efff4..20608ad 100644 --- a/src/components/composites/purchase/short/center.tsx +++ b/src/components/composites/purchase/short/center.tsx @@ -10,19 +10,21 @@ import check from '../_assets/check.svg' import {useFormContext, useWatch} from 'react-hook-form' import {Schema} from '@/components/composites/purchase/short/form' import {Card} from '@/components/ui/card' -import {useEffect} from 'react' -export default function Center() { +export default function Center({ + priceMap, + liveList, + expireList, +}: { + priceMap: Map + liveList: string[] + expireList: string[] +}) { const form = useFormContext() const type = useWatch({name: 'type'}) - useEffect(() => { - if (type === '1') { - form.setValue('daily_limit', 2000) - } - else { - form.setValue('quota', 10000) - } - }, [type, form]) + const expire = useWatch({name: 'expire'}) + const isTime = type === '1' + return ( @@ -34,8 +36,17 @@ export default function Center() { {({id, field}) => ( { + field.onChange(v) + if (v === '2') { + form.setValue('expire', '0') + } + else if (expireList.length > 0) { + form.setValue('expire', expireList[0]) + form.setValue('daily_limit', 2000) + } + }} className="flex gap-4 max-md:flex-col"> ( - - - - - - + {liveList.map((live) => { + const params = new URLSearchParams() + params.set('mode', isTime ? 'time' : 'quota') + params.set('live', live) + params.set('expire', isTime ? expire : '0') + const price = priceMap.get(params.toString()) + const minutes = Number(live) + const hours = minutes / 60 + const label = minutes % 60 === 0 ? `${hours} 小时` : `${minutes} 分钟` + return ( + + ) + })} )} {/* 根据套餐类型显示不同表单项 */} - {type === '2' ? ( + {!isTime ? ( /* 包量:IP 购买数量 */ {/* 包时:套餐时效 */} - + {({id, field}) => ( - - - - - - - - + + {expireList.map(day => ( + + ))} )} @@ -153,8 +173,8 @@ export default function Center() { theme="outline" type="button" className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg" - onClick={() => form.setValue('daily_limit', Math.max(2_000, Number(field.value) - 1_000))} - disabled={Number(field.value) === 2_000}> + onClick={() => form.setValue('daily_limit', Math.max(2000, Number(field.value) - 1000))} + disabled={Number(field.value) === 2000}> { const value = Number(e.target.value) - if (value < 2_000) { - form.setValue('daily_limit', 2_000) + if (value < 2000) { + form.setValue('daily_limit', 2000) } }} /> @@ -175,7 +195,7 @@ export default function Center() { theme="outline" type="button" className="h-10 w-10 border border-gray-200 rounded-sm flex items-center justify-center text-lg" - onClick={() => form.setValue('daily_limit', Number(field.value) + 1_000)}> + onClick={() => form.setValue('daily_limit', Number(field.value) + 1000)}> @@ -212,10 +232,6 @@ export default function Center() { check IP资源定期筛选

    -

    - check - 完备的API接口 -

    check 包量/包时计费方式 diff --git a/src/components/composites/purchase/short/form.tsx b/src/components/composites/purchase/short/form.tsx index 3cb54fb..6e75922 100644 --- a/src/components/composites/purchase/short/form.tsx +++ b/src/components/composites/purchase/short/form.tsx @@ -5,28 +5,52 @@ import Right from '@/components/composites/purchase/short/right' import {Form} from '@/components/ui/form' import * as z from 'zod' import {zodResolver} from '@hookform/resolvers/zod' +import {ProductItem} from '@/actions/product' // 定义表单验证架构 const schema = z.object({ type: z.enum(['1', '2']).default('2'), - live: z.enum(['3', '5', '10', '15', '30']), - quota: z.number().min(10000, '购买数量不能少于10000个'), - expire: z.enum(['7', '15', '30', '90', '180', '365']), - daily_limit: z.number().min(2000, '每日限额不能少于2000个'), - pay_type: z.enum(['wechat', 'alipay', 'balance']), + live: z.string(), + quota: z.number().min(10000, '购买数量不能少于 10000 个'), + expire: z.string(), + daily_limit: z.number().min(2000, '每日限额不能少于 2000 个'), + pay_type: z.enum(['wechat', 'alipay', 'balance']).default('balance'), }) -// 从架构中推断类型 export type Schema = z.infer -export default function ShortForm() { +export default function ShortForm({skuList}: {skuList: ProductItem['skus']}) { + if (!skuList?.length) throw new Error('没有套餐数据') + + const priceMap = new Map() + const _liveList = new Set() + const _expireList = new Set() + + for (const sku of skuList) { + const params = new URLSearchParams(sku.code) + const mode = params.get('mode') + const live = params.get('live') + const expire = params.get('expire') + + if (live && live !== '0') { + _liveList.add(Number(live)) + } + if (mode === 'time' && expire && expire !== '0') { + _expireList.add(Number(expire)) + } + priceMap.set(sku.code, sku.price) + } + + const liveList = Array.from(_liveList).filter(Boolean).map(String) + const expireList = Array.from(_expireList).filter(Boolean).map(String) + const form = useForm({ resolver: zodResolver(schema), defaultValues: { type: '2', // 默认为包量套餐 - live: '3', // 分钟 - quota: 10_000, // >= 10000 - expire: '30', // 天 + live: liveList[0] || '', + expire: '0', // 包量模式下无效 + quota: 10_000, // >= 10000, daily_limit: 2_000, // >= 2000 pay_type: 'balance', // 余额支付 }, @@ -34,8 +58,8 @@ export default function ShortForm() { return (

    -
    - +
    + ) } diff --git a/src/components/composites/purchase/short/right.tsx b/src/components/composites/purchase/short/right.tsx index cbeec61..a8ec203 100644 --- a/src/components/composites/purchase/short/right.tsx +++ b/src/components/composites/purchase/short/right.tsx @@ -11,8 +11,9 @@ import {Card} from '@/components/ui/card' import {getPrice} from '@/actions/resource' import {ExtraResp} from '@/lib/api' import {FieldPayment} from '../shared/field-payment' +import {ProductItem} from '@/actions/product' -export default function Right() { +export default function Right({skuList}: {skuList: ProductItem['skus']}) { const {control} = useFormContext() const method = useWatch({control, name: 'pay_type'}) const live = useWatch({control, name: 'live'}) @@ -22,8 +23,8 @@ export default function Right() { const dailyLimit = useWatch({control, name: 'daily_limit'}) const [priceData, setPriceData] = useState>({ price: '0.00', - discounted_price: '0.00', - discounted: 0, + actual: '0.00', + discounted: '0.00', }) useEffect(() => { @@ -38,7 +39,6 @@ export default function Right() { expire: mode === '1' ? Number(expire) : undefined, }, }) - if (!priceResponse.success) { throw new Error('获取价格失败') } @@ -46,7 +46,7 @@ export default function Right() { const data = priceResponse.data setPriceData({ price: data.price, - discounted_price: data.discounted_price ?? data.price ?? '', + actual: data.actual ?? data.price ?? '', discounted: data.discounted, }) } @@ -54,15 +54,29 @@ export default function Right() { console.error('获取价格失败:', error) setPriceData({ price: '0.00', - discounted_price: '0.00', - discounted: 0, + actual: '0.00', + discounted: '0.00', }) } } price() }, [expire, live, quota, mode, dailyLimit]) - const {price, discounted_price: discountedPrice = '', discounted} = priceData + const {price, actual: discountedPrice = ''} = priceData + + // 计算总折扣价(原价 - 实付价格) + const calculateTotalDiscount = () => { + const originalPrice = parseFloat(price) + const actualPrice = parseFloat(discountedPrice) + if (isNaN(originalPrice) || isNaN(actualPrice)) { + return '0.00' + } + const discount = originalPrice - actualPrice + return discount.toFixed(2) + } + + const totalDiscount = calculateTotalDiscount() + const hasDiscount = parseFloat(totalDiscount) > 0 return (
  • - 实价 + 原价 ¥{price}
  • + {hasDiscount && ( +
  • + 总折扣 + + -¥{totalDiscount} + +
  • + )} ) : ( <> @@ -117,19 +139,19 @@ export default function Right() {
  • - 实价 + 原价 ¥{price}
  • - {/* {discounted === 1 ? '' : ( + {hasDiscount && (
  • 总折扣 - -¥{discounted === 1 ? '' : discounted} + -¥{totalDiscount}
  • - )} */} + )} )} diff --git a/src/components/updateSend-msg.tsx b/src/components/updateSend-msg.tsx new file mode 100644 index 0000000..049cfe0 --- /dev/null +++ b/src/components/updateSend-msg.tsx @@ -0,0 +1,78 @@ +'use client' +import {useEffect, useRef, useState} from 'react' +import Cap from '@cap.js/widget' +import {updateSendSMS} from '@/actions/verify' +import {toast} from 'sonner' +import {Button} from '@/components/ui/button' +import {LoaderIcon} from 'lucide-react' + +export default function SendMsg() { + const [countdown, setCountdown] = useState(0) + const [progress, setProgress] = useState(0) + const [mode, setMode] = useState<'ready' | 'wait' | 'check'>('ready') + const cap = useRef(new Cap({apiEndpoint: '/'})) + cap.current.addEventListener('progress', (event) => { + setProgress(event.detail.progress) + }) + + // 发送验证码 + const sendCode = async () => { + try { + setMode('check') + + // 完成挑战 + const result = await cap.current.solve() + if (!result.success || !cap.current.token) { + throw new Error('人机验证失败') + } + + // 发送验证码 + const resp = await updateSendSMS({ + captcha: cap.current.token, + }) + if (!resp.success) { + throw new Error(`验证码发送失败: ${resp.message}`) + } + + setMode('wait') + setCountdown(60) + toast.success('验证码已发送') + } + catch (e) { + setMode('ready') + toast.error('验证码发送失败', { + description: (e as Error).message, + }) + } + } + + // 计时 + useEffect(() => { + const interval = setInterval(() => { + if (countdown > 0) { + setCountdown(countdown - 1) + } + else if (mode === 'wait') { + setMode('ready') + } + }, 1000) + return () => clearInterval(interval) + }, [countdown, mode]) + + return ( + + ) +} diff --git a/src/lib/models/product-sku.ts b/src/lib/models/product-sku.ts new file mode 100644 index 0000000..43c63b7 --- /dev/null +++ b/src/lib/models/product-sku.ts @@ -0,0 +1,10 @@ +export type ProductSku = { + id: number + code: string + name: string + price: string + price_min: string + product_id: number + discount_id: number + status: number +} diff --git a/src/lib/models/product.ts b/src/lib/models/product.ts new file mode 100644 index 0000000..959e888 --- /dev/null +++ b/src/lib/models/product.ts @@ -0,0 +1,11 @@ +import {ProductSku} from './product-sku' + +export type Product = { + id: number + code: string + name: string + description: string + sort: number + status: number + skus?: ProductSku[] +}