Files
xiaoxia-saas/apps/web/src/pages/subscription/UpgradeSubscription.tsx
T
xiaoxia d438c1423f
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 27s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 1m13s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 3m16s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m7s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m24s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m0s
AI Code Review / AI Code Review (pull_request) Successful in 3m20s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 6m15s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 16m1s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 29s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
refactor(subscription): 拆分 UpgradeSubscription 页面,抽离 Hook + UI 组件
主文件 235→155 行(-34%),拆分为:
- constants: 套餐元数据 + 价格工具函数
- SubscriptionUI: BillingCycleSwitch + Spinner 子组件
- useSubscription: 订阅查询、变更、续费、取消等逻辑
- UpgradeSubscription: 页面入口 + 布局 + 确认框交互

导入路径保持向后兼容
2026-07-27 09:28:05 +08:00

141 lines
4.3 KiB
TypeScript

/**
* 升级/降级/续费页面
* P1-3: antd Button/Modal/Radio/Spin → 自定义 UI 组件
*/
import React from "react"
import { Modal } from "@/components/ui"
import { useNavigate } from "react-router-dom"
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 UpgradeSubscription: React.FC = () => {
const navigate = useNavigate()
const {
subscription,
loading,
submitting,
selectedPlan,
billingCycle,
setSelectedPlan,
setBillingCycle,
executeChangePlan,
handleToggleAutoRenew,
handleCancel,
} = useSubscription()
const handleUpgradeClick = () => {
if (!subscription) return
if (selectedPlan === subscription.plan_id && billingCycle === subscription.billing_cycle) {
return
}
const plan = PLANS_META[selectedPlan]
const price = getPlanPrice(selectedPlan, billingCycle)
Modal.confirm({
title: "确认变更套餐",
content: `即将变更为「${plan.name}」(${billingCycle === "monthly" ? "月付" : "年付"}),${price > 0 ? `费用 ¥${price}${billingCycle === "monthly" ? "/月" : "/年"}` : "免费"}。变更立即生效。`,
okText: "确认变更",
cancelText: "取消",
onOk: executeChangePlan,
})
}
const handleCancelClick = () => {
Modal.confirm({
title: "确认取消订阅",
content: "取消后,当前周期结束前仍可正常使用,到期后降级为体验版。",
okText: "确认取消",
cancelText: "再想想",
onOk: async () => {
const ok = await handleCancel()
if (ok) navigate("/app/subscription")
},
})
}
if (loading) {
return (
<div className="xx-upgrade-page">
<Spinner size="large" />
</div>
)
}
const currentPlan = subscription?.plan_id ?? "free"
return (
<div className="xx-upgrade-page">
<PageHead title="变更订阅方案" description={`当前套餐:${getPlanName(currentPlan)}`} />
<div className="xx-upgrade-plans">
{(["standard", "pro", "enterprise"] as PlanType[]).map((planId) => {
const plan = PLANS_META[planId]
const isCurrent = planId === currentPlan
return (
<div
key={planId}
className={`xx-upgrade-card ${isCurrent ? "current" : ""} ${selectedPlan === planId ? "selected" : ""}`}
onClick={() => setSelectedPlan(planId)}
>
{isCurrent && <div className="xx-current-badge">当前</div>}
<h3>{plan.name}</h3>
<div className="xx-price">
<BillingCycleSwitch
value={billingCycle}
onChange={setBillingCycle}
monthlyPrice={plan.price}
yearlyPrice={plan.yearlyPrice}
/>
</div>
</div>
)
})}
</div>
<div className="xx-upgrade-actions">
<Button
buttonType="primary"
buttonSize="lg"
disabled={submitting || selectedPlan === currentPlan}
onClick={handleUpgradeClick}
>
{submitting ? "处理中..." : "确认变更"}
</Button>
{subscription && subscription.status === "active" && (
<div className="xx-auto-renew-section">
<span>自动续费:{subscription.auto_renew ? "已开启" : "已关闭"}</span>
<Button
buttonType="text"
buttonSize="sm"
onClick={() => handleToggleAutoRenew(!subscription.auto_renew)}
>
{subscription.auto_renew ? "关闭" : "开启"}
</Button>
</div>
)}
{subscription && subscription.status === "active" && currentPlan !== "free" && (
<Button
buttonType="danger"
buttonSize="sm"
onClick={handleCancelClick}
className="xx-cancel-btn"
>
取消订阅
</Button>
)}
</div>
</div>
)
}
export default UpgradeSubscription
export const Component = UpgradeSubscription