diff --git a/app/components/auth/SubscriptionDialog.tsx b/app/components/auth/SubscriptionDialog.tsx index 380068b..00c9f54 100644 --- a/app/components/auth/SubscriptionDialog.tsx +++ b/app/components/auth/SubscriptionDialog.tsx @@ -7,6 +7,7 @@ import { LoginRegisterDialog } from './LoginRegisterDialog'; import type { SubscriptionPlan } from '~/types/subscription'; import pkg from 'lodash'; const {toString} = pkg; +import { TokenReloadModal } from './TokenReloadModal'; // 新增导入 interface SubscriptionDialogProps { isOpen: boolean; @@ -48,6 +49,7 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) const { user, token, isAuthenticated, login } = useAuth(); const [paymentData, setPaymentData] = useState(null); const [isLoginRegisterOpen, setIsLoginRegisterOpen] = useState(false); + const [isTokenReloadModalOpen, setIsTokenReloadModalOpen] = useState(false); useEffect(() => { if (isOpen) { @@ -138,6 +140,15 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) toast.success('登录成功!'); }, [fetchUserSubscription]); + const handleTokenReloadClick = () => { + setIsTokenReloadModalOpen(true); + }; + + const handleTokenReloadSuccess = useCallback(() => { + fetchUserSubscription(); // 重新获取用户订阅信息 + toast.success('代币充值成功!'); + }, [fetchUserSubscription]); + if (isLoading) return null; return ( @@ -167,8 +178,17 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) 需要更多代币?
- 升级您的计划或购买 - 代币充值包 + 升级您的计划或 + { + e.preventDefault(); + handleTokenReloadClick(); + }} + > + 购买代币充值包 + @@ -242,6 +262,11 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps) onClose={() => setIsLoginRegisterOpen(false)} onLoginSuccess={handleLoginSuccess} /> + setIsTokenReloadModalOpen(false)} + onReloadSuccess={handleTokenReloadSuccess} + /> ); } diff --git a/app/components/auth/TokenReloadModal.tsx b/app/components/auth/TokenReloadModal.tsx new file mode 100644 index 0000000..088a423 --- /dev/null +++ b/app/components/auth/TokenReloadModal.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import { Dialog, DialogTitle, DialogDescription, DialogRoot } from '~/components/ui/Dialog'; +import { useAuth } from '~/hooks/useAuth'; +import { toast } from 'react-toastify'; +import type { PurchaseResponse } from '~/routes/api.purchase-token-reload'; + +interface TokenReloadModalProps { + isOpen: boolean; + onClose: () => void; + onReloadSuccess: () => void; +} + +interface TokenReloadPack { + _id: string; + name: string; + tokens: number; + price: number; + description: string; +} + + + +export function TokenReloadModal({ isOpen, onClose, onReloadSuccess }: TokenReloadModalProps) { + const [tokenReloadPacks, setTokenReloadPacks] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const { token } = useAuth(); + + useEffect(() => { + if (isOpen) { + fetchTokenReloadPacks(); + } + }, [isOpen]); + + const fetchTokenReloadPacks = async () => { + setIsLoading(true); + try { + const response = await fetch('/api/token-reload-packs'); + if (!response.ok) { + throw new Error('获取代币充值包失败'); + } + const data = await response.json() as TokenReloadPack[]; + setTokenReloadPacks(data); + } catch (error) { + console.error('获取代币充值包时出错:', error); + toast.error('获取代币充值包失败,请稍后再试'); + } finally { + setIsLoading(false); + } + }; + + const handlePurchase = async (packId: string) => { + if (!token) { + toast.error('登录状态异常,请重新登录'); + return; + } + try { + const response = await fetch('/api/purchase-token-reload', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ packId }), + }); + const result = await response.json() as PurchaseResponse; + if (response.ok && result.success) { + onReloadSuccess(); + onClose(); + } else { + toast.error(result.error || '购买代币充值包失败,请稍后重试'); + } + } catch (error) { + console.error('Error purchasing token reload pack:', error); + toast.error('购买代币充值包过程中出现错误,请稍后重试'); + } + }; + + if (isLoading) return null; + + return ( + + + 购买代币充值包 + +
+ {tokenReloadPacks.map((pack) => ( +
+

{pack.name}

+
+ {(pack.tokens / 1000000).toFixed(0)}M 代币 +
+

{pack.description}

+
+ ¥{pack.price.toFixed(2)} +
+ +
+ ))} +
+
+
+
+ ); +} diff --git a/app/routes/api.purchase-token-reload.ts b/app/routes/api.purchase-token-reload.ts new file mode 100644 index 0000000..604a729 --- /dev/null +++ b/app/routes/api.purchase-token-reload.ts @@ -0,0 +1,69 @@ +import { json } from '@remix-run/cloudflare'; +import { db } from '~/utils/db.server'; +import SDPay from '~/utils/SDPay.server'; +import { requireAuth } from '~/middleware/auth.server'; + +export interface PurchaseResponse { + success: boolean; + paymentData?: { + // 根据实际返回的数据结构定义 + no: string; + pay_type: string; + order_amount: string; + qr_img: string; + expires_in: string; + // ... 其他可能的字段 + }; + error?: string; + } + +export async function action({ request }: { request: Request }) { + let userId; + try { + userId = await requireAuth(request); + } catch (error) { + return error as Response; + } + + const { packId } = await request.json() as { packId: string }; + + try { + // 获取代币充值包详情 + const pack = await db('token_reloads').where('_id', packId).first(); + if (!pack) { + return json({ error: '无效的代币充值包' }, { status: 400 }); + } + + // 创建 SDPay 实例 + const sdpay = new SDPay(); + + // 生成订单号 + const orderNo = `reload_${Date.now()}_${userId}`; + + // 获取支付数据 + const paymentData = await sdpay.createPayment( + orderNo, + `${pack.name} 代币充值`, + 'alipay', // 或其他支付方式 + pack.price * 100, // 转换为分 + userId.toString() + ); + + // 创建待处理的交易记录 + await db('user_transactions').insert({ + user_id: userId, + type: 'token_reload', + token_reload_id: packId, + amount: pack.price, + tokens: pack.tokens, + status: 'pending', + payment_method: 'alipay', // 或其他支付方式 + transaction_id: orderNo, + }); + + return json({ success: true, paymentData }); + } catch (error) { + console.error('初始化代币充值购买时出错:', error); + return json({ error: '初始化代币充值购买失败' }, { status: 500 }); + } +} \ No newline at end of file