6155b4d21e
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m56s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m37s
CI/CD Pipeline / Validate - Style (push) Successful in 3m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Integration Tests (push) Successful in 5m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m13s
CI/CD Pipeline / Validate - Security (push) Successful in 9m25s
CI/CD Pipeline / Unit Tests (push) Successful in 10m29s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Failing after 4h19m11s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 4h26m39s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 4h28m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 4h25m45s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 4h25m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 4h28m48s
CI/CD Pipeline / PR Build Web Image (push) Failing after 4h28m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 4h28m48s
CI/CD Pipeline / Deploy Production (push) Failing after 4h18m14s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 4h18m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 4h18m19s
CI/CD Pipeline / Canary Release to Production (push) Failing after 4h18m14s
CI/CD Pipeline / Frontend Lint (push) Failing after 4h28m48s
CI/CD Pipeline / Build Production API Image (push) Failing after 4h54m12s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
151 lines
4.8 KiB
TypeScript
151 lines
4.8 KiB
TypeScript
/**
|
|
* 升级/降级/续费页面
|
|
*/
|
|
import React from "react"
|
|
import { Modal } from "@/components/ui"
|
|
import { useNavigate } from "react-router-dom"
|
|
import type { PlanId } from "@/api/subscription/types"
|
|
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"
|
|
|
|
/** 可选择的付费档位(不含 free) */
|
|
const PAID_PLANS: PlanId[] = ["monthly", "quarterly", "yearly"]
|
|
|
|
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("/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">
|
|
{PAID_PLANS.map((planId) => {
|
|
const plan = PLANS_META[planId]
|
|
const isCurrent = planId === currentPlan
|
|
// 年卡显示年价,其他显示月价
|
|
const monthlyPrice = plan.priceYuan
|
|
const yearlyPrice = plan.yearlyPriceYuan || plan.priceYuan * 12
|
|
// 选中季卡时默认切到月付周期;年卡切到年付
|
|
const resolvedCycle: "monthly" | "yearly" = planId === "yearly" ? "yearly" : "monthly"
|
|
return (
|
|
<div
|
|
key={planId}
|
|
className={`xx-upgrade-card ${isCurrent ? "current" : ""} ${selectedPlan === planId ? "selected" : ""}`}
|
|
onClick={() => {
|
|
setSelectedPlan(planId)
|
|
setBillingCycle(resolvedCycle)
|
|
}}
|
|
>
|
|
{isCurrent && <div className="xx-current-badge">当前</div>}
|
|
<h3>{plan.name}</h3>
|
|
<div className="xx-price">
|
|
<BillingCycleSwitch
|
|
value={billingCycle}
|
|
onChange={setBillingCycle}
|
|
monthlyPrice={monthlyPrice}
|
|
yearlyPrice={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
|