From 5745d51b5a0cef785df9f5c9f2bf10554c707e40 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 16 Sep 2026 10:19:32 +0800 Subject: [PATCH] chore(points): align types/api with final backend contract - apps/web/src/pages/subscription/Billing.tsx --- apps/web/src/pages/subscription/Billing.tsx | 297 ++++++++++++-------- 1 file changed, 173 insertions(+), 124 deletions(-) diff --git a/apps/web/src/pages/subscription/Billing.tsx b/apps/web/src/pages/subscription/Billing.tsx index 68a8107a9..f421a155b 100644 --- a/apps/web/src/pages/subscription/Billing.tsx +++ b/apps/web/src/pages/subscription/Billing.tsx @@ -1,153 +1,202 @@ /** - * 账单管理页面 - * 展示当前订阅信息 + 自动续费开关 - * P1-3: antd Switch→自定义ToggleSwitch, antd Spin→自定义Spinner + * 订阅管理 & 账单页 + * - 展示当前订阅信息 + * - 取消订阅 / 开关自动续费 + * - 账单历史 */ -import React, { useState, useEffect } from "react" -import { message } from "antd" -import { getCurrentSubscription, toggleAutoRenew } from "@/api/subscription" -import type { SubscriptionInfo } from "@/api/subscription" -import PageHead from "@/components/layout/PageHead" -import "./Billing.css" +import React, { useEffect, useState } from "react" +import { Card, Button, Tag, Table, Alert, Space, Modal, message, Spin, Descriptions } from "antd" +import { useNavigate } from "react-router-dom" +import type { ColumnsType } from "antd/es/table" +import { usePointsStore } from "@/store/pointsStore" +import { getBillingRecords, cancelSubscription, toggleAutoRenew } from "@/api/subscription" +import { PLAN_LABEL, BILLING_CYCLE_LABEL } from "@/api/subscription/types" +import type { BillingRecord } from "@/api/subscription/types" +import styles from "./Subscription.css" -const formatDate = (iso: string): string => { - const d = new Date(iso) - return d.toLocaleDateString("zh-CN", { - year: "numeric", - month: "2-digit", - day: "2-digit", - }) -} +const formatYuan = (cents: number) => `¥${(cents / 100).toFixed(2)}` -/** 自定义 ToggleSwitch 组件 */ -const ToggleSwitch: React.FC<{ - checked: boolean - onChange: (checked: boolean) => void - loading?: boolean - checkedChildren?: string - unCheckedChildren?: string -}> = ({ checked, onChange, loading, checkedChildren, unCheckedChildren }) => ( - -) - -/** 自定义 Spinner 组件 */ -const Spinner: React.FC<{ size?: "small" | "large" }> = ({ size = "large" }) => ( -
-
-
-
-
-) - -const Billing: React.FC = () => { - const [subscription, setSubscription] = useState(null) +const BillingPage: React.FC = () => { + const navigate = useNavigate() + const { subscription, init, refreshBalance } = usePointsStore() + const [records, setRecords] = useState([]) const [loading, setLoading] = useState(true) - const [autoRenewChecked, setAutoRenewChecked] = useState(false) - const [autoRenewLoading, setAutoRenewLoading] = useState(false) + const [actionLoading, setActionLoading] = useState(false) useEffect(() => { - loadData() - }, []) + init() + loadBilling() + }, [init]) - const loadData = async () => { + const loadBilling = async () => { try { - const data = await getCurrentSubscription() - setSubscription(data) - setAutoRenewChecked(data.auto_renew) - } catch (err: unknown) { - if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("加载订阅数据失败") + setLoading(true) + const data = await getBillingRecords() + setRecords(data) + } catch { + // 拦截器处理 } finally { setLoading(false) } } - const handleToggleAutoRenew = async (checked: boolean) => { - setAutoRenewLoading(true) + const handleCancel = () => { + Modal.confirm({ + title: "确认取消订阅?", + content: "取消后当前计费周期结束时将不再续费,期间仍可使用会员权益。", + okText: "确认取消", + cancelText: "再想想", + okType: "danger", + onOk: async () => { + try { + setActionLoading(true) + const resp = await cancelSubscription() + message.success(resp.message || "已取消订阅") + await refreshBalance() + } catch (err) { + message.error((err as Error).message || "操作失败") + } finally { + setActionLoading(false) + } + }, + }) + } + + const handleToggleAutoRenew = async () => { + if (!subscription) return + const next = !subscription.auto_renew try { - const res = await toggleAutoRenew(checked) - message.success(res.message) - setAutoRenewChecked(checked) - if (subscription) { - setSubscription({ ...subscription, auto_renew: checked }) - } - } catch (err: unknown) { - if (!(err as { __msgShown?: boolean })?.__msgShown) message.error("操作失败") + setActionLoading(true) + const resp = await toggleAutoRenew({ enabled: next }) + message.success(resp.message) + await refreshBalance() + } catch (err) { + message.error((err as Error).message || "操作失败") } finally { - setAutoRenewLoading(false) + setActionLoading(false) } } - if (loading) { - return ( -
- -
- ) - } + const columns: ColumnsType = [ + { + title: "时间", + dataIndex: "created_at", + render: (v: string) => new Date(v).toLocaleString("zh-CN"), + }, + { + title: "类型", + dataIndex: "order_type", + render: (v: string) => { + const map: Record = { + subscribe: "订阅", + renew: "续费", + upgrade: "升级", + refund: "退款", + } + return map[v] || v + }, + }, + { + title: "套餐", + dataIndex: "plan_id", + render: (v: string) => PLAN_LABEL[v as keyof typeof PLAN_LABEL] || v, + }, + { + title: "金额", + dataIndex: "amount_cents", + align: "right", + render: (v: number) => formatYuan(v), + }, + { + title: "状态", + dataIndex: "status", + render: (s: string) => { + const map: Record = { + paid: { color: "green", label: "已支付" }, + pending: { color: "orange", label: "待支付" }, + failed: { color: "red", label: "失败" }, + refunded: { color: "default", label: "已退款" }, + } + const cfg = map[s] || { color: "default", label: s } + return {cfg.label} + }, + }, + ] + + const isPaid = subscription && subscription.plan_id !== "free" return ( -
- +
+

订阅管理

- {subscription && ( + {!subscription ? ( + + ) : !isPaid ? ( + navigate("/subscription")}> + 开通会员 + + } + /> + ) : ( <> - {/* 当前订阅概览 */} -
-

当前订阅

-
-
- 套餐 - {subscription.plan_name} -
-
- 计费周期 - - {subscription.billing_cycle === "monthly" ? "月付" : "年付"} - -
-
- 下次扣费 - {formatDate(subscription.current_period_end)} -
-
-
- - {/* 自动续费 */} -
-

自动续费

-
-
-

到期自动续费

-

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

-
- -
-
+ + + {PLAN_LABEL[subscription.plan_id]} + + {BILLING_CYCLE_LABEL[subscription.billing_cycle]} + + + {formatYuan(subscription.amount)} + + + + {subscription.status} + + + + {new Date(subscription.current_period_start).toLocaleDateString("zh-CN")} + + + {new Date(subscription.current_period_end).toLocaleDateString("zh-CN")} + + + + {subscription.auto_renew ? "已开启" : "已关闭"} + + + + + + + + + )} + + + + ) } -export default Billing -export const Component = Billing +export default BillingPage