From d438c1423f4b86ea7a6a7903d79a60df3886b7c7 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 09:27:53 +0800 Subject: [PATCH] =?UTF-8?q?refactor(subscription):=20=E6=8B=86=E5=88=86=20?= =?UTF-8?q?UpgradeSubscription=20=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=8A=BD?= =?UTF-8?q?=E7=A6=BB=20Hook=20+=20UI=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主文件 235→155 行(-34%),拆分为: - constants: 套餐元数据 + 价格工具函数 - SubscriptionUI: BillingCycleSwitch + Spinner 子组件 - useSubscription: 订阅查询、变更、续费、取消等逻辑 - UpgradeSubscription: 页面入口 + 布局 + 确认框交互 导入路径保持向后兼容 --- .../subscription/UpgradeSubscription.tsx | 151 ++++-------------- .../components/SubscriptionUI.tsx | 50 ++++++ apps/web/src/pages/subscription/constants.ts | 16 ++ .../subscription/hooks/useSubscription.ts | 111 +++++++++++++ 4 files changed, 205 insertions(+), 123 deletions(-) create mode 100644 apps/web/src/pages/subscription/components/SubscriptionUI.tsx create mode 100644 apps/web/src/pages/subscription/constants.ts create mode 100644 apps/web/src/pages/subscription/hooks/useSubscription.ts diff --git a/apps/web/src/pages/subscription/UpgradeSubscription.tsx b/apps/web/src/pages/subscription/UpgradeSubscription.tsx index 720beeb2d..36033d6b0 100644 --- a/apps/web/src/pages/subscription/UpgradeSubscription.tsx +++ b/apps/web/src/pages/subscription/UpgradeSubscription.tsx @@ -2,151 +2,59 @@ * 升级/降级/续费页面 * P1-3: antd Button/Modal/Radio/Spin → 自定义 UI 组件 */ -import React, { useState, useEffect } from "react" -import { message } from "antd" -import { Button, Modal } from "@/components/ui" +import React from "react" +import { Modal } from "@/components/ui" import { useNavigate } from "react-router-dom" -import { - getCurrentSubscription, - changePlan, - toggleAutoRenew, - cancelSubscription, -} from "@/api/subscription" -import type { SubscriptionInfo, PlanType, BillingCycle } from "@/api/subscription" +import type { PlanType } from "@/api/subscription" import PageHead from "@/components/layout/PageHead" +import { Button } from "@/components/ui" +import { PLANS_META, getPlanName, getPlanPrice } from "./constants" +import { BillingCycleSwitch, Spinner } from "./components/SubscriptionUI" +import { useSubscription } from "./hooks/useSubscription" import "./UpgradeSubscription.css" -const PLANS_META: Record = { - free: { name: "体验版", price: 0, yearlyPrice: 0 }, - standard: { name: "标准版", price: 99, yearlyPrice: 990 }, - pro: { name: "专业版", price: 299, yearlyPrice: 2990 }, - enterprise: { name: "企业版", price: 0, yearlyPrice: 0 }, -} - -/** 自定义计费周期切换组件 */ -const BillingCycleSwitch: React.FC<{ - value: BillingCycle - onChange: (cycle: BillingCycle) => void - monthlyPrice: number - yearlyPrice: number -}> = ({ value, onChange, monthlyPrice, yearlyPrice }) => ( -
- - -
-) - -/** 自定义 Spinner 组件 */ -const Spinner: React.FC<{ size?: "small" | "large" }> = ({ size = "large" }) => ( -
-
-
-
-
-) - const UpgradeSubscription: React.FC = () => { const navigate = useNavigate() - const [subscription, setSubscription] = useState(null) - const [loading, setLoading] = useState(true) - const [submitting, setSubmitting] = useState(false) - const [selectedPlan, setSelectedPlan] = useState("standard") - const [billingCycle, setBillingCycle] = useState("monthly") + const { + subscription, + loading, + submitting, + selectedPlan, + billingCycle, + setSelectedPlan, + setBillingCycle, + executeChangePlan, + handleToggleAutoRenew, + handleCancel, + } = useSubscription() - useEffect(() => { - loadSubscription() - }, []) - - const loadSubscription = async () => { - try { - const data = await getCurrentSubscription() - setSubscription(data) - setSelectedPlan(data.plan_id) - } catch (err: unknown) { - if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("获取订阅信息失败") - } finally { - setLoading(false) - } - } - - const handleUpgrade = async () => { + const handleUpgradeClick = () => { if (!subscription) return if (selectedPlan === subscription.plan_id && billingCycle === subscription.billing_cycle) { - message.info("当前已是该套餐") return } const plan = PLANS_META[selectedPlan] - const price = billingCycle === "yearly" ? plan.yearlyPrice : plan.price + const price = getPlanPrice(selectedPlan, billingCycle) Modal.confirm({ title: "确认变更套餐", content: `即将变更为「${plan.name}」(${billingCycle === "monthly" ? "月付" : "年付"}),${price > 0 ? `费用 ¥${price}${billingCycle === "monthly" ? "/月" : "/年"}` : "免费"}。变更立即生效。`, okText: "确认变更", cancelText: "取消", - onOk: async () => { - try { - setSubmitting(true) - const res = await changePlan({ - target_plan_id: selectedPlan, - billing_cycle: billingCycle, - }) - if (res.success) { - message.success(res.message) - setSubscription(res.new_subscription ?? null) - } else { - message.error(res.message) - } - } catch (err: unknown) { - if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("套餐变更失败,请重试") - } finally { - setSubmitting(false) - } - }, + onOk: executeChangePlan, }) } - const handleToggleAutoRenew = async (enabled: boolean) => { - try { - const res = await toggleAutoRenew(enabled) - message.success(res.message) - if (subscription) { - setSubscription({ ...subscription, auto_renew: enabled }) - } - } catch (err: unknown) { - if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("操作失败") - } - } - - const handleCancel = () => { + const handleCancelClick = () => { Modal.confirm({ title: "确认取消订阅", content: "取消后,当前周期结束前仍可正常使用,到期后降级为体验版。", okText: "确认取消", cancelText: "再想想", onOk: async () => { - try { - const res = await cancelSubscription() - message.success(res.message) - navigate("/app/subscription") - } catch (err: unknown) { - if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("取消失败") - } + const ok = await handleCancel() + if (ok) navigate("/app/subscription") }, }) } @@ -163,10 +71,7 @@ const UpgradeSubscription: React.FC = () => { return (
- +
{(["standard", "pro", "enterprise"] as PlanType[]).map((planId) => { @@ -198,7 +103,7 @@ const UpgradeSubscription: React.FC = () => { buttonType="primary" buttonSize="lg" disabled={submitting || selectedPlan === currentPlan} - onClick={handleUpgrade} + onClick={handleUpgradeClick} > {submitting ? "处理中..." : "确认变更"} @@ -220,7 +125,7 @@ const UpgradeSubscription: React.FC = () => { + +
+) + +interface SpinnerProps { + size?: "small" | "large" +} + +/** 自定义 Spinner 组件 */ +export const Spinner: React.FC = ({ size = "large" }) => ( +
+
+
+
+
+) diff --git a/apps/web/src/pages/subscription/constants.ts b/apps/web/src/pages/subscription/constants.ts new file mode 100644 index 000000000..0e1b1843e --- /dev/null +++ b/apps/web/src/pages/subscription/constants.ts @@ -0,0 +1,16 @@ +import type { PlanType, BillingCycle } from "@/api/subscription" + +export const PLANS_META: Record = { + free: { name: "体验版", price: 0, yearlyPrice: 0 }, + standard: { name: "标准版", price: 99, yearlyPrice: 990 }, + pro: { name: "专业版", price: 299, yearlyPrice: 2990 }, + enterprise: { name: "企业版", price: 0, yearlyPrice: 0 }, +} + +export const getPlanName = (planId: PlanType | string) => PLANS_META[planId]?.name ?? "体验版" + +export const getPlanPrice = (planId: PlanType | string, cycle: BillingCycle) => { + const plan = PLANS_META[planId] + if (!plan) return 0 + return cycle === "yearly" ? plan.yearlyPrice : plan.price +} diff --git a/apps/web/src/pages/subscription/hooks/useSubscription.ts b/apps/web/src/pages/subscription/hooks/useSubscription.ts new file mode 100644 index 000000000..eeeb4e8c2 --- /dev/null +++ b/apps/web/src/pages/subscription/hooks/useSubscription.ts @@ -0,0 +1,111 @@ +import { useState, useEffect, useCallback } from "react" +import { message } from "antd" +import { + getCurrentSubscription, + changePlan, + toggleAutoRenew, + cancelSubscription, + type SubscriptionInfo, + type PlanType, + type BillingCycle, +} from "@/api/subscription" + +/** + * 订阅管理 Hook + * 封装订阅信息查询、套餐变更、自动续费切换、取消订阅等逻辑 + */ +export function useSubscription() { + const [subscription, setSubscription] = useState(null) + const [loading, setLoading] = useState(true) + const [submitting, setSubmitting] = useState(false) + const [selectedPlan, setSelectedPlan] = useState("standard") + const [billingCycle, setBillingCycle] = useState("monthly") + + const loadSubscription = useCallback(async () => { + try { + const data = await getCurrentSubscription() + setSubscription(data) + setSelectedPlan(data.plan_id) + } catch (err: unknown) { + if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("获取订阅信息失败") + } finally { + setLoading(false) + } + }, []) + + useEffect(() => { + loadSubscription() + }, [loadSubscription]) + + const handleUpgrade = useCallback(async () => { + if (!subscription) return + if (selectedPlan === subscription.plan_id && billingCycle === subscription.billing_cycle) { + message.info("当前已是该套餐") + return + } + // 由调用方决定是否弹确认框 + }, [subscription, selectedPlan, billingCycle]) + + const executeChangePlan = useCallback(async () => { + try { + setSubmitting(true) + const res = await changePlan({ + target_plan_id: selectedPlan, + billing_cycle: billingCycle, + }) + if (res.success) { + message.success(res.message) + setSubscription(res.new_subscription ?? null) + } else { + message.error(res.message) + } + } catch (err: unknown) { + if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("套餐变更失败,请重试") + } finally { + setSubmitting(false) + } + }, [selectedPlan, billingCycle]) + + const handleToggleAutoRenew = useCallback( + async (enabled: boolean) => { + try { + const res = await toggleAutoRenew(enabled) + message.success(res.message) + if (subscription) { + setSubscription({ ...subscription, auto_renew: enabled }) + } + } catch (err: unknown) { + if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("操作失败") + } + }, + [subscription], + ) + + const handleCancel = useCallback(async () => { + try { + const res = await cancelSubscription() + message.success(res.message) + return true + } catch (err: unknown) { + if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("取消失败") + return false + } + }, []) + + return { + // 状态 + subscription, + loading, + submitting, + selectedPlan, + billingCycle, + setSelectedPlan, + setBillingCycle, + // 操作 + loadSubscription, + handleUpgrade, + executeChangePlan, + handleToggleAutoRenew, + handleCancel, + } +} -- 2.54.0