From 7faf2cddcf45bc27c5a7d39550112224aae9dcbd Mon Sep 17 00:00:00 2001 From: yannyang Date: Thu, 30 Jul 2026 17:16:23 +0800 Subject: [PATCH] feat: add pricing pay page --- src/App.tsx | 1 + src/pages/Pricing/PricingLogic.ts | 58 ++++++-- src/pages/Pricing/components/PricingH5.tsx | 74 +++++++++- src/pages/Pricing/components/PricingWeb.tsx | 74 +++++++++- src/pages/Pricing/styles/pricing.css | 141 ++++++++++++++++++++ 5 files changed, 330 insertions(+), 18 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 53db1b3..9bb3327 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -64,6 +64,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/Pricing/PricingLogic.ts b/src/pages/Pricing/PricingLogic.ts index d1912f9..81a0f3b 100644 --- a/src/pages/Pricing/PricingLogic.ts +++ b/src/pages/Pricing/PricingLogic.ts @@ -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(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, }; diff --git a/src/pages/Pricing/components/PricingH5.tsx b/src/pages/Pricing/components/PricingH5.tsx index 0a52fea..8674bd4 100644 --- a/src/pages/Pricing/components/PricingH5.tsx +++ b/src/pages/Pricing/components/PricingH5.tsx @@ -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 ( +
+
+
+ +
+ +
+
+

确认订单

+
+ 方案 + {tier.name} +
+
+ 时长 + {duration === 30 ? '30天' : '365天'} +
+ +
+ 应付金额 + + ¥ + {price} + +
+
+ +
+
+ +
+ 正在等待支付结果... +
+
+ +
+ 支付剩余时间: + + {String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')} + +
+ +
+

请长按识别或保存二维码支付

+

支付完成后系统将自动为您开通

+
+
+
+
+
+ ); + } + const tiers = activeTab === 'personal' ? PERSONAL_TIERS : ENTERPRISE_TIERS; return ( diff --git a/src/pages/Pricing/components/PricingWeb.tsx b/src/pages/Pricing/components/PricingWeb.tsx index e2cc697..2326ce7 100644 --- a/src/pages/Pricing/components/PricingWeb.tsx +++ b/src/pages/Pricing/components/PricingWeb.tsx @@ -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 ( +
+
+
+ +
+ +
+
+

确认订单并支付

+
+ 订阅方案 + {tier.name} +
+
+ 订阅时长 + {duration === 30 ? '30 天' : '365 天'} +
+ +
+ 应付金额 + + ¥ + {price} + +
+
+ +
+
+ +
+ 正在等待支付结果... +
+
+ +
+ 支付剩余时间: + + {String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')} + +
+ +
+

请使用微信或支付宝扫描上方二维码进行支付

+

支付完成后,系统将自动为您开通相应权限

+
+
+
+
+
+ ); + } + const tiers = activeTab === 'personal' ? PERSONAL_TIERS : ENTERPRISE_TIERS; return ( diff --git a/src/pages/Pricing/styles/pricing.css b/src/pages/Pricing/styles/pricing.css index 16d471e..d819501 100644 --- a/src/pages/Pricing/styles/pricing.css +++ b/src/pages/Pricing/styles/pricing.css @@ -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; + } +}