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