feat: add pricing pay page
parent
aabc979fec
commit
7faf2cddcf
|
|
@ -64,6 +64,7 @@ export default function App() {
|
||||||
<Route path="/stats" element={<StatsPage />} />
|
<Route path="/stats" element={<StatsPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
<Route path="/pricing" element={<PricingPage />} />
|
<Route path="/pricing" element={<PricingPage />} />
|
||||||
|
<Route path="/pricing/pay/:tierId" element={<PricingPage />} />
|
||||||
<Route path="/workflows" element={<WorkflowsPage />} />
|
<Route path="/workflows" element={<WorkflowsPage />} />
|
||||||
<Route path="/knowledge" element={<KnowledgeBasePage />} />
|
<Route path="/knowledge" element={<KnowledgeBasePage />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<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 { App as AntApp } from 'antd';
|
||||||
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { MembershipAPI, MembershipInfo } from '../../api/membership';
|
import { MembershipAPI, MembershipInfo } from '../../api/membership';
|
||||||
|
|
||||||
export interface PricingTier {
|
export interface PricingTier {
|
||||||
|
|
@ -113,16 +114,47 @@ export const ENTERPRISE_TIERS: PricingTier[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export function usePricingLogic() {
|
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 [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 [membership, setMembership] = useState<MembershipInfo | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [timeLeft, setTimeLeft] = useState(600); // 10 minutes in seconds
|
||||||
const { message } = AntApp.useApp();
|
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(() => {
|
useEffect(() => {
|
||||||
loadMembership();
|
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 () => {
|
const loadMembership = async () => {
|
||||||
try {
|
try {
|
||||||
const info = await MembershipAPI.getMe();
|
const info = await MembershipAPI.getMe();
|
||||||
|
|
@ -135,18 +167,13 @@ export function usePricingLogic() {
|
||||||
const handleSubscribe = async (tier: PricingTier) => {
|
const handleSubscribe = async (tier: PricingTier) => {
|
||||||
if (tier.price === '面议' || tier.id === 'trial') return;
|
if (tier.price === '面议' || tier.id === 'trial') return;
|
||||||
|
|
||||||
setLoading(true);
|
// 使用子路由跳转
|
||||||
try {
|
navigate(`/pricing/pay/${tier.id}?cycle=${billingCycle}`);
|
||||||
const durationDays = billingCycle === 'monthly' ? 30 : 365;
|
setTimeLeft(600);
|
||||||
const { payUrl } = await MembershipAPI.subscribe({ tier: tier.id, durationDays });
|
};
|
||||||
if (payUrl) {
|
|
||||||
window.location.href = payUrl;
|
const cancelPayment = () => {
|
||||||
}
|
navigate('/pricing');
|
||||||
} catch (e: any) {
|
|
||||||
message.error(e?.response?.data?.error ?? e?.message ?? '发起订阅失败');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -157,6 +184,9 @@ export function usePricingLogic() {
|
||||||
membership,
|
membership,
|
||||||
loading,
|
loading,
|
||||||
handleSubscribe,
|
handleSubscribe,
|
||||||
|
paymentInfo,
|
||||||
|
timeLeft,
|
||||||
|
cancelPayment,
|
||||||
PERSONAL_TIERS,
|
PERSONAL_TIERS,
|
||||||
ENTERPRISE_TIERS,
|
ENTERPRISE_TIERS,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { CheckOutlined } from '@ant-design/icons';
|
import { CheckOutlined, ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { Button, Spin } from 'antd';
|
import { Button, Spin, QRCode, Divider } from 'antd';
|
||||||
import type { PricingLogicOutput } from '../PricingLogic';
|
import type { PricingLogicOutput } from '../PricingLogic';
|
||||||
import '../styles/pricing.css';
|
import '../styles/pricing.css';
|
||||||
|
|
||||||
|
|
@ -19,10 +19,80 @@ export default function PricingH5({ logic }: Props) {
|
||||||
setBillingCycle,
|
setBillingCycle,
|
||||||
loading,
|
loading,
|
||||||
handleSubscribe,
|
handleSubscribe,
|
||||||
|
paymentInfo,
|
||||||
|
timeLeft,
|
||||||
|
cancelPayment,
|
||||||
PERSONAL_TIERS,
|
PERSONAL_TIERS,
|
||||||
ENTERPRISE_TIERS,
|
ENTERPRISE_TIERS,
|
||||||
} = logic;
|
} = 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;
|
const tiers = activeTab === 'personal' ? PERSONAL_TIERS : ENTERPRISE_TIERS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { CheckOutlined } from '@ant-design/icons';
|
import { CheckOutlined, ArrowLeftOutlined, LoadingOutlined } from '@ant-design/icons';
|
||||||
import { Button, Spin } from 'antd';
|
import { Button, Spin, QRCode, Space, Divider } from 'antd';
|
||||||
import type { PricingLogicOutput, PricingTier } from '../PricingLogic';
|
import type { PricingLogicOutput, PricingTier } from '../PricingLogic';
|
||||||
import '../styles/pricing.css';
|
import '../styles/pricing.css';
|
||||||
|
|
||||||
|
|
@ -19,10 +19,80 @@ export default function PricingWeb({ logic }: Props) {
|
||||||
setBillingCycle,
|
setBillingCycle,
|
||||||
loading,
|
loading,
|
||||||
handleSubscribe,
|
handleSubscribe,
|
||||||
|
paymentInfo,
|
||||||
|
timeLeft,
|
||||||
|
cancelPayment,
|
||||||
PERSONAL_TIERS,
|
PERSONAL_TIERS,
|
||||||
ENTERPRISE_TIERS,
|
ENTERPRISE_TIERS,
|
||||||
} = logic;
|
} = 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;
|
const tiers = activeTab === 'personal' ? PERSONAL_TIERS : ENTERPRISE_TIERS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -213,3 +213,144 @@
|
||||||
.pricing-page-h5 .pricing-grid {
|
.pricing-page-h5 .pricing-grid {
|
||||||
grid-template-columns: 1fr;
|
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