|
|
@@ -1,6 +1,6 @@
|
|
|
'use client';
|
|
|
|
|
|
-import { useEffect, useState } from 'react';
|
|
|
+import { useEffect, useMemo, useState } from 'react';
|
|
|
import api from '@/lib/api';
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
import {
|
|
|
@@ -69,6 +69,14 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
|
|
|
const [providers, setProviders] = useState<PaymentProvider[]>([]);
|
|
|
const [paymentData, setPaymentData] = useState<PaymentResult | null>(null);
|
|
|
const [qrCode, setQrCode] = useState<string>('');
|
|
|
+ const [isSlotSubscription, setIsSlotSubscription] = useState(false);
|
|
|
+ const availableProviders = useMemo(() => {
|
|
|
+ if (!isSlotSubscription) return providers;
|
|
|
+ return providers.filter((provider) => {
|
|
|
+ const target = `${provider.name || ''} ${provider.title || ''}`.toLowerCase();
|
|
|
+ return target.includes('stripe') || target.includes('card');
|
|
|
+ });
|
|
|
+ }, [providers, isSlotSubscription]);
|
|
|
|
|
|
// 消息弹窗状态
|
|
|
const [msgModal, setMsgModal] = useState({
|
|
|
@@ -97,6 +105,7 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
|
|
|
|
|
|
useEffect(() => {
|
|
|
fetchProviders();
|
|
|
+ fetchOrderInfo();
|
|
|
}, []);
|
|
|
|
|
|
const fetchProviders = async () => {
|
|
|
@@ -109,6 +118,18 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const fetchOrderInfo = async () => {
|
|
|
+ try {
|
|
|
+ const res = await api.get('/api/vas/order/detail', { params: { order_id: orderId } });
|
|
|
+ const data = res.data.data || res.data;
|
|
|
+ const hasSlotRouting = Boolean(data?.user_inputs?.slot_routing_key);
|
|
|
+ const isProductMatch = data?.product_id === 16;
|
|
|
+ setIsSlotSubscription(hasSlotRouting || isProductMatch);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Failed to load order info', error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
// 发起支付 (Step 1 -> Step 2)
|
|
|
const handlePay = async (providerName: string) => {
|
|
|
setLoading(true);
|
|
|
@@ -117,6 +138,11 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
|
|
|
if (providerCode.includes('wechat')) providerCode = 'wechat';
|
|
|
else if (providerCode.includes('ali')) providerCode = 'alipay';
|
|
|
else if (providerCode.includes('stripe') || providerCode.includes('card')) providerCode = 'stripe';
|
|
|
+ if (isSlotSubscription && providerCode !== 'stripe') {
|
|
|
+ showMessage(t('payment.subscription_only_stripe_hint'), 'info');
|
|
|
+ setLoading(false);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
// 1. 创建支付单
|
|
|
const payRes = await api.post('/api/vas/payment/create', {
|
|
|
@@ -211,189 +237,159 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <div className="bg-white p-6 md:p-8 rounded-xl shadow-sm border text-center max-w-2xl mx-auto min-h-[400px] flex flex-col justify-center relative">
|
|
|
+ <div className="bg-white p-6 md:p-8 rounded-2xl shadow-2xl border border-slate-100 text-center max-w-2xl mx-auto min-h-[400px]">
|
|
|
|
|
|
{/* === Step 1: 选择支付方式 === */}
|
|
|
{step === 1 && (
|
|
|
- <>
|
|
|
- <h2 className="text-xl md:text-2xl font-bold mb-2 text-gray-900">{t('payment.order_created')}</h2>
|
|
|
- <p className="text-gray-500 mb-6 md:mb-8 text-sm break-all">{t('payment.order_id')}: <span className="font-mono font-bold text-gray-700">{orderId}</span></p>
|
|
|
-
|
|
|
- <h3 className="text-left font-semibold mb-4 text-gray-800">{t('payment.select_method')}</h3>
|
|
|
-
|
|
|
- {providers.length === 0 ? (
|
|
|
- <div className="text-gray-400 py-4 text-sm">{t('common.loading')}</div>
|
|
|
+ <div className="space-y-6 text-left">
|
|
|
+ <div className="rounded-2xl border border-slate-100 bg-white p-6 shadow-md">
|
|
|
+ <p className="text-xs uppercase tracking-[0.4em] text-slate-500">{t('payment.order_created')}</p>
|
|
|
+ <div className="mt-2 text-2xl lg:text-3xl font-bold flex items-baseline gap-2">
|
|
|
+ <span>{t('payment.order_id')}</span>
|
|
|
+ <span className="font-mono text-blue-600 text-lg lg:text-xl">{orderId}</span>
|
|
|
+ </div>
|
|
|
+ <p className="mt-3 text-sm leading-relaxed text-slate-500">
|
|
|
+ {t('payment.select_method_subtitle')}
|
|
|
+ </p>
|
|
|
+ {isSlotSubscription && (
|
|
|
+ <div className="mt-4 inline-flex items-center gap-1 rounded-full border border-slate-200 px-3 py-1 text-xs text-slate-500 bg-slate-50">
|
|
|
+ <Sparkles size={14} className="text-blue-500" />
|
|
|
+ {t('payment.subscription_only_stripe_hint')}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {availableProviders.length === 0 ? (
|
|
|
+ <div className="text-gray-400 py-4 text-sm text-center">{t('common.loading')}</div>
|
|
|
) : (
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
- {providers.map((p, idx) => (
|
|
|
+ {availableProviders.map((p, idx) => (
|
|
|
<button
|
|
|
key={idx}
|
|
|
onClick={() => handlePay(p.name)}
|
|
|
disabled={loading}
|
|
|
- className="group flex flex-col items-center justify-center p-4 py-5 md:py-6 border rounded-xl hover:bg-blue-50 hover:border-blue-200 transition bg-white h-auto active:scale-95 shadow-sm"
|
|
|
+ className="group relative flex flex-col items-center justify-center gap-3 rounded-2xl border border-slate-200 bg-white p-5 hover:border-blue-300 hover:shadow-[0_15px_45px_rgba(14,165,233,0.15)] transition-all active:scale-[0.98]"
|
|
|
>
|
|
|
{p.icon ? (
|
|
|
<img
|
|
|
src={p.icon}
|
|
|
alt={p.name}
|
|
|
- className="h-12 w-full object-contain mb-2 md:h-16 md:mb-3 group-hover:scale-105 transition-transform"
|
|
|
+ className="h-12 w-full object-contain mb-2 md:h-16 md:mb-3 transition-transform duration-200"
|
|
|
/>
|
|
|
) : (
|
|
|
- <div className="h-12 w-12 md:h-16 md:w-16 bg-gray-100 rounded-full mb-2 md:mb-3 flex items-center justify-center text-gray-400 text-xs font-bold">
|
|
|
- {p.name.substring(0, 3)}
|
|
|
+ <div className="flex h-14 w-14 items-center justify-center rounded-full border border-slate-200 bg-slate-50 text-xs font-bold text-slate-500">
|
|
|
+ {p.name.substring(0, 3).toUpperCase()}
|
|
|
</div>
|
|
|
)}
|
|
|
- <span className="font-medium text-gray-600 text-sm group-hover:text-blue-700">{p.title || p.name}</span>
|
|
|
- {p.currency && <span className="text-xs text-gray-400 mt-1">({p.currency})</span>}
|
|
|
+ <span className="font-semibold text-slate-800 text-base md:text-lg">{p.title || p.name}</span>
|
|
|
+ {p.currency && <span className="text-xs text-slate-400">{p.currency}</span>}
|
|
|
</button>
|
|
|
))}
|
|
|
</div>
|
|
|
)}
|
|
|
- {loading && <div className="mt-6 flex justify-center text-blue-600 text-sm items-center"><Loader2 className="animate-spin w-4 h-4 mr-2"/>{t('payment.creating')}</div>}
|
|
|
- </>
|
|
|
+ {loading && <div className="mt-4 text-blue-600 text-sm flex items-center justify-center gap-2"><Loader2 className="animate-spin w-4 h-4"/>{t('payment.creating')}</div>}
|
|
|
+ </div>
|
|
|
)}
|
|
|
|
|
|
{/* === Step 2: 支付详情 & 确认 === */}
|
|
|
{step === 2 && paymentData && (
|
|
|
- <div className="animate-in fade-in zoom-in duration-300 text-left">
|
|
|
-
|
|
|
- {/* 顶部导航 */}
|
|
|
- <div className="flex items-center justify-between mb-6">
|
|
|
+ <div className="space-y-6 text-left">
|
|
|
+ <div className="flex items-center justify-between bg-white rounded-2xl border border-slate-100 p-4 shadow-sm">
|
|
|
<button
|
|
|
onClick={handleBack}
|
|
|
- className="text-gray-400 hover:text-gray-600 flex items-center text-sm transition active:scale-95 py-2 pr-2"
|
|
|
+ className="text-slate-500 hover:text-slate-800 flex items-center gap-2 text-sm font-medium transition"
|
|
|
>
|
|
|
- <ArrowLeft className="w-4 h-4 mr-1" /> {t('payment.reselect')}
|
|
|
+ <ArrowLeft className="w-4 h-4" /> {t('payment.reselect')}
|
|
|
</button>
|
|
|
- <div className="flex items-center text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded whitespace-nowrap">
|
|
|
- <Clock className="w-3 h-3 mr-1" />
|
|
|
- <LocalTime
|
|
|
- date={paymentData.expire_at}
|
|
|
- options={{ hour: '2-digit', minute: '2-digit' }}
|
|
|
- />
|
|
|
- <span className="ml-1">{t('payment.expires')}</span>
|
|
|
+ <div className="flex items-center gap-2 text-xs text-blue-600 bg-blue-50 px-3 py-1 rounded-full">
|
|
|
+ <Clock className="w-3 h-3" />
|
|
|
+ <LocalTime date={paymentData.expire_at} options={{ hour: '2-digit', minute: '2-digit' }} />
|
|
|
+ <span>{t('payment.expires')}</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <h2 className="text-lg md:text-xl font-bold mb-4 text-gray-900 text-center">{t('payment.confirm_info')}</h2>
|
|
|
-
|
|
|
- {/* Amount Card */}
|
|
|
- <div className="bg-slate-50 rounded-xl p-4 md:p-5 mb-6 border border-slate-100 space-y-3">
|
|
|
-
|
|
|
- {/* 1. 原始金额 */}
|
|
|
- <div className="flex justify-between text-sm text-gray-600">
|
|
|
- <span>{t('payment.original_amount')}</span>
|
|
|
- <span className="font-medium text-gray-900">{formatMoney(paymentData.base_amount, paymentData.base_currency)}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 2. 汇率 */}
|
|
|
- {paymentData.currency !== paymentData.base_currency && (
|
|
|
- <div className="flex justify-between text-xs text-gray-400 items-center">
|
|
|
- <span className="flex items-center gap-1"><ArrowRightLeft size={12}/> {t('payment.exchange_rate')}</span>
|
|
|
- <span>1 {paymentData.base_currency} ≈ {paymentData.exchange_rate} {paymentData.currency}</span>
|
|
|
- </div>
|
|
|
- )}
|
|
|
+ <h2 className="text-lg md:text-xl font-bold text-slate-900">{t('payment.confirm_info')}</h2>
|
|
|
|
|
|
- {/* 4. 人工调价 (New) */}
|
|
|
- {paymentData.adjustment_delta && paymentData.adjustment_delta !== 0 ? (
|
|
|
- <div className={`flex justify-between text-sm font-medium items-center p-2 rounded-lg border
|
|
|
- ${paymentData.adjustment_delta < 0
|
|
|
- ? 'bg-green-50 text-green-700 border-green-100' // 降价
|
|
|
- : 'bg-amber-50 text-amber-700 border-amber-100' // 加价
|
|
|
- }`}>
|
|
|
- <span className="flex items-center gap-1">
|
|
|
- {paymentData.adjustment_delta < 0 ? <TrendingDown size={14}/> : <TrendingUp size={14}/>}
|
|
|
- {t('payment.manual_adjustment')}
|
|
|
- </span>
|
|
|
- <span>
|
|
|
- {paymentData.adjustment_delta > 0 ? '+' : ''}
|
|
|
- {formatMoney(paymentData.adjustment_delta, paymentData.base_currency)}
|
|
|
- </span>
|
|
|
+ <div className="grid gap-6 lg:grid-cols-[1.1fr_0.9fr]">
|
|
|
+ <div className="bg-white rounded-3xl border border-slate-100 shadow-sm p-6 space-y-4">
|
|
|
+ <div className="flex justify-between items-center text-sm text-slate-500">
|
|
|
+ <span>{t('payment.original_amount')}</span>
|
|
|
+ <span className="font-medium text-slate-900">{formatMoney(paymentData.base_amount, paymentData.base_currency)}</span>
|
|
|
</div>
|
|
|
- ) : null}
|
|
|
-
|
|
|
- {/* 3. 随机立减 */}
|
|
|
- {paymentData.random_offset !== 0 && (
|
|
|
- <div className="flex justify-between text-sm text-red-500 font-medium items-center bg-red-50 p-2 rounded-lg border border-red-100">
|
|
|
- <span className="flex items-center gap-1">
|
|
|
- <Sparkles size={14} className="fill-red-500" /> {t('payment.random_discount')}
|
|
|
- </span>
|
|
|
- <span>
|
|
|
- -{formatMoney(paymentData.random_offset, paymentData.currency)}
|
|
|
- </span>
|
|
|
+ {paymentData.currency !== paymentData.base_currency && (
|
|
|
+ <div className="flex justify-between text-xs text-slate-400">
|
|
|
+ <span>{t('payment.exchange_rate')}</span>
|
|
|
+ <span>1 {paymentData.base_currency} ≈ {paymentData.exchange_rate} {paymentData.currency}</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {paymentData.adjustment_delta !== undefined && paymentData.adjustment_delta !== 0 && (
|
|
|
+ <div className={`flex justify-between text-sm items-center rounded-xl border px-4 py-2 ${paymentData.adjustment_delta < 0 ? 'border-green-100 bg-green-50 text-green-700' : 'border-amber-100 bg-amber-50 text-amber-700'}`}>
|
|
|
+ <span className="flex items-center gap-1">
|
|
|
+ {paymentData.adjustment_delta < 0 ? <TrendingDown size={14} /> : <TrendingUp size={14} />}
|
|
|
+ {t('payment.manual_adjustment')}
|
|
|
+ </span>
|
|
|
+ <span>
|
|
|
+ {paymentData.adjustment_delta > 0 ? '+' : ''}{formatMoney(paymentData.adjustment_delta, paymentData.base_currency)}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {paymentData.random_offset !== 0 && (
|
|
|
+ <div className="flex justify-between text-sm text-red-600 bg-red-50 rounded-xl px-4 py-2 border border-red-100">
|
|
|
+ <span className="flex items-center gap-1">
|
|
|
+ <Sparkles size={14} className="text-red-500" /> {t('payment.random_discount')}
|
|
|
+ </span>
|
|
|
+ <span>-{formatMoney(paymentData.random_offset, paymentData.currency)}</span>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className="border-t border-slate-200 pt-3 text-lg font-bold flex justify-between items-center">
|
|
|
+ <span>{t('payment.actual_pay')}</span>
|
|
|
+ <span className="text-blue-600 text-2xl md:text-3xl">{formatMoney(paymentData.amount, paymentData.currency)}</span>
|
|
|
</div>
|
|
|
- )}
|
|
|
-
|
|
|
- <div className="border-t border-slate-200 my-2"></div>
|
|
|
-
|
|
|
- {/* 5. 实际需付 */}
|
|
|
- <div className="flex justify-between items-end">
|
|
|
- <span className="text-gray-600 font-medium pb-1">{t('payment.actual_pay')}</span>
|
|
|
- <span className="text-2xl md:text-3xl font-bold text-blue-600">
|
|
|
- {formatMoney(paymentData.amount, paymentData.currency)}
|
|
|
- </span>
|
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
|
- {/* Action Area */}
|
|
|
- <div className="text-center">
|
|
|
-
|
|
|
- {/* Link Payment */}
|
|
|
- {paymentData.channel === 'online_link' && (
|
|
|
- <div className="space-y-4 mb-8">
|
|
|
- <p className="text-sm text-gray-500">{t('payment.link_pay_hint')}</p>
|
|
|
- <a
|
|
|
- href={paymentData.payment_url}
|
|
|
- target="_blank"
|
|
|
- rel="noopener noreferrer"
|
|
|
- className="w-full flex items-center justify-center gap-2 px-6 py-3.5 bg-blue-600 text-white rounded-xl hover:bg-blue-700 font-bold transition shadow-lg shadow-blue-200 active:scale-95"
|
|
|
- >
|
|
|
- {t('payment.go_to_pay')} <ExternalLink size={18} />
|
|
|
- </a>
|
|
|
- </div>
|
|
|
- )}
|
|
|
-
|
|
|
- {/* QR Payment */}
|
|
|
- {paymentData.channel === 'qr_static' && (
|
|
|
- <div className="space-y-4 mb-8">
|
|
|
- <p className="text-sm text-gray-500">{t('payment.qr_pay_hint')}</p>
|
|
|
- <div className="bg-white p-4 inline-block rounded-xl border shadow-sm ring-4 ring-slate-50 max-w-full">
|
|
|
- {qrCode ? (
|
|
|
- <img
|
|
|
- src={qrCode.startsWith('http') || qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`}
|
|
|
- alt="Payment QR"
|
|
|
- className="w-48 h-48 md:w-52 md:h-52 object-contain bg-white rounded-lg"
|
|
|
- />
|
|
|
- ) : (
|
|
|
- <div className="w-48 h-48 flex items-center justify-center text-gray-400">{t('common.loading')}</div>
|
|
|
- )}
|
|
|
+ <div className="space-y-4">
|
|
|
+ {paymentData.channel === 'online_link' && (
|
|
|
+ <div className="bg-white rounded-3xl border border-slate-200 p-5 shadow-sm space-y-3">
|
|
|
+ <p className="text-sm text-slate-500">{t('payment.link_pay_hint')}</p>
|
|
|
+ <a
|
|
|
+ href={paymentData.payment_url}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="w-full inline-flex items-center justify-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold transition hover:bg-slate-100"
|
|
|
+ >
|
|
|
+ {t('payment.go_to_pay')} <ExternalLink size={18} />
|
|
|
+ </a>
|
|
|
</div>
|
|
|
+ )}
|
|
|
+ {paymentData.channel === 'qr_static' && (
|
|
|
+ <div className="bg-white rounded-3xl border border-slate-100 shadow-sm p-4 text-center">
|
|
|
+ <p className="text-sm text-slate-500 mb-3">{t('payment.qr_pay_hint')}</p>
|
|
|
+ <div className="inline-flex rounded-2xl border border-slate-200 p-3">
|
|
|
+ {qrCode ? (
|
|
|
+ <img
|
|
|
+ src={qrCode.startsWith('http') || qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`}
|
|
|
+ alt="Payment QR"
|
|
|
+ className="w-36 h-36 object-contain"
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <div className="w-36 h-36 flex items-center justify-center text-gray-400">{t('common.loading')}</div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className="rounded-3xl border border-amber-100 bg-amber-50 p-4 text-xs text-amber-700">
|
|
|
+ <AlertCircle size={16} className="inline-block mr-1" /> {t('payment.confirm_hint_text')}
|
|
|
</div>
|
|
|
- )}
|
|
|
-
|
|
|
- {/* Confirm Area */}
|
|
|
- <div className="mt-8 pt-6 border-t border-slate-100">
|
|
|
- <div className="flex items-start gap-2 bg-amber-50 p-3 rounded-lg text-left mb-4 border border-amber-100">
|
|
|
- <AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
|
|
|
- <p className="text-xs text-amber-800 leading-relaxed">
|
|
|
- {t('payment.confirm_hint_text')}
|
|
|
- </p>
|
|
|
- </div>
|
|
|
-
|
|
|
<button
|
|
|
onClick={handleUserConfirm}
|
|
|
disabled={confirming}
|
|
|
- className="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3.5 rounded-xl transition shadow-lg shadow-green-200 flex items-center justify-center gap-2 active:scale-[0.98]"
|
|
|
+ className="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3.5 rounded-2xl transition shadow-lg shadow-red-200 flex items-center justify-center gap-2"
|
|
|
>
|
|
|
{confirming ? <Loader2 size={20} className="animate-spin" /> : <CheckCircle2 size={20} />}
|
|
|
{t('payment.completed_btn')}
|
|
|
</button>
|
|
|
-
|
|
|
- <p className="text-xs text-gray-400 mt-3">
|
|
|
- {t('payment.confirm_subtext')}
|
|
|
- </p>
|
|
|
+ <p className="text-center text-xs text-slate-400">{t('payment.confirm_subtext')}</p>
|
|
|
</div>
|
|
|
-
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
@@ -408,4 +404,4 @@ export default function PaymentProcessor({ orderId }: PaymentProcessorProps) {
|
|
|
/>
|
|
|
</div>
|
|
|
);
|
|
|
-}
|
|
|
+}
|