feat: add pricing pay page
parent
aabc979fec
commit
7faf2cddcf
|
|
@ -64,6 +64,7 @@ export default function App() {
|
|||
<Route path="/stats" element={<StatsPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/pricing" element={<PricingPage />} />
|
||||
<Route path="/pricing/pay/:tierId" element={<PricingPage />} />
|
||||
<Route path="/workflows" element={<WorkflowsPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgeBasePage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { App as AntApp } from 'antd';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { MembershipAPI, MembershipInfo } from '../../api/membership';
|
||||
|
||||
export interface PricingTier {
|
||||
|
|
@ -113,16 +114,47 @@ export const ENTERPRISE_TIERS: PricingTier[] = [
|
|||
];
|
||||
|
||||
export function usePricingLogic() {
|
||||
const navigate = useNavigate();
|
||||
const { tierId } = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const cycle = searchParams.get('cycle') || 'monthly';
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'personal' | 'enterprise'>('personal');
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly');
|
||||
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>(cycle as any);
|
||||
const [membership, setMembership] = useState<MembershipInfo | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [timeLeft, setTimeLeft] = useState(600); // 10 minutes in seconds
|
||||
const { message } = AntApp.useApp();
|
||||
|
||||
// 根据 URL 参数计算当前的支付信息
|
||||
const paymentInfo = useMemo(() => {
|
||||
if (!tierId) return null;
|
||||
const allTiers = [...PERSONAL_TIERS, ...ENTERPRISE_TIERS];
|
||||
const tier = allTiers.find(t => t.id === tierId);
|
||||
if (!tier) return null;
|
||||
return {
|
||||
tier,
|
||||
duration: cycle === 'monthly' ? 30 : 365
|
||||
};
|
||||
}, [tierId, cycle]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMembership();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: any;
|
||||
if (paymentInfo && timeLeft > 0) {
|
||||
timer = setInterval(() => {
|
||||
setTimeLeft((prev) => prev - 1);
|
||||
}, 1000);
|
||||
} else if (timeLeft === 0) {
|
||||
navigate('/pricing', { replace: true });
|
||||
message.error('支付超时,请重新发起');
|
||||
}
|
||||
return () => clearInterval(timer);
|
||||
}, [paymentInfo, timeLeft, navigate]);
|
||||
|
||||
const loadMembership = async () => {
|
||||
try {
|
||||
const info = await MembershipAPI.getMe();
|
||||
|
|
@ -135,18 +167,13 @@ export function usePricingLogic() {
|
|||
const handleSubscribe = async (tier: PricingTier) => {
|
||||
if (tier.price === '面议' || tier.id === 'trial') return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const durationDays = billingCycle === 'monthly' ? 30 : 365;
|
||||
const { payUrl } = await MembershipAPI.subscribe({ tier: tier.id, durationDays });
|
||||
if (payUrl) {
|
||||
window.location.href = payUrl;
|
||||
}
|
||||
} catch (e: any) {
|
||||
message.error(e?.response?.data?.error ?? e?.message ?? '发起订阅失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// 使用子路由跳转
|
||||
navigate(`/pricing/pay/${tier.id}?cycle=${billingCycle}`);
|
||||
setTimeLeft(600);
|
||||
};
|
||||
|
||||
const cancelPayment = () => {
|
||||
navigate('/pricing');
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
@ -157,6 +184,9 @@ export function usePricingLogic() {
|
|||
membership,
|
||||
loading,
|
||||
handleSubscribe,
|
||||
paymentInfo,
|
||||
timeLeft,
|
||||
cancelPayment,
|
||||
PERSONAL_TIERS,
|
||||
ENTERPRISE_TIERS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CheckOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin } from 'antd';
|
||||
import { CheckOutlined, ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin, QRCode, Divider } from 'antd';
|
||||
import type { PricingLogicOutput } from '../PricingLogic';
|
||||
import '../styles/pricing.css';
|
||||
|
||||
|
|
@ -19,10 +19,80 @@ export default function PricingH5({ logic }: Props) {
|
|||
setBillingCycle,
|
||||
loading,
|
||||
handleSubscribe,
|
||||
paymentInfo,
|
||||
timeLeft,
|
||||
cancelPayment,
|
||||
PERSONAL_TIERS,
|
||||
ENTERPRISE_TIERS,
|
||||
} = logic;
|
||||
|
||||
if (paymentInfo) {
|
||||
const { tier, duration } = paymentInfo;
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const seconds = timeLeft % 60;
|
||||
const price = duration === 30 ? tier.price : (tier.yearlyPrice || tier.price);
|
||||
|
||||
return (
|
||||
<div className="pricing-page pricing-page-h5">
|
||||
<div className="payment-container">
|
||||
<div className="payment-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={cancelPayment}
|
||||
className="back-btn"
|
||||
>
|
||||
返回方案选择
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="payment-content">
|
||||
<div className="payment-info-card">
|
||||
<h2 className="payment-title">确认订单</h2>
|
||||
<div className="payment-detail-item">
|
||||
<span className="label">方案</span>
|
||||
<span className="value">{tier.name}</span>
|
||||
</div>
|
||||
<div className="payment-detail-item">
|
||||
<span className="label">时长</span>
|
||||
<span className="value">{duration === 30 ? '30天' : '365天'}</span>
|
||||
</div>
|
||||
<Divider style={{ margin: '16px 0' }} />
|
||||
<div className="payment-total">
|
||||
<span className="label">应付金额</span>
|
||||
<span className="total-amount">
|
||||
<span className="symbol">¥</span>
|
||||
{price}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payment-qr-card">
|
||||
<div className="qr-wrapper">
|
||||
<QRCode value="https://aura-ai.com/mock-pay" size={180} />
|
||||
<div className="qr-status">
|
||||
<LoadingOutlined /> 正在等待支付结果...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payment-countdown">
|
||||
支付剩余时间:
|
||||
<span className="time">
|
||||
{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="payment-tips">
|
||||
<p>请长按识别或保存二维码支付</p>
|
||||
<p>支付完成后系统将自动为您开通</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tiers = activeTab === 'personal' ? PERSONAL_TIERS : ENTERPRISE_TIERS;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { CheckOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin } from 'antd';
|
||||
import { CheckOutlined, ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Spin, QRCode, Space, Divider } from 'antd';
|
||||
import type { PricingLogicOutput, PricingTier } from '../PricingLogic';
|
||||
import '../styles/pricing.css';
|
||||
|
||||
|
|
@ -19,10 +19,80 @@ export default function PricingWeb({ logic }: Props) {
|
|||
setBillingCycle,
|
||||
loading,
|
||||
handleSubscribe,
|
||||
paymentInfo,
|
||||
timeLeft,
|
||||
cancelPayment,
|
||||
PERSONAL_TIERS,
|
||||
ENTERPRISE_TIERS,
|
||||
} = logic;
|
||||
|
||||
if (paymentInfo) {
|
||||
const { tier, duration } = paymentInfo;
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const seconds = timeLeft % 60;
|
||||
const price = duration === 30 ? tier.price : (tier.yearlyPrice || tier.price);
|
||||
|
||||
return (
|
||||
<div className="pricing-page">
|
||||
<div className="payment-container">
|
||||
<div className="payment-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={cancelPayment}
|
||||
className="back-btn"
|
||||
>
|
||||
返回方案选择
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="payment-content">
|
||||
<div className="payment-info-card">
|
||||
<h2 className="payment-title">确认订单并支付</h2>
|
||||
<div className="payment-detail-item">
|
||||
<span className="label">订阅方案</span>
|
||||
<span className="value">{tier.name}</span>
|
||||
</div>
|
||||
<div className="payment-detail-item">
|
||||
<span className="label">订阅时长</span>
|
||||
<span className="value">{duration === 30 ? '30 天' : '365 天'}</span>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="payment-total">
|
||||
<span className="label">应付金额</span>
|
||||
<span className="total-amount">
|
||||
<span className="symbol">¥</span>
|
||||
{price}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payment-qr-card">
|
||||
<div className="qr-wrapper">
|
||||
<QRCode value="https://aura-ai.com/mock-pay" size={200} />
|
||||
<div className="qr-status">
|
||||
<LoadingOutlined /> 正在等待支付结果...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payment-countdown">
|
||||
支付剩余时间:
|
||||
<span className="time">
|
||||
{String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="payment-tips">
|
||||
<p>请使用微信或支付宝扫描上方二维码进行支付</p>
|
||||
<p>支付完成后,系统将自动为您开通相应权限</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tiers = activeTab === 'personal' ? PERSONAL_TIERS : ENTERPRISE_TIERS;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -213,3 +213,144 @@
|
|||
.pricing-page-h5 .pricing-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Payment View Styles */
|
||||
.payment-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 24px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.payment-header {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--color-border-secondary);
|
||||
}
|
||||
|
||||
.payment-header .back-btn {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.payment-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
padding: 40px;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.payment-title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.payment-detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.payment-detail-item .label {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.payment-detail-item .value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.payment-total {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.payment-total .label {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.payment-total .total-amount {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--color-brand);
|
||||
}
|
||||
|
||||
.payment-total .symbol {
|
||||
font-size: 18px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.payment-qr-card {
|
||||
background: var(--color-fill-secondary);
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.qr-status {
|
||||
margin-top: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-brand);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.payment-countdown {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.payment-countdown .time {
|
||||
font-family: monospace;
|
||||
font-weight: 700;
|
||||
color: #ef4444;
|
||||
font-size: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.payment-tips {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.payment-tips p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.payment-content {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 24px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.payment-container {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue