1 Commits

Author SHA1 Message Date
2b77ea189b 支付组件统一使用二维码展示 2026-03-13 14:13:06 +08:00
16 changed files with 206 additions and 193 deletions

View File

@@ -1,5 +1,9 @@
## TODO
- 导航栏
- 账单页面
- 实名认证响应
分离公共 api 接口 env 定义
统一前端基础库类型api
@@ -127,11 +131,3 @@ stores共享状态组件
- 图片使用 Next.js Image 组件自动优化
- 动态导入 (next/dynamic) 实现纯客户端组件

View File

@@ -5,8 +5,10 @@ import {Loader} from 'lucide-react'
import {useState} from 'react'
import Image from 'next/image'
import {PaymentModalProps} from './payment-modal'
import {getTradeMethodDecoration} from '@/lib/models/trade'
export function DesktopPayment(props: PaymentModalProps) {
const decoration = getTradeMethodDecoration(props.method)
const [loading, setLoading] = useState(false)
const onSubmit = async () => {
@@ -19,10 +21,10 @@ export function DesktopPayment(props: PaymentModalProps) {
<DialogContent>
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">
{props.decoration.icon ? (
{decoration.icon ? (
<Image
src={props.decoration.icon}
alt={props.decoration.text}
src={decoration.icon}
alt={decoration.text}
width={24}
height={24}
className="rounded-md"
@@ -30,7 +32,7 @@ export function DesktopPayment(props: PaymentModalProps) {
) : (
<div className="w-6 h-6 bg-gray-200 rounded-full"/>
)}
<span>{props.decoration.text}</span>
<span>{decoration.text}</span>
</DialogTitle>
</DialogHeader>
@@ -43,7 +45,7 @@ export function DesktopPayment(props: PaymentModalProps) {
/>
<p className="text-sm text-gray-600">
使
{props.decoration.text}
{decoration.text}
</p>

View File

@@ -6,8 +6,10 @@ import {CreditCard, Loader} from 'lucide-react'
import {useState} from 'react'
import Image from 'next/image'
import {PaymentModalProps} from './payment-modal'
import {getTradeMethodDecoration} from '@/lib/models/trade'
export function MobilePayment(props: PaymentModalProps) {
const decoration = getTradeMethodDecoration(props.method)
const [loading, setLoading] = useState(false) // 加载状态
const [paymentInitiated, setPaymentInitiated] = useState(false) // 是否已发起支付
@@ -54,16 +56,16 @@ export function MobilePayment(props: PaymentModalProps) {
<div className="flex justify-between">
<span className="text-gray-600"></span>
<div className="flex items-center gap-2">
{props.decoration.icon && (
{decoration.icon && (
<Image
src={props.decoration.icon}
alt={props.decoration.text}
src={decoration.icon}
alt={decoration.text}
width={28}
height={28}
className="rounded-md"
/>
)}
<span>{props.decoration.text}</span>
<span>{decoration.text}</span>
</div>
</div>
<div className="flex justify-between">

View File

@@ -6,7 +6,7 @@ import {Dialog} from '@/components/ui/dialog'
import {PaymentProps} from './type'
import {payClose} from '@/actions/resource'
import {useEffect} from 'react'
import {useRouter} from 'next/navigation'
import {UniversalDesktopPayment} from './universal-desktop-payment'
export type PaymentModalProps = {
onConfirm: (showFail: boolean) => Promise<void>
@@ -61,17 +61,13 @@ export function PaymentModal(props: PaymentModalProps) {
onOpenChange={(open) => {
if (!open) handleClose()
}}>
{props.platform === TradePlatform.Mobile ? (
<MobilePayment
{...props}
onClose={handleClose}
/>
) : (
<DesktopPayment
{...props}
onClose={handleClose}
/>
)}
{/* {props.platform === TradePlatform.Mobile
? <MobilePayment {...props} onClose={handleClose}/>
: <DesktopPayment {...props} onClose={handleClose}/>
} */}
<UniversalDesktopPayment {...props} onClose={handleClose}/>
</Dialog>
)
}

View File

@@ -8,8 +8,4 @@ export type PaymentProps = {
amount: number
platform: TradePlatform
method: TradeMethod
decoration: {
icon: StaticImageData
text: string
}
}

View File

@@ -0,0 +1,83 @@
'use client'
import {DialogClose, DialogContent, DialogHeader, DialogTitle} from '@/components/ui/dialog'
import {Button} from '@/components/ui/button'
import {Loader} from 'lucide-react'
import {useState} from 'react'
import Image from 'next/image'
import {PaymentModalProps} from './payment-modal'
import {getTradeMethodDecoration, TradePlatform} from '@/lib/models/trade'
export function UniversalDesktopPayment(props: PaymentModalProps) {
const decoration = getTradeMethodDecoration(props.method)
const [loading, setLoading] = useState(false)
const onSubmit = async () => {
setLoading(true)
await props.onConfirm(true)
setLoading(false)
}
return (
<DialogContent>
<DialogHeader>
<DialogTitle className="flex gap-2 items-center">
{decoration.icon ? (
<Image
src={decoration.icon}
alt={decoration.text}
width={24}
height={24}
className="rounded-md"
/>
) : (
<div className="w-6 h-6 bg-gray-200 rounded-full"/>
)}
<span>{decoration.text}</span>
</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center gap-4">
<Image
src={props.pay_url}
width={208}
height={208}
alt="二维码"
/>
<div className="flex-none flex flex-col gap-1 items-center">
<p className="text-sm text-gray-600">
使
</p>
<p className="text-sm text-gray-600">
</p>
</div>
<div className="w-full text-center space-y-2">
<p className="text-sm font-medium">
:
<span className="text-accent">
¥
{props.amount?.toFixed(2) || '0.00'}
</span>
</p>
<p className="text-xs text-gray-500">
:
{props.inner_no}
</p>
</div>
<div className="flex gap-4 w-full justify-center">
<Button onClick={onSubmit}>
{loading && <Loader className="animate-spin mr-2"/>}
</Button>
<DialogClose asChild>
<Button theme="outline" onClick={() => props.onClose?.()}>
</Button>
</DialogClose>
</div>
</div>
</DialogContent>
)
}

View File

@@ -1,6 +1,5 @@
'use client'
import {createContext} from 'react'
import {useForm, UseFormReturn} from 'react-hook-form'
import {useForm} from 'react-hook-form'
import Center from '@/components/composites/purchase/long/center'
import Right from '@/components/composites/purchase/long/right'
import {Form} from '@/components/ui/form'
@@ -20,13 +19,6 @@ const schema = z.object({
// 从架构中推断类型
export type Schema = z.infer<typeof schema>
type PurchaseFormContextType = {
form: UseFormReturn<Schema>
onSubmit?: () => void
}
export const LongFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
export default function LongForm() {
const form = useForm<Schema>({
resolver: zodResolver(schema),
@@ -36,16 +28,14 @@ export default function LongForm() {
quota: 500,
expire: '30', // 天
daily_limit: 100,
pay_type: 'balance', // 余额支付
pay_type: 'wechat', // 余额支付
},
})
return (
<Form form={form} className="flex flex-col lg:flex-row gap-4">
<LongFormContext.Provider value={{form}}>
<Center/>
<Right/>
</LongFormContext.Provider>
<Center/>
<Right/>
</Form>
)
}

View File

@@ -1,15 +1,6 @@
'use client'
import {Suspense, use, useContext, useEffect, useMemo, useState} from 'react'
import {PurchaseFormContext} from '@/components/composites/purchase/short/form'
import {RadioGroup} from '@/components/ui/radio-group'
import {FormField} from '@/components/ui/form'
import FormOption from '@/components/composites/purchase/option'
import Image from 'next/image'
import alipay from '../_assets/alipay.svg'
import wechat from '../_assets/wechat.svg'
import balance from '../_assets/balance.svg'
import {Suspense, use, useEffect, useState} from 'react'
import {useProfileStore} from '@/components/stores/profile'
import RechargeModal from '@/components/composites/recharge'
import Pay from '@/components/composites/purchase/pay'
import {buttonVariants} from '@/components/ui/button'
import Link from 'next/link'
@@ -17,8 +8,9 @@ import {merge} from '@/lib/utils'
import {useFormContext, useWatch} from 'react-hook-form'
import {Schema} from '@/components/composites/purchase/long/form'
import {Card} from '@/components/ui/card'
import {getPrice, CreateResourceReq} from '@/actions/resource'
import {getPrice} from '@/actions/resource'
import {ExtraResp} from '@/lib/api'
import {FieldPayment} from '../shared/field-payment'
export default function Right() {
const {control} = useFormContext<Schema>()
@@ -164,52 +156,7 @@ function BalanceOrLogin(props: {
const profile = use(useProfileStore(store => store.profile))
return profile ? (
<>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500">账户余额</span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile?.balance}</span>
<RechargeModal/>
</p>
</div> */}
{/* <FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span>余额</span>
</FormOption> */}
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
{/* <FieldPayment/> */}
<Pay
method={props.method}
balance={profile.balance}

View File

@@ -11,7 +11,7 @@ import {useRouter} from 'next/navigation'
import {completeResource, createResource, CreateResourceReq, prepareResource} from '@/actions/resource'
import {
TradeMethod,
TradeMethodDecoration,
TradePlatform,
} from '@/lib/models/trade'
import {PaymentModal} from '@/components/composites/payment/payment-modal'
import {PaymentProps} from '@/components/composites/payment/type'
@@ -32,7 +32,7 @@ export default function Pay(props: PayProps) {
const [open, setOpen] = useState(false)
const [trade, setTrade] = useState<PaymentProps | null>(null)
const router = useRouter()
const platform = usePlatformType()
// const platform = usePlatformType()
const onOpen = async () => {
setOpen(true)
@@ -45,7 +45,7 @@ export default function Pay(props: PayProps) {
const req = {
...props.resource,
payment_method: method,
payment_platform: platform,
payment_platform: TradePlatform.Desktop,
}
const resp = await prepareResource(req)
@@ -60,9 +60,8 @@ export default function Pay(props: PayProps) {
inner_no: resp.data.trade_no,
pay_url: resp.data.pay_url,
amount: Number(props.amount),
platform: platform,
platform: TradePlatform.Desktop,
method: method,
decoration: TradeMethodDecoration[props.method],
})
}

View File

@@ -0,0 +1,57 @@
import FormOption from '../option'
import {RadioGroup} from '@/components/ui/radio-group'
import {FormField} from '@/components/ui/form'
import Image from 'next/image'
import alipay from '../_assets/alipay.svg'
import wechat from '../_assets/wechat.svg'
export function FieldPayment() {
return (
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500">账户余额</span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile.balance}</span>
<RechargeModal/>
</p>
</div> */}
{/* <FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span>余额</span>
</FormOption> */}
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
)
}

View File

@@ -1,6 +1,5 @@
'use client'
import {createContext} from 'react'
import {useForm, UseFormReturn} from 'react-hook-form'
import {useForm} from 'react-hook-form'
import Center from '@/components/composites/purchase/short/center'
import Right from '@/components/composites/purchase/short/right'
import {Form} from '@/components/ui/form'
@@ -20,16 +19,7 @@ const schema = z.object({
// 从架构中推断类型
export type Schema = z.infer<typeof schema>
type PurchaseFormContextType = {
form: UseFormReturn<Schema>
onSubmit?: () => void
}
export const PurchaseFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
export type PurchaseFormProps = {}
export default function PurchaseForm(props: PurchaseFormProps) {
export default function ShortForm() {
const form = useForm<Schema>({
resolver: zodResolver(schema),
defaultValues: {
@@ -38,7 +28,7 @@ export default function PurchaseForm(props: PurchaseFormProps) {
quota: 10_000, // >= 10000
expire: '30', // 天
daily_limit: 2_000, // >= 2000
pay_type: 'balance', // 余额支付
pay_type: 'wechat', // 余额支付
},
})

View File

@@ -1,23 +1,16 @@
'use client'
import {Suspense, use, useEffect, useMemo, useState} from 'react'
import {Suspense, use, useEffect, useState} from 'react'
import {Schema} from '@/components/composites/purchase/short/form'
import {RadioGroup} from '@/components/ui/radio-group'
import {FormField} from '@/components/ui/form'
import FormOption from '@/components/composites/purchase/option'
import Image from 'next/image'
import alipay from '../_assets/alipay.svg'
import wechat from '../_assets/wechat.svg'
import balance from '../_assets/balance.svg'
import {useProfileStore} from '@/components/stores/profile'
import RechargeModal from '@/components/composites/recharge'
import {buttonVariants} from '@/components/ui/button'
import Link from 'next/link'
import {merge} from '@/lib/utils'
import Pay from '@/components/composites/purchase/pay'
import {useFormContext, useWatch} from 'react-hook-form'
import {Card} from '@/components/ui/card'
import {CreateResourceReq, getPrice} from '@/actions/resource'
import {getPrice} from '@/actions/resource'
import {ExtraResp} from '@/lib/api'
import {FieldPayment} from '../shared/field-payment'
export default function Right() {
const {control} = useFormContext<Schema>()
@@ -165,52 +158,7 @@ function BalanceOrLogin(props: {
const profile = use(useProfileStore(store => store.profile))
return profile ? (
<>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6">
{({id, field}) => (
<RadioGroup
id={id}
defaultValue={field.value}
onValueChange={field.onChange}
className="flex flex-col gap-3">
{/* <div className="w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md">
<p className="flex items-center gap-3">
<Image src={balance} alt="余额icon"/>
<span className="text-sm text-gray-500">账户余额</span>
</p>
<p className="flex justify-between items-center">
<span className="text-xl">{profile.balance}</span>
<RechargeModal/>
</p>
</div> */}
{/* <FormOption
id={`${id}-balance`}
value="balance"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={balance} alt="余额 icon"/>
<span>余额</span>
</FormOption> */}
<FormOption
id={`${id}-wechat`}
value="wechat"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={wechat} alt="微信 logo"/>
<span></span>
</FormOption>
<FormOption
id={`${id}-alipay`}
value="alipay"
compare={field.value}
className="p-3 w-full flex-row gap-2 justify-center">
<Image src={alipay} alt="支付宝 logo"/>
<span></span>
</FormOption>
</RadioGroup>
)}
</FormField>
{/* <FieldPayment/> */}
<Pay
method={props.method}
balance={profile.balance}

View File

@@ -21,7 +21,6 @@ import {merge} from '@/lib/utils'
import {
TradePlatform,
TradeMethod,
TradeMethodDecoration,
} from '@/lib/models/trade'
import {PaymentModal} from '@/components/composites/payment/payment-modal'
import Image from 'next/image'
@@ -77,7 +76,6 @@ export default function RechargeModal(props: RechargeModelProps) {
amount: data.amount,
platform: platform,
method: method,
decoration: TradeMethodDecoration[data.method],
})
}
else {

View File

@@ -6,10 +6,9 @@ export const usePlatformType = (): TradePlatform => {
// 在SSR环境下返回默认值
const [platform, setPlatform] = useState<TradePlatform>(() => {
if (typeof window === 'undefined') return TradePlatform.Desktop
// return window.matchMedia('(max-width: 768px)').matches
// ? TradePlatform.Mobile
// : TradePlatform.Desktop
return TradePlatform.Desktop
return window.matchMedia('(max-width: 768px)').matches
? TradePlatform.Mobile
: TradePlatform.Desktop
})
useEffect(() => {

View File

@@ -1,16 +1,26 @@
import {StaticImageData} from 'next/image'
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
import balance from '@/components/composites/purchase/_assets/balance.svg'
export const TradeMethodDecoration = {
alipay: {
text: '支付宝',
icon: alipay,
},
wechat: {
text: '微信支付',
icon: wechat,
},
export function getTradeMethodDecoration(method: TradeMethod) {
switch (method) {
case TradeMethod.Alipay:
return {
text: '支付宝',
icon: alipay,
}
case TradeMethod.Wechat:
return {
text: '微信支付',
icon: wechat,
}
default:
return {
text: '扫码支付',
icon: balance,
}
}
}
// 支付方法枚举