feat: Phase 2 - 订阅管理前端页面 #77
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 订阅 API 模块
|
||||
* 提供订阅管理相关接口(当前使用 mock 数据,后端就绪后切换)
|
||||
*/
|
||||
import apiClient from './client';
|
||||
|
||||
/** 套餐类型 */
|
||||
export type PlanType = 'free' | 'standard' | 'pro' | 'enterprise';
|
||||
|
||||
/** 订阅状态 */
|
||||
export type SubscriptionStatus = 'active' | 'expired' | 'cancelled' | 'trial';
|
||||
|
||||
/** 账单状态 */
|
||||
export type BillingStatus = 'paid' | 'pending' | 'failed' | 'refunded';
|
||||
|
||||
/** 计费周期 */
|
||||
export type BillingCycle = 'monthly' | 'yearly';
|
||||
|
||||
/** 套餐信息 */
|
||||
export interface Plan {
|
||||
id: PlanType;
|
||||
name: string;
|
||||
price: number | null;
|
||||
yearly_price?: number | null;
|
||||
description: string;
|
||||
recommended: boolean;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
/** 当前订阅信息 */
|
||||
export interface SubscriptionInfo {
|
||||
id: string;
|
||||
plan_id: PlanType;
|
||||
plan_name: string;
|
||||
status: SubscriptionStatus;
|
||||
billing_cycle: BillingCycle;
|
||||
current_period_start: string;
|
||||
current_period_end: string;
|
||||
amount: number;
|
||||
auto_renew: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/** 账单记录 */
|
||||
export interface BillingRecord {
|
||||
id: string;
|
||||
plan_name: string;
|
||||
amount: number;
|
||||
billing_cycle: BillingCycle;
|
||||
status: BillingStatus;
|
||||
payment_method: string;
|
||||
created_at: string;
|
||||
invoice_url?: string;
|
||||
}
|
||||
|
||||
/** 升级/降级请求 */
|
||||
export interface ChangePlanRequest {
|
||||
target_plan_id: PlanType;
|
||||
billing_cycle: BillingCycle;
|
||||
}
|
||||
|
||||
/** 升级/降级响应 */
|
||||
export interface ChangePlanResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
new_subscription?: SubscriptionInfo;
|
||||
}
|
||||
|
||||
// ============ Mock 数据 ============
|
||||
|
||||
const MOCK_SUBSCRIPTION: SubscriptionInfo = {
|
||||
id: 'sub-001',
|
||||
plan_id: 'standard',
|
||||
plan_name: '标准版',
|
||||
status: 'active',
|
||||
billing_cycle: 'monthly',
|
||||
current_period_start: '2026-06-01T00:00:00Z',
|
||||
current_period_end: '2026-07-01T00:00:00Z',
|
||||
amount: 99,
|
||||
auto_renew: true,
|
||||
created_at: '2026-03-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const MOCK_BILLING_RECORDS: BillingRecord[] = [
|
||||
{
|
||||
id: 'bill-001', plan_name: '标准版', amount: 99,
|
||||
billing_cycle: 'monthly', status: 'paid', payment_method: '微信支付',
|
||||
created_at: '2026-06-01T00:00:00Z', invoice_url: '#',
|
||||
},
|
||||
{
|
||||
id: 'bill-002', plan_name: '标准版', amount: 99,
|
||||
billing_cycle: 'monthly', status: 'paid', payment_method: '微信支付',
|
||||
created_at: '2026-05-01T00:00:00Z', invoice_url: '#',
|
||||
},
|
||||
{
|
||||
id: 'bill-003', plan_name: '标准版', amount: 99,
|
||||
billing_cycle: 'monthly', status: 'paid', payment_method: '支付宝',
|
||||
created_at: '2026-04-01T00:00:00Z', invoice_url: '#',
|
||||
},
|
||||
];
|
||||
|
||||
/** 是否使用 mock 数据(后端就绪后改为 false) */
|
||||
const USE_MOCK = true;
|
||||
|
||||
// ============ API 函数 ============
|
||||
|
||||
/** 获取当前订阅信息 */
|
||||
export const getCurrentSubscription = async (): Promise<SubscriptionInfo> => {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return MOCK_SUBSCRIPTION;
|
||||
}
|
||||
const response = await apiClient.get('/subscription/current');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 获取账单记录列表 */
|
||||
export const getBillingRecords = async (): Promise<BillingRecord[]> => {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return MOCK_BILLING_RECORDS;
|
||||
}
|
||||
const response = await apiClient.get('/subscription/billing');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 升级/降级套餐 */
|
||||
export const changePlan = async (request: ChangePlanRequest): Promise<ChangePlanResponse> => {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return {
|
||||
success: true,
|
||||
message: '套餐变更成功',
|
||||
new_subscription: {
|
||||
...MOCK_SUBSCRIPTION,
|
||||
plan_id: request.target_plan_id,
|
||||
plan_name: request.target_plan_id === 'pro' ? '专业版' : request.target_plan_id === 'standard' ? '标准版' : '体验版',
|
||||
},
|
||||
};
|
||||
}
|
||||
const response = await apiClient.post('/subscription/change', request);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 取消订阅 */
|
||||
export const cancelSubscription = async (): Promise<{ success: boolean; message: string }> => {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
return { success: true, message: '订阅已取消,当前周期结束后停止服务' };
|
||||
}
|
||||
const response = await apiClient.post('/subscription/cancel');
|
||||
return response.data;
|
||||
};
|
||||
|
||||
/** 切换自动续费 */
|
||||
export const toggleAutoRenew = async (enabled: boolean): Promise<{ success: boolean; message: string }> => {
|
||||
if (USE_MOCK) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return { success: true, message: enabled ? '已开启自动续费' : '已关闭自动续费' };
|
||||
}
|
||||
const response = await apiClient.post('/subscription/auto-renew', { enabled });
|
||||
return response.data;
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
/* 账单管理页面 */
|
||||
.xx-billing-page {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
/* 订阅概览 */
|
||||
.xx-billing-overview {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(226, 232, 240, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
margin-bottom: 40px;
|
||||
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.xx-billing-overview h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: var(--slate, #0f172a);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.xx-overview-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.xx-overview-details {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.xx-overview-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.xx-label {
|
||||
font-size: 13px;
|
||||
color: var(--muted, #64748b);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.xx-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--slate, #0f172a);
|
||||
}
|
||||
|
||||
/* 账单记录 */
|
||||
.xx-billing-history {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(226, 232, 240, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 8px 30px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.xx-billing-history h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: var(--slate, #0f172a);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.xx-billing-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.xx-table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 0.8fr 1fr 0.8fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(241, 245, 249, 0.8);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--muted, #64748b);
|
||||
}
|
||||
|
||||
.xx-table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr 0.8fr 1fr 0.8fr 1fr;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
|
||||
font-size: 14px;
|
||||
color: var(--slate, #0f172a);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.xx-table-row:hover {
|
||||
background: rgba(241, 245, 249, 0.4);
|
||||
}
|
||||
|
||||
.xx-table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.xx-amount {
|
||||
font-weight: 700;
|
||||
color: var(--indigo, #4f46e5);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.xx-table-header,
|
||||
.xx-table-row {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.xx-table-header span:nth-child(4),
|
||||
.xx-table-header span:nth-child(6),
|
||||
.xx-table-row span:nth-child(4),
|
||||
.xx-table-row span:nth-child(6) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,145 @@
|
||||
import React from 'react';
|
||||
import { Button, Result } from 'antd';
|
||||
/**
|
||||
* 账单管理页面
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Tag, message, Spin, Empty, Modal } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import '../profile/ProfileSettings.css';
|
||||
import { getBillingRecords, getCurrentSubscription } from '@/api/subscription';
|
||||
import type { BillingRecord, SubscriptionInfo } from '@/api/subscription';
|
||||
import './Billing.css';
|
||||
|
||||
const STATUS_MAP: Record<string, { color: string; label: string }> = {
|
||||
paid: { color: 'success', label: '已支付' },
|
||||
pending: { color: 'warning', label: '待支付' },
|
||||
failed: { color: 'error', label: '支付失败' },
|
||||
refunded: { color: 'default', label: '已退款' },
|
||||
};
|
||||
|
||||
const formatDate = (iso: string): string => {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
||||
};
|
||||
|
||||
const formatAmount = (amount: number): string => {
|
||||
if (amount === 0) return '免费';
|
||||
return `¥${amount.toFixed(2)}`;
|
||||
};
|
||||
|
||||
const Billing: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [records, setRecords] = useState<BillingRecord[]>([]);
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const [billingData, subData] = await Promise.allSettled([
|
||||
getBillingRecords(),
|
||||
getCurrentSubscription(),
|
||||
]);
|
||||
if (billingData.status === 'fulfilled') setRecords(billingData.value);
|
||||
if (subData.status === 'fulfilled') setSubscription(subData.value);
|
||||
} catch {
|
||||
message.error('加载账单数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadInvoice = (record: BillingRecord) => {
|
||||
if (!record.invoice_url || record.invoice_url === '#') {
|
||||
message.info('发票功能暂未开放');
|
||||
return;
|
||||
}
|
||||
window.open(record.invoice_url, '_blank');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="xx-billing-page">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="xx-settings-page">
|
||||
<div className="xx-result-page">
|
||||
<Result
|
||||
status="info"
|
||||
title="账单管理暂未开放"
|
||||
subTitle="当前版本未接入账单、支付、发票和下载后端服务,因此不展示模拟账单,也不会伪造下载成功。"
|
||||
extra={[
|
||||
<Button key="plans" type="primary" onClick={() => navigate('/subscription')}>
|
||||
返回订阅说明
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<div className="xx-billing-page">
|
||||
{/* 当前订阅概览 */}
|
||||
{subscription && (
|
||||
<div className="xx-billing-overview">
|
||||
<h2>当前订阅</h2>
|
||||
<div className="xx-overview-details">
|
||||
<div className="xx-overview-item">
|
||||
<span className="xx-label">套餐</span>
|
||||
<span className="xx-value">{subscription.plan_name}</span>
|
||||
</div>
|
||||
<div className="xx-overview-item">
|
||||
<span className="xx-label">计费周期</span>
|
||||
<span className="xx-value">
|
||||
{subscription.billing_cycle === 'monthly' ? '月付' : '年付'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="xx-overview-item">
|
||||
<span className="xx-label">下次扣费</span>
|
||||
<span className="xx-value">{formatDate(subscription.current_period_end)}</span>
|
||||
</div>
|
||||
<div className="xx-overview-item">
|
||||
<span className="xx-label">自动续费</span>
|
||||
<span className="xx-value">{subscription.auto_renew ? '已开启' : '已关闭'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" onClick={() => navigate('/subscription/upgrade')}>
|
||||
管理订阅
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 账单记录 */}
|
||||
<div className="xx-billing-history">
|
||||
<h2>账单记录</h2>
|
||||
{records.length === 0 ? (
|
||||
<Empty description="暂无账单记录" />
|
||||
) : (
|
||||
<div className="xx-billing-table">
|
||||
<div className="xx-table-header">
|
||||
<span>日期</span>
|
||||
<span>套餐</span>
|
||||
<span>金额</span>
|
||||
<span>支付方式</span>
|
||||
<span>状态</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
{records.map((record) => {
|
||||
const statusInfo = STATUS_MAP[record.status] ?? STATUS_MAP.pending;
|
||||
return (
|
||||
<div key={record.id} className="xx-table-row">
|
||||
<span>{formatDate(record.created_at)}</span>
|
||||
<span>{record.plan_name}</span>
|
||||
<span className="xx-amount">{formatAmount(record.amount)}</span>
|
||||
<span>{record.payment_method}</span>
|
||||
<span>
|
||||
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
|
||||
</span>
|
||||
<span>
|
||||
{record.status === 'paid' && record.invoice_url && (
|
||||
<Button type="link" size="small" onClick={() => handleDownloadInvoice(record)}>
|
||||
下载发票
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Billing;
|
||||
|
||||
export const Component = Billing;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
/* 升级/降级页面 */
|
||||
.xx-upgrade-page {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.xx-upgrade-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.xx-upgrade-header h2 {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: var(--slate, #0f172a);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.xx-upgrade-header p {
|
||||
font-size: 16px;
|
||||
color: var(--muted, #64748b);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 套餐卡片 */
|
||||
.xx-upgrade-plans {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.xx-upgrade-plans {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.xx-upgrade-card {
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 2px solid rgba(226, 232, 240, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 28px 24px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xx-upgrade-card:hover {
|
||||
border-color: var(--indigo, #4f46e5);
|
||||
box-shadow: 0 8px 30px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.xx-upgrade-card.selected {
|
||||
border-color: var(--indigo, #4f46e5);
|
||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.12), 0 8px 30px rgba(79, 70, 229, 0.1);
|
||||
}
|
||||
|
||||
.xx-upgrade-card.current {
|
||||
border-color: var(--green, #10b981);
|
||||
}
|
||||
|
||||
.xx-current-badge {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: 16px;
|
||||
background: var(--green, #10b981);
|
||||
color: white;
|
||||
padding: 3px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.xx-upgrade-card h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: var(--slate, #0f172a);
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.xx-price {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.xx-save {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--green, #10b981);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 操作区 */
|
||||
.xx-upgrade-actions {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.xx-auto-renew-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--muted, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.xx-cancel-btn {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -1,30 +1,188 @@
|
||||
import React from 'react';
|
||||
import { Button, Result } from 'antd';
|
||||
/**
|
||||
* 升级/降级/续费页面
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Radio, message, Spin, Modal } from 'antd';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import '../profile/ProfileSettings.css';
|
||||
import { getCurrentSubscription, changePlan, toggleAutoRenew, cancelSubscription } from '@/api/subscription';
|
||||
import type { SubscriptionInfo, PlanType, BillingCycle } from '@/api/subscription';
|
||||
import './UpgradeSubscription.css';
|
||||
|
||||
const PLANS_META: Record<string, { name: string; price: number; yearlyPrice: number }> = {
|
||||
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 UpgradeSubscription: React.FC = () => {
|
||||
// workspaceId removed - projects now belong directly to user
|
||||
const navigate = useNavigate();
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [selectedPlan, setSelectedPlan] = useState<PlanType>('standard');
|
||||
const [billingCycle, setBillingCycle] = useState<BillingCycle>('monthly');
|
||||
|
||||
useEffect(() => {
|
||||
loadSubscription();
|
||||
}, []);
|
||||
|
||||
const loadSubscription = async () => {
|
||||
try {
|
||||
const data = await getCurrentSubscription();
|
||||
setSubscription(data);
|
||||
setSelectedPlan(data.plan_id);
|
||||
} catch {
|
||||
message.error('获取订阅信息失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
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;
|
||||
|
||||
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 {
|
||||
message.error('套餐变更失败,请重试');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleAutoRenew = async (enabled: boolean) => {
|
||||
try {
|
||||
const res = await toggleAutoRenew(enabled);
|
||||
message.success(res.message);
|
||||
if (subscription) {
|
||||
setSubscription({ ...subscription, auto_renew: enabled });
|
||||
}
|
||||
} catch {
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
Modal.confirm({
|
||||
title: '确认取消订阅',
|
||||
content: '取消后,当前周期结束前仍可正常使用,到期后降级为体验版。',
|
||||
okText: '确认取消',
|
||||
okType: 'danger',
|
||||
cancelText: '再想想',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await cancelSubscription();
|
||||
message.success(res.message);
|
||||
navigate('/subscription');
|
||||
} catch {
|
||||
message.error('取消失败');
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="xx-upgrade-page"><Spin size="large" /></div>;
|
||||
}
|
||||
|
||||
const currentPlan = subscription?.plan_id ?? 'free';
|
||||
|
||||
return (
|
||||
<div className="xx-settings-page">
|
||||
<div className="xx-result-page">
|
||||
<Result
|
||||
status="info"
|
||||
title="订阅升级暂未开放"
|
||||
subTitle="当前版本未接入订阅、支付和配额变更后端服务。为避免假成功或调用不存在的接口,此入口已暂时关闭。"
|
||||
extra={[
|
||||
<Button key="plans" onClick={() => navigate('/subscription')}>
|
||||
查看套餐说明
|
||||
</Button>,
|
||||
]}
|
||||
/>
|
||||
<div className="xx-upgrade-page">
|
||||
<div className="xx-upgrade-header">
|
||||
<h2>变更订阅方案</h2>
|
||||
<p>当前套餐:{PLANS_META[currentPlan]?.name ?? '体验版'}</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Radio.Group
|
||||
value={billingCycle}
|
||||
onChange={(e) => setBillingCycle(e.target.value)}
|
||||
size="small"
|
||||
>
|
||||
<Radio.Button value="monthly">
|
||||
¥{plan.price}/月
|
||||
</Radio.Button>
|
||||
<Radio.Button value="yearly">
|
||||
¥{plan.yearlyPrice}/年
|
||||
{plan.yearlyPrice > 0 && plan.price > 0 && (
|
||||
<span className="xx-save">省 ¥{plan.price * 12 - plan.yearlyPrice}</span>
|
||||
)}
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="xx-upgrade-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
loading={submitting}
|
||||
onClick={handleUpgrade}
|
||||
disabled={selectedPlan === currentPlan}
|
||||
>
|
||||
确认变更
|
||||
</Button>
|
||||
|
||||
{subscription && subscription.status === 'active' && (
|
||||
<div className="xx-auto-renew-section">
|
||||
<span>自动续费:{subscription.auto_renew ? '已开启' : '已关闭'}</span>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleToggleAutoRenew(!subscription.auto_renew)}
|
||||
>
|
||||
{subscription.auto_renew ? '关闭' : '开启'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{subscription && subscription.status === 'active' && currentPlan !== 'free' && (
|
||||
<Button type="link" danger onClick={handleCancel} className="xx-cancel-btn">
|
||||
取消订阅
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradeSubscription;
|
||||
|
||||
export const Component = UpgradeSubscription;
|
||||
|
||||
Reference in New Issue
Block a user