3 Commits

24 changed files with 227 additions and 206 deletions

View File

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

View File

@@ -141,7 +141,7 @@ export default function LoginCard() {
<button
type="button"
tabIndex={-1}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 cursor-pointer"
onClick={() => setShowPwd(v => !v)}
aria-label={showPwd ? '隐藏密码' : '显示密码'}
>

View File

@@ -28,7 +28,7 @@ export function HeroSection() {
</div>
<FreeTrial className={[
`mt-32 max-md:mt-20 w-96 max-md:w-full h-16 md:h-24 rounded-lg shadow-lg`,
`mt-32 max-md:mt-20 w-96 max-md:w-full h-16 md:h-24 rounded-lg shadow-lg cursor-pointer`,
`bg-linear-to-r from-blue-500 to-cyan-400 text-white text-xl lg:text-4xl`,
].join(' ')}/>
</Wrap>

View File

@@ -16,7 +16,7 @@ export default function SidebarDrawer() {
<span className="font-medium text-slate-900"></span>
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<button className="flex items-center gap-2 text-slate-600 hover:text-slate-900 p-1">
<button className="flex items-center gap-2 text-slate-600 hover:text-slate-900 p-1 cursor-pointer">
<Menu size={20}/>
<span className="text-sm"></span>
</button>

View File

@@ -225,7 +225,7 @@ function MenuItem(props: {
onPointerLeave={props.onPointerLeave}
className={[
`h-full px-4 flex gap-3 items-center cursor-pointer text-lg`,
`transition-colors duration-200 ease-in-out`,
`transition-colors duration-200 ease-in-out cursor-pointer`,
props.active
? `text-blue-500`
: ``,

View File

@@ -99,7 +99,7 @@ export default function BillsPage(props: BillsPageProps) {
<SelectItem value="all"></SelectItem>
<SelectItem value="3"></SelectItem>
<SelectItem value="1"></SelectItem>
<SelectItem value="2">退</SelectItem>
{/* <SelectItem value="2">退款</SelectItem> */}
</SelectContent>
</Select>
)}
@@ -284,13 +284,13 @@ export default function BillsPage(props: BillsPageProps) {
format(new Date(row.original.created_at), 'yyyy-MM-dd HH:mm')
),
},
{
accessorKey: 'action', header: `操作`, cell: item => (
<div className="flex gap-2">
-
</div>
),
},
// {
// accessorKey: 'action', header: `操作`, cell: item => (
// <div className="flex gap-2">
// -
// </div>
// ),
// },
]}
/>
</Page>

View File

@@ -116,7 +116,14 @@ export function Header() {
</div>
{/* right */}
<div className="flex-none flex items-center justify-end pr-4">
<div className="flex-none flex items-center justify-end pr-4 max-md:hidden gap-2">
<Link
href="/"
className={merge(
`flex-none h-16 flex items-center justify-center`,
)}>
</Link>
<Suspense>
<HeaderUserCenter/>
</Suspense>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 31 KiB

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

@@ -11,6 +11,7 @@ export const buttonVariants = cva(
'aria-invalid:ring-fail/20 dark:aria-invalid:ring-fail/40 aria-invalid:border-fail', // 无效状态样式
'inline-flex items-center justify-center gap-2', // 布局
'[&_svg]:pointer-events-none [&_svg:not([class*="size-"])]:size-4 shrink-0 [&_svg]:shrink-0 ',
'cursor-pointer',
],
{
variants: {

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,
}
}
}
// 支付方法枚举