From ba7d22168d4be68edb3b9634dd54ead335e5dbee Mon Sep 17 00:00:00 2001 From: Eamon-meng <17516219072@163.com> Date: Wed, 18 Jun 2025 19:05:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86=E8=B4=AD=E4=B9=B0?= =?UTF-8?q?=E5=A5=97=E9=A4=90=E9=87=8C=E7=9A=84=E5=85=85=E5=80=BC=E5=92=8C?= =?UTF-8?q?=E7=AB=8B=E5=8D=B3=E6=94=AF=E4=BB=98=E7=9A=84=E4=BC=A0=E5=8F=82?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=88=91=E7=9A=84=E8=B4=A6=E5=8D=95?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=BE=85=E6=94=AF=E4=BB=98=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E8=B7=B3=E8=BD=AC=E5=92=8C=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/resource.ts | 3 +- src/actions/user.ts | 6 +- src/app/admin/bills/page.tsx | 95 +++----- src/components/composites/payment/index.ts | 8 + .../composites/payment/payment-dialog.tsx | 157 ++++++++++++ src/components/composites/payment/payment.tsx | 228 ++++++++++++++++++ src/components/composites/purchase/pay.tsx | 30 ++- src/components/composites/recharge/index.tsx | 135 ++++++----- src/lib/models/trade.ts | 136 +++++++++++ 9 files changed, 662 insertions(+), 136 deletions(-) create mode 100644 src/components/composites/payment/index.ts create mode 100644 src/components/composites/payment/payment-dialog.tsx create mode 100644 src/components/composites/payment/payment.tsx create mode 100644 src/lib/models/trade.ts diff --git a/src/actions/resource.ts b/src/actions/resource.ts index 9befd03..8fe2a51 100644 --- a/src/actions/resource.ts +++ b/src/actions/resource.ts @@ -55,7 +55,6 @@ export async function createResource(props: { } export async function prepareResource(props: { - method: number type: number short?: { live: number @@ -71,6 +70,8 @@ export async function prepareResource(props: { expire: number daily_limit: number } + payment_method: number + payment_platform: number }) { return await callByUser<{ trade_no: string diff --git a/src/actions/user.ts b/src/actions/user.ts index 9086d80..358c1e3 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -1,13 +1,15 @@ 'use server' import {callByUser, callPublic} from '@/actions/base' -export async function RechargeByAlipay(props: { +export async function RechargeByPay(props: { amount: string + platform: number + method: number }) { return callByUser<{ trade_no: string pay_url: string - }>('/api/user/recharge/prepare/alipay', props) + }>('/api/user/recharge/prepare', props) } export async function RechargeByAlipayConfirm(props: { diff --git a/src/app/admin/bills/page.tsx b/src/app/admin/bills/page.tsx index 5622d4e..fd49bfe 100644 --- a/src/app/admin/bills/page.tsx +++ b/src/app/admin/bills/page.tsx @@ -4,7 +4,7 @@ import {PageRecord} from '@/lib/api' import {Bill} from '@/lib/models' import {useStatus} from '@/lib/states' import {listBills} from '@/actions/bill' -import {Search, Eraser, CreditCard, AlertCircle, CheckCircle, Clock, ClockIcon} from 'lucide-react' +import {Search, Eraser, CreditCard} from 'lucide-react' import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/components/ui/select' import {Button} from '@/components/ui/button' import DataTable from '@/components/data-table' @@ -16,7 +16,7 @@ import zod from 'zod' import {zodResolver} from '@hookform/resolvers/zod' import {Label} from '@/components/ui/label' import Page from '@/components/page' -import Link from 'next/link' +import {PaymentStatusCell} from '@/components/composites/payment' const filterSchema = zod.object({ type: zod.enum(['all', '3', '1', '2']).default('all'), @@ -29,10 +29,6 @@ type FilterSchema = zod.infer export type BillsPageProps = {} export default function BillsPage(props: BillsPageProps) { - // ====================== - // 查询 - // ====================== - const [status, setStatus] = useStatus() const [data, setData] = useState>({ page: 1, @@ -70,10 +66,6 @@ export default function BillsPage(props: BillsPageProps) { refresh(1, 10).then() }, []) - // ====================== - // 筛选 - // ====================== - const form = useForm({ resolver: zodResolver(filterSchema), defaultValues: { @@ -86,14 +78,8 @@ export default function BillsPage(props: BillsPageProps) { await refresh(1, data.size) } - // ====================== - // render - // ====================== - return ( - - {/* 操作区 */}
@@ -151,7 +137,6 @@ export default function BillsPage(props: BillsPageProps) {
- {/* 数据表 */} ( - <> - {row.original.trade && ( - row.original.trade.status === 0 ? ( -
-
- - 订单待支付 - - {row.original.trade.inner_no} - -
- -
- ) : row.original.trade.status === 1 ? ( -
- - 已完成 -
- ) : row.original.trade.status === 2 ? ( -
- - 已取消 -
- ) : row.original.trade.status === 3 ? ( -
- - 已退款 -
- ) : ( - - - ) - )} - + accessorKey: 'status', + header: `状态`, + cell: ({row}) => ( + ), }, { - accessorKey: 'amount', header: `支付信息`, cell: ({row}) => ( -
- - {!row.original.trade && '余额'} - {row.original.trade && row.original.trade.method === 1 && '支付宝'} - {row.original.trade && row.original.trade.method === 2 && '微信'} - - 0 ? `text-green-400` : `text-orange-400` - }> - ¥ - {row.original.amount} - -
- ), + accessorKey: 'amount', + header: '支付信息', + cell: ({row}) => { + const amount = typeof row.original.amount === 'string' + ? parseFloat(row.original.amount) + : row.original.amount || 0 + return ( +
+ + {!row.original.trade && '余额'} + {row.original.trade && row.original.trade.method === 1 && '支付宝'} + {row.original.trade && row.original.trade.method === 2 && '微信'} + + 0 ? 'text-green-400' : 'text-orange-400'}> + ¥ + {amount.toFixed(2)} + +
+ ) + }, }, { accessorKey: 'created_at', header: '创建时间', cell: ({row}) => ( diff --git a/src/components/composites/payment/index.ts b/src/components/composites/payment/index.ts new file mode 100644 index 0000000..e1ef363 --- /dev/null +++ b/src/components/composites/payment/index.ts @@ -0,0 +1,8 @@ +// BillsPage负责数据获取和列表展示 + +// PaymentStatusCell负责支付状态显示和交互 + +// PaymentDialog负责支付过程处理 +// 导出支付相关组件 +export * from './payment-dialog' +export * from './payment' diff --git a/src/components/composites/payment/payment-dialog.tsx b/src/components/composites/payment/payment-dialog.tsx new file mode 100644 index 0000000..7c39649 --- /dev/null +++ b/src/components/composites/payment/payment-dialog.tsx @@ -0,0 +1,157 @@ +'use client' +import {useEffect, useRef, useState, useMemo} from 'react' +import * as qrcode from 'qrcode' +import {Dialog, DialogContent, DialogHeader, DialogTitle} from '@/components/ui/dialog' +import {Button} from '@/components/ui/button' +import {Loader, CheckCircle} from 'lucide-react' +import {toast} from 'sonner' +import wechat from '@/components/composites/purchase/_assets/wechat.svg' +import alipay from '@/components/composites/purchase/_assets/alipay.svg' +import Image from 'next/image' +import {completeResource} from '@/actions/resource' + +export function PaymentDialog({trade, open, onOpenChange}: { + trade: { + inner_no: string + method: number + pay_url: string + amount?: number + } + open: boolean + onOpenChange: (open: boolean) => void +}) { + const [loading, setLoading] = useState(false) + const [paymentVerified, setPaymentVerified] = useState(false) + + const paymentInfo = useMemo(() => { + return trade.method === 1 ? { + icon: alipay, + name: '支付宝', + } : { + icon: wechat, + name: '微信支付', + } + }, [trade.method]) + + const canvas = useRef(null) + // 生成微信二维码 + useEffect(() => { + if (!open || !canvas.current || trade.method === 1) return + qrcode.toCanvas(canvas.current, trade.pay_url, { + width: 200, + margin: 0, + }).catch((err) => { + console.error('生成二维码失败:', err) + toast.error('生成支付二维码失败') + }) + }, [open, trade.method, trade.pay_url]) + + // 支付成功后自动关闭 + useEffect(() => { + if (paymentVerified) { + const timer = setTimeout(() => { + onOpenChange(false) + setPaymentVerified(false) + }, 2000) + return () => clearTimeout(timer) + } + }, [paymentVerified, onOpenChange]) + + const handleComplete = async () => { + setLoading(true) + try { + const resp = await completeResource({ + trade_no: trade.inner_no, + }) + if (!resp.success) { + throw new Error(resp.message) + } + toast.success('支付成功') + setPaymentVerified(true) + } + catch (e) { + toast.error('支付验证失败', { + description: (e as Error).message, + }) + } + finally { + setLoading(false) + } + } + + return ( + + { + if (canvas.current && trade.pay_url) { + qrcode.toCanvas(canvas.current, trade.pay_url, {width: 200}) + } + }}> + + + {paymentInfo.icon && ( + {`${paymentInfo.name} + )} + {paymentInfo.name} + + + +
+ {paymentVerified ? ( +
+ +

支付验证成功

+
+ ) : ( + <> +
+
+ {trade.method === 1 ? ( +