From c7027c07d1a9b9695fb5fa25072ffdba8e5ae5cb Mon Sep 17 00:00:00 2001 From: CI Test Date: Sun, 28 Jun 2026 20:15:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(subscription):=20=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=AE=A2=E9=98=85API=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4mock=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/api/subscription.ts | 72 +--------- apps/web/src/pages/subscription/Billing.tsx | 144 ++++++++------------ 2 files changed, 60 insertions(+), 156 deletions(-) diff --git a/apps/web/src/api/subscription.ts b/apps/web/src/api/subscription.ts index 8fecd715d..5bba83b3b 100644 --- a/apps/web/src/api/subscription.ts +++ b/apps/web/src/api/subscription.ts @@ -1,6 +1,6 @@ /** * 订阅 API 模块 - * 提供订阅管理相关接口(当前使用 mock 数据,后端就绪后切换) + * 对接后端订阅管理接口 */ import apiClient from './client'; @@ -66,98 +66,34 @@ export interface ChangePlanResponse { 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 => { - 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 => { - if (USE_MOCK) { - await new Promise((resolve) => setTimeout(resolve, 300)); - return MOCK_BILLING_RECORDS; - } - const response = await apiClient.get('/subscription/billing'); + const response = await apiClient.get('/subscription/billing-records'); return response.data; }; /** 升级/降级套餐 */ export const changePlan = async (request: ChangePlanRequest): Promise => { - 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); + const response = await apiClient.post('/subscription/change-plan', 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 }); + const response = await apiClient.post('/subscription/toggle-auto-renew', { enabled }); return response.data; }; diff --git a/apps/web/src/pages/subscription/Billing.tsx b/apps/web/src/pages/subscription/Billing.tsx index 10fcf5a0b..f5047345d 100644 --- a/apps/web/src/pages/subscription/Billing.tsx +++ b/apps/web/src/pages/subscription/Billing.tsx @@ -1,20 +1,13 @@ /** * 账单管理页面 + * 展示当前订阅信息 + 自动续费开关 */ import React, { useState, useEffect } from 'react'; -import { Button, Tag, message, Spin, Empty } from 'antd'; -import { useNavigate } from 'react-router-dom'; -import { getBillingRecords, getCurrentSubscription } from '@/api/subscription'; -import type { BillingRecord, SubscriptionInfo } from '@/api/subscription'; +import { Switch, message, Spin } from 'antd'; +import { getCurrentSubscription, toggleAutoRenew } from '@/api/subscription'; +import type { SubscriptionInfo } from '@/api/subscription'; import './Billing.css'; -const STATUS_MAP: Record = { - 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' }); @@ -26,10 +19,9 @@ const formatAmount = (amount: number): string => { }; const Billing: React.FC = () => { - const navigate = useNavigate(); - const [records, setRecords] = useState([]); const [subscription, setSubscription] = useState(null); const [loading, setLoading] = useState(true); + const [autoRenewChecked, setAutoRenewChecked] = useState(false); useEffect(() => { loadData(); @@ -37,25 +29,27 @@ const Billing: React.FC = () => { 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); + const data = await getCurrentSubscription(); + setSubscription(data); + setAutoRenewChecked(data.auto_renew); } catch { - message.error('加载账单数据失败'); + message.error('加载订阅数据失败'); } finally { setLoading(false); } }; - const handleDownloadInvoice = (record: BillingRecord) => { - if (!record.invoice_url || record.invoice_url === '#') { - message.info('发票功能暂未开放'); - return; + const handleToggleAutoRenew = async (checked: boolean) => { + try { + const res = await toggleAutoRenew(checked); + message.success(res.message); + setAutoRenewChecked(checked); + if (subscription) { + setSubscription({ ...subscription, auto_renew: checked }); + } + } catch { + message.error('操作失败'); } - window.open(record.invoice_url, '_blank'); }; if (loading) { @@ -68,75 +62,49 @@ const Billing: React.FC = () => { return (
- {/* 当前订阅概览 */} {subscription && ( -
-

当前订阅

-
-
- 套餐 - {subscription.plan_name} -
-
- 计费周期 - - {subscription.billing_cycle === 'monthly' ? '月付' : '年付'} - -
-
- 下次扣费 - {formatDate(subscription.current_period_end)} -
-
- 自动续费 - {subscription.auto_renew ? '已开启' : '已关闭'} + <> + {/* 当前订阅概览 */} +
+

当前订阅

+
+
+ 套餐 + {subscription.plan_name} +
+
+ 计费周期 + + {subscription.billing_cycle === 'monthly' ? '月付' : '年付'} + +
+
+ 下次扣费 + {formatDate(subscription.current_period_end)} +
- -
- )} - {/* 账单记录 */} -
-

账单记录

- {records.length === 0 ? ( - - ) : ( -
-
- 日期 - 套餐 - 金额 - 支付方式 - 状态 - 操作 + {/* 自动续费 */} +
+

自动续费

+
+
+

到期自动续费

+

+ 开启后,将在每个计费周期结束时自动扣费续期,避免服务中断。 +

+
+
- {records.map((record) => { - const statusInfo = STATUS_MAP[record.status] ?? STATUS_MAP.pending; - return ( -
- {formatDate(record.created_at)} - {record.plan_name} - {formatAmount(record.amount)} - {record.payment_method} - - {statusInfo.label} - - - {record.status === 'paid' && record.invoice_url && ( - - )} - -
- ); - })}
- )} -
+ + )}
); }; -- 2.54.0