支付组件统一使用二维码展示

This commit is contained in:
2026-03-13 14:13:06 +08:00
parent 82bd8051d8
commit 2b77ea189b
16 changed files with 206 additions and 193 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,6 @@
'use client' 'use client'
import {Suspense, use, useContext, useEffect, useMemo, useState} from 'react' import {Suspense, use, useEffect, 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 {useProfileStore} from '@/components/stores/profile' import {useProfileStore} from '@/components/stores/profile'
import RechargeModal from '@/components/composites/recharge'
import Pay from '@/components/composites/purchase/pay' import Pay from '@/components/composites/purchase/pay'
import {buttonVariants} from '@/components/ui/button' import {buttonVariants} from '@/components/ui/button'
import Link from 'next/link' import Link from 'next/link'
@@ -17,8 +8,9 @@ import {merge} from '@/lib/utils'
import {useFormContext, useWatch} from 'react-hook-form' import {useFormContext, useWatch} from 'react-hook-form'
import {Schema} from '@/components/composites/purchase/long/form' import {Schema} from '@/components/composites/purchase/long/form'
import {Card} from '@/components/ui/card' import {Card} from '@/components/ui/card'
import {getPrice, CreateResourceReq} from '@/actions/resource' import {getPrice} from '@/actions/resource'
import {ExtraResp} from '@/lib/api' import {ExtraResp} from '@/lib/api'
import {FieldPayment} from '../shared/field-payment'
export default function Right() { export default function Right() {
const {control} = useFormContext<Schema>() const {control} = useFormContext<Schema>()
@@ -164,52 +156,7 @@ function BalanceOrLogin(props: {
const profile = use(useProfileStore(store => store.profile)) const profile = use(useProfileStore(store => store.profile))
return profile ? ( return profile ? (
<> <>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6"> {/* <FieldPayment/> */}
{({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>
<Pay <Pay
method={props.method} method={props.method}
balance={profile.balance} balance={profile.balance}

View File

@@ -11,7 +11,7 @@ import {useRouter} from 'next/navigation'
import {completeResource, createResource, CreateResourceReq, prepareResource} from '@/actions/resource' import {completeResource, createResource, CreateResourceReq, prepareResource} from '@/actions/resource'
import { import {
TradeMethod, TradeMethod,
TradeMethodDecoration, TradePlatform,
} from '@/lib/models/trade' } from '@/lib/models/trade'
import {PaymentModal} from '@/components/composites/payment/payment-modal' import {PaymentModal} from '@/components/composites/payment/payment-modal'
import {PaymentProps} from '@/components/composites/payment/type' import {PaymentProps} from '@/components/composites/payment/type'
@@ -32,7 +32,7 @@ export default function Pay(props: PayProps) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [trade, setTrade] = useState<PaymentProps | null>(null) const [trade, setTrade] = useState<PaymentProps | null>(null)
const router = useRouter() const router = useRouter()
const platform = usePlatformType() // const platform = usePlatformType()
const onOpen = async () => { const onOpen = async () => {
setOpen(true) setOpen(true)
@@ -45,7 +45,7 @@ export default function Pay(props: PayProps) {
const req = { const req = {
...props.resource, ...props.resource,
payment_method: method, payment_method: method,
payment_platform: platform, payment_platform: TradePlatform.Desktop,
} }
const resp = await prepareResource(req) const resp = await prepareResource(req)
@@ -60,9 +60,8 @@ export default function Pay(props: PayProps) {
inner_no: resp.data.trade_no, inner_no: resp.data.trade_no,
pay_url: resp.data.pay_url, pay_url: resp.data.pay_url,
amount: Number(props.amount), amount: Number(props.amount),
platform: platform, platform: TradePlatform.Desktop,
method: method, 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' 'use client'
import {createContext} from 'react' import {useForm} from 'react-hook-form'
import {useForm, UseFormReturn} from 'react-hook-form'
import Center from '@/components/composites/purchase/short/center' import Center from '@/components/composites/purchase/short/center'
import Right from '@/components/composites/purchase/short/right' import Right from '@/components/composites/purchase/short/right'
import {Form} from '@/components/ui/form' import {Form} from '@/components/ui/form'
@@ -20,16 +19,7 @@ const schema = z.object({
// 从架构中推断类型 // 从架构中推断类型
export type Schema = z.infer<typeof schema> export type Schema = z.infer<typeof schema>
type PurchaseFormContextType = { export default function ShortForm() {
form: UseFormReturn<Schema>
onSubmit?: () => void
}
export const PurchaseFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
export type PurchaseFormProps = {}
export default function PurchaseForm(props: PurchaseFormProps) {
const form = useForm<Schema>({ const form = useForm<Schema>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { defaultValues: {
@@ -38,7 +28,7 @@ export default function PurchaseForm(props: PurchaseFormProps) {
quota: 10_000, // >= 10000 quota: 10_000, // >= 10000
expire: '30', // 天 expire: '30', // 天
daily_limit: 2_000, // >= 2000 daily_limit: 2_000, // >= 2000
pay_type: 'balance', // 余额支付 pay_type: 'wechat', // 余额支付
}, },
}) })

View File

@@ -1,23 +1,16 @@
'use client' '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 {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 {useProfileStore} from '@/components/stores/profile'
import RechargeModal from '@/components/composites/recharge'
import {buttonVariants} from '@/components/ui/button' import {buttonVariants} from '@/components/ui/button'
import Link from 'next/link' import Link from 'next/link'
import {merge} from '@/lib/utils' import {merge} from '@/lib/utils'
import Pay from '@/components/composites/purchase/pay' import Pay from '@/components/composites/purchase/pay'
import {useFormContext, useWatch} from 'react-hook-form' import {useFormContext, useWatch} from 'react-hook-form'
import {Card} from '@/components/ui/card' import {Card} from '@/components/ui/card'
import {CreateResourceReq, getPrice} from '@/actions/resource' import {getPrice} from '@/actions/resource'
import {ExtraResp} from '@/lib/api' import {ExtraResp} from '@/lib/api'
import {FieldPayment} from '../shared/field-payment'
export default function Right() { export default function Right() {
const {control} = useFormContext<Schema>() const {control} = useFormContext<Schema>()
@@ -165,52 +158,7 @@ function BalanceOrLogin(props: {
const profile = use(useProfileStore(store => store.profile)) const profile = use(useProfileStore(store => store.profile))
return profile ? ( return profile ? (
<> <>
<FormField name="pay_type" label="支付方式" className="flex flex-col gap-6"> {/* <FieldPayment/> */}
{({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>
<Pay <Pay
method={props.method} method={props.method}
balance={profile.balance} balance={profile.balance}

View File

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

View File

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

View File

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