mirror of
https://github.com/stackblitz/bolt.new
synced 2025-06-26 18:17:50 +00:00
feat: 更新支付modal和订阅对话框的逻辑和接口
This commit is contained in:
parent
68cccbb822
commit
cd3a79e954
@ -24,28 +24,42 @@ interface PaymentResponse {
|
|||||||
return_url: string;
|
return_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PaymentStatusResponse {
|
||||||
|
status: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }: PaymentModalProps) {
|
export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }: PaymentModalProps) {
|
||||||
const [timeLeft, setTimeLeft] = useState(parseInt(paymentData.expires_in));
|
const [timeLeft, setTimeLeft] = useState(parseInt(paymentData.expires_in));
|
||||||
|
|
||||||
const checkPaymentStatus = useCallback(async () => {
|
const checkPaymentStatus = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/check-payment-status?orderNo=${paymentData.no}`);
|
const response = await fetch(`/api/check-payment-status?orderNo=${paymentData.no}`);
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
console.error('Order not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json() as PaymentStatusResponse;
|
||||||
if (data.status === 'completed') {
|
if (data.status === 'completed') {
|
||||||
clearInterval(timer);
|
|
||||||
onPaymentSuccess();
|
onPaymentSuccess();
|
||||||
onClose();
|
onClose();
|
||||||
toast.success('支付成功!');
|
toast.success('支付成功!');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking payment status:', error);
|
console.error('Error checking payment status:', error);
|
||||||
|
toast.error('检查支付状态时出错,请稍后再试');
|
||||||
}
|
}
|
||||||
}, [paymentData.no, onPaymentSuccess, onClose]);
|
}, [paymentData.no, onPaymentSuccess, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const checkAndUpdateStatus = () => {
|
||||||
setTimeLeft((prevTime) => {
|
setTimeLeft((prevTime) => {
|
||||||
if (prevTime <= 1) {
|
if (prevTime <= 1) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
@ -57,7 +71,9 @@ export function PaymentModal({ isOpen, onClose, paymentData, onPaymentSuccess }:
|
|||||||
});
|
});
|
||||||
|
|
||||||
checkPaymentStatus();
|
checkPaymentStatus();
|
||||||
}, 3000); // 每3秒检查一次支付状态
|
};
|
||||||
|
|
||||||
|
timer = setInterval(checkAndUpdateStatus, 3000); // 每3秒检查一次支付状态
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [isOpen, onClose, checkPaymentStatus]);
|
}, [isOpen, onClose, checkPaymentStatus]);
|
||||||
|
|||||||
@ -15,9 +15,18 @@ interface SubscriptionDialogProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface UserSubscription {
|
interface UserSubscription {
|
||||||
plan: SubscriptionPlan;
|
tokenBalance: number;
|
||||||
tokensLeft: number;
|
subscription: {
|
||||||
nextReloadDate: string;
|
plan: {
|
||||||
|
_id: string;
|
||||||
|
name: string;
|
||||||
|
tokens: number;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
save_percentage?: number;
|
||||||
|
};
|
||||||
|
expirationDate: string;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PaymentResponse {
|
interface PaymentResponse {
|
||||||
@ -164,14 +173,14 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isAuthenticated && userSubscription && (
|
{isAuthenticated && userSubscription && userSubscription.subscription && (
|
||||||
<div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
|
<div className="bg-bolt-elements-background-depth-2 p-4 rounded-lg">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-bolt-elements-textPrimary font-bold">{toString(userSubscription.tokensLeft)}</span>
|
<span className="text-bolt-elements-textPrimary font-bold">{toString(userSubscription.tokenBalance)}</span>
|
||||||
<span className="text-bolt-elements-textSecondary"> 代币剩余。</span>
|
<span className="text-bolt-elements-textSecondary"> 代币剩余。</span>
|
||||||
<span className="text-bolt-elements-textSecondary">
|
<span className="text-bolt-elements-textSecondary">
|
||||||
{userSubscription.plan.tokens.toLocaleString()}代币将在{new Date(userSubscription.nextReloadDate).toLocaleDateString()}后添加。
|
{userSubscription.subscription.plan.tokens.toLocaleString()}代币将在{new Date(userSubscription.subscription.expirationDate).toLocaleDateString()}后添加。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
@ -220,7 +229,7 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{subscriptionPlans.map((plan) => (
|
{subscriptionPlans.map((plan) => (
|
||||||
<div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${isAuthenticated && plan._id === userSubscription?.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}>
|
<div key={plan._id} className={`bg-bolt-elements-background-depth-2 p-4 rounded-lg ${isAuthenticated && userSubscription && userSubscription.subscription && plan._id === userSubscription.subscription.plan._id ? 'border-2 border-bolt-elements-item-contentAccent' : ''}`}>
|
||||||
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3>
|
<h3 className="text-bolt-elements-textPrimary font-bold text-lg">{plan.name}</h3>
|
||||||
<div className="text-bolt-elements-textSecondary mb-2">
|
<div className="text-bolt-elements-textSecondary mb-2">
|
||||||
{(plan.tokens / 1000000).toFixed(0)}M 代币
|
{(plan.tokens / 1000000).toFixed(0)}M 代币
|
||||||
@ -235,12 +244,12 @@ export function SubscriptionDialog({ isOpen, onClose }: SubscriptionDialogProps)
|
|||||||
<button
|
<button
|
||||||
onClick={() => handlePurchase(plan._id)}
|
onClick={() => handlePurchase(plan._id)}
|
||||||
className={`w-full py-2 rounded-md ${
|
className={`w-full py-2 rounded-md ${
|
||||||
isAuthenticated && plan._id === userSubscription?.plan._id
|
isAuthenticated && userSubscription && userSubscription.subscription && plan._id === userSubscription.subscription.plan._id
|
||||||
? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
|
? 'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text'
|
||||||
: 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
|
: 'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isAuthenticated && plan._id === userSubscription?.plan._id ? '管理当前计划' : `升级到${plan.name}`}
|
{isAuthenticated && userSubscription && userSubscription.subscription && plan._id === userSubscription.subscription.plan._id ? '管理当前计划' : `升级到${plan.name}`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,26 +1,35 @@
|
|||||||
import { json } from '@remix-run/cloudflare';
|
import { json } from '@remix-run/cloudflare';
|
||||||
import { db } from '~/utils/db.server';
|
import { db } from '~/utils/db.server';
|
||||||
|
import { requireAuth } from '~/middleware/auth.server';
|
||||||
|
|
||||||
export async function loader({ request }: { request: Request }) {
|
export async function loader({ request }: { request: Request }) {
|
||||||
|
let userId;
|
||||||
|
try {
|
||||||
|
userId = await requireAuth(request);
|
||||||
|
} catch (error) {
|
||||||
|
return error as Response;
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const orderNo = url.searchParams.get('orderNo');
|
const orderNo = url.searchParams.get('orderNo');
|
||||||
|
|
||||||
if (!orderNo) {
|
if (!orderNo) {
|
||||||
return json({ error: 'Order number is required' }, { status: 400 });
|
return json({ error: '订单号不能为空' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transaction = await db('user_transactions')
|
const transaction = await db('user_transactions')
|
||||||
.where('transaction_id', orderNo)
|
.where('transaction_id', orderNo)
|
||||||
|
.where('user_id', userId)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
return json({ error: 'Transaction not found' }, { status: 404 });
|
return json({ error: '订单不存在' }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return json({ status: transaction.status });
|
return json({ status: transaction.status });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking payment status:', error);
|
console.error('Error checking payment status:', error);
|
||||||
return json({ error: 'Failed to check payment status' }, { status: 500 });
|
return json({ error: '检查支付状态失败' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export async function action({ request }: { request: Request }) {
|
|||||||
orderNo,
|
orderNo,
|
||||||
`${plan.name} 订阅 (${billingCycle === 'yearly' ? '年付' : '月付'})`,
|
`${plan.name} 订阅 (${billingCycle === 'yearly' ? '年付' : '月付'})`,
|
||||||
'alipay', // 或其他支付方式
|
'alipay', // 或其他支付方式
|
||||||
price * 100, // 转换为分
|
price, // 不用转换为分
|
||||||
userId.toString()
|
userId.toString()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -30,9 +30,16 @@ export async function loader({ request }: { request: Request }) {
|
|||||||
|
|
||||||
return json({
|
return json({
|
||||||
tokenBalance: user.token_balance,
|
tokenBalance: user.token_balance,
|
||||||
subscription: subscription
|
subscription: subscription && subscriptionPlan
|
||||||
? {
|
? {
|
||||||
planName: subscriptionPlan.name,
|
plan: {
|
||||||
|
_id: subscriptionPlan._id,
|
||||||
|
name: subscriptionPlan.name,
|
||||||
|
tokens: subscriptionPlan.tokens,
|
||||||
|
price: subscriptionPlan.price,
|
||||||
|
description: subscriptionPlan.description,
|
||||||
|
save_percentage: subscriptionPlan.save_percentage,
|
||||||
|
},
|
||||||
expirationDate: subscription.expiration_date,
|
expirationDate: subscription.expiration_date,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user