From 06e6486aa369f60ddea5f2004dfc6d823723db47 Mon Sep 17 00:00:00 2001 From: YoungestSongMo <2130460579@qq.com> Date: Mon, 15 Jun 2026 15:08:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=85=E5=80=BC=E9=A1=B5=E9=9D=A2=20?= =?UTF-8?q?+=20=E8=AE=A2=E5=8D=95=E6=98=8E=E7=BB=86=E9=A1=B5=E9=9D=A2=20+?= =?UTF-8?q?=20=E9=A1=B5=E9=9D=A2=E8=B0=83=E5=BA=A6=E5=99=A8=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=20(0.0.20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增页面: - RechargePage — 积分充值表单(预设金额/自定义金额/支付宝/协议勾选) + 支付状态跟踪(form/pending/paid/expired/failed 五阶段) + 订单状态轮询(3s 间隔,终态自动停止) + PAGE_LOCK 事件(待支付时禁止关闭 Modal) - OrdersPage — 订单明细列表(状态筛选 + 分页 + 继续支付) + Segmented 状态 Tab(全部/待支付/已支付/已过期/失败) + 待支付订单"继续支付"按钮 事件总线增强: - 新增 EVENTS.CLOSE_PAGE — 页面内部关闭容器 - 新增 EVENTS.PAGE_LOCK — 锁定/解锁容器关闭(防止操作中断) PageDispatcher 增强: - 注册 RechargePage(/recharge)和 OrdersPage(/orders) - 响应 CLOSE_PAGE / PAGE_LOCK 事件 - Modal/Drawer 锁定态下禁用遮罩关闭 支付 API 模块: - PaymentAPI.createOrder / getOrdersListInfo / getOrderInfo - 完整类型定义(请求/响应体、状态常量、中文映射) 杂项: - 导航栏"充值积分"→"充值"(精简) --- CLAUDE.md | 1 + src/components/shell/PageDispatcher.tsx | 39 +- src/components/shell/navbar/nav-config.ts | 2 +- src/pages/panels/OrdersPage.tsx | 321 +++++++++++++ src/pages/panels/RechargePage.tsx | 524 ++++++++++++++++++++++ src/services/modules/payments.ts | 99 ++++ src/utils/event-bus.ts | 4 + 7 files changed, 984 insertions(+), 6 deletions(-) create mode 100644 src/pages/panels/OrdersPage.tsx create mode 100644 src/pages/panels/RechargePage.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 731dec1..e7e8dd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,4 +22,5 @@ - CLAUDE.md - 永远不要修改CLAUDE.md文档 - 在各个文档中的更新需要保持精简。且职责划分保持严谨,除了架构、项目文档,已完成的事需要从TODO.md文档中移除。 +- 在没有得到修改指令时,不要修改文档文件。 diff --git a/src/components/shell/PageDispatcher.tsx b/src/components/shell/PageDispatcher.tsx index 6fc419b..6f55001 100644 --- a/src/components/shell/PageDispatcher.tsx +++ b/src/components/shell/PageDispatcher.tsx @@ -24,6 +24,8 @@ import {FloatingPanel} from '@/components/ui/FloatingPanel'; import {WechatWorkPage} from '@/pages/panels/WechatWorkPage'; import {AccountInfo} from '@/pages/panels/AccountInfo'; import {BillingInfo} from '@/pages/panels/BillingInfo'; +import {RechargePage} from '@/pages/panels/RechargePage'; +import {OrdersPage} from '@/pages/panels/OrdersPage'; // ---------- 类型 ---------- @@ -81,13 +83,16 @@ const PAGE_MAP: Record = { height: () => Math.min(window.outerHeight * 0.65, 680), }, '/vip': {label: '开会员/激活'}, - '/recharge': {label: '充值积分'}, + '/recharge': {component: RechargePage, label: '充值', width: 520}, '/billing': {label: '账单记录', component: BillingInfo, width: 960, height: 680,}, '/orders': { + component: OrdersPage, label: '订单明细', + width: 820, + height: 640, }, '/projects': {label: '项目管理'}, '/quota': { @@ -126,6 +131,12 @@ function resolveHeight(target: string): number | undefined { export function PageDispatcher() { const [activePanel, setActivePanel] = useState(null); const nextIdRef = useRef(0); + const [locked, setLocked] = useState(false); + + // 关闭面板 + const closePanel = useCallback(() => { + setActivePanel(null); + }, []); // 监听 OPEN_PAGE 事件 useEffect(() => { @@ -149,11 +160,26 @@ export function PageDispatcher() { return () => off(EVENTS.OPEN_PAGE, handler); }, []); - // 关闭面板 - const closePanel = useCallback(() => { - setActivePanel(null); + // 监听 CLOSE_PAGE 事件(允许页面组件内部"取消"按钮关闭容器) + useEffect(() => { + on(EVENTS.CLOSE_PAGE, closePanel); + return () => off(EVENTS.CLOSE_PAGE, closePanel); + }, [closePanel]); + + // 监听 PAGE_LOCK 事件(页面组件操作进行中时禁止关闭容器) + useEffect(() => { + const handler = (payload: unknown) => { + setLocked((payload as { locked: boolean }).locked); + }; + on(EVENTS.PAGE_LOCK, handler); + return () => off(EVENTS.PAGE_LOCK, handler); }, []); + // 面板关闭时重置锁定状态(防止下次打开时仍处于锁定) + useEffect(() => { + if (!activePanel) setLocked(false); + }, [activePanel]); + // 页面组件(useMemo 稳定引用) const PanelPage = useMemo( () => (activePanel ? resolvePage(activePanel.target) : null), @@ -178,7 +204,7 @@ export function PageDispatcher() { onCancel={closePanel} footer={null} destroyOnHidden={true} - mask={{closable: true}} + mask={{closable: !locked}} width={panelWidth ?? defaultModalWidth} > {/* eslint-disable-next-line react-hooks/static-components */} @@ -193,6 +219,9 @@ export function PageDispatcher() { title={activePanel.label} onClose={closePanel} destroyOnHidden={true} + mask={{ + closable:locked + }} size={panelWidth ?? defaultDrawerWidth} > {/* eslint-disable-next-line react-hooks/static-components */} diff --git a/src/components/shell/navbar/nav-config.ts b/src/components/shell/navbar/nav-config.ts index 854fcc5..34226dd 100644 --- a/src/components/shell/navbar/nav-config.ts +++ b/src/components/shell/navbar/nav-config.ts @@ -65,7 +65,7 @@ const personalRight: NavItemConfig[] = [ }, { key: 'recharge-points', - label: '充值积分', + label: '充值', iconSrc: rechargeSvg, action: 'page', target: '/recharge', diff --git a/src/pages/panels/OrdersPage.tsx b/src/pages/panels/OrdersPage.tsx new file mode 100644 index 0000000..3678235 --- /dev/null +++ b/src/pages/panels/OrdersPage.tsx @@ -0,0 +1,321 @@ + + +import { useState, useCallback, useEffect, useRef } from 'react'; +import { + Card, + Table, + Button, + Typography, + Tag, + Segmented, + Result, + App, + theme as antTheme, +} from 'antd'; +import { DollarOutlined, ReloadOutlined } from '@ant-design/icons'; + +import { + PaymentAPI, + PAYMENT_STATUS, + PAYMENT_METHODS, + getPaymentStatusText, + type PaymentStatus, + type OrderListResponseBody, + type OrderInfoResponseBody, +} from '@/services/modules/payments'; +import { centToYuan, formatDate } from '@/utils/display'; + +const { Text } = Typography; + +// ---------- 常量 ---------- + +/** 订单状态轮询间隔(毫秒) */ +// startPolling 实现后将使用此常量 +const POLL_INTERVAL_MS = 3000; + +/** 状态筛选项 */ +const STATUS_FILTERS = [ + { label: '全部', value: 'all' }, + { label: '待支付', value: PAYMENT_STATUS.Pending }, + { label: '已支付', value: PAYMENT_STATUS.Paid }, + { label: '已过期', value: PAYMENT_STATUS.Expired }, + { label: '支付失败', value: PAYMENT_STATUS.Failed }, +]; + +/** 状态 → Tag 颜色映射 */ +const STATUS_TAG_COLOR: Record = { + [PAYMENT_STATUS.Pending]: 'processing', + [PAYMENT_STATUS.Paid]: 'success', + [PAYMENT_STATUS.Expired]: 'default', + [PAYMENT_STATUS.Failed]: 'error', +}; + +/** 支付方式 → 显示文本 */ +function getPaymentChannelLabel(channel: string): string { + if (channel === PAYMENT_METHODS.Alipay) return '支付宝'; + if (channel === PAYMENT_METHODS.WeChat) return '微信支付'; + return channel; +} + +// ---------- 组件 ---------- + +export function OrdersPage() { + const { message } = App.useApp(); + const { token } = antTheme.useToken(); + + // ======== 筛选 & 分页 ======== + + const [statusFilter, setStatusFilter] = useState('all'); + const [pagination, setPagination] = useState({ page: 1, page_size: 10 }); + + // ======== 数据状态 ======== + + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 轮询定时器 Map(orderId → timer) + const pollingRef = useRef>>(new Map()); + // 组件是否已挂载 + const mountedRef = useRef(true); + + // ======== 数据获取 ======== + + const fetchOrders = useCallback(async () => { + setLoading(true); + setError(null); + try { + const params: { page: number; page_size: number; status?: PaymentStatus } = { + page: pagination.page, + page_size: pagination.page_size, + }; + if (statusFilter !== 'all') { + params.status = statusFilter as PaymentStatus; + } + const data = await PaymentAPI.getOrdersListInfo(params); + if (mountedRef.current) { + setOrders(data); + } + } catch (err) { + if (mountedRef.current) { + setError((err as Error)?.message || '加载订单列表失败'); + } + } finally { + if (mountedRef.current) { + setLoading(false); + } + } + }, [pagination, statusFilter]); + + // 筛选或分页变化 → 重新获取 + useEffect(() => { + fetchOrders(); + }, [fetchOrders]); + + // 挂载/卸载管理 + useEffect(() => { + mountedRef.current = true; + const polling = pollingRef.current; + return () => { + mountedRef.current = false; + // 清理所有轮询定时器 + polling.forEach((timer) => clearTimeout(timer)); + polling.clear(); + }; + }, []); + + // ======== 单订单轮询 ======== + + // TODO(human): 实现 startPolling 函数 + // 该函数接收 orderId,对该订单启动轮询(每 POLL_INTERVAL_MS 查一次) + // 轮询到终态时停止,并刷新列表;网络异常时延长间隔重试 + // 需要考虑: + // 1. 取消之前同一订单的轮询(避免重复) + // 2. 组件卸载时清理定时器(通过 mountedRef) + // 3. 轮询结果更新到 orders state 中(局部更新后,终态时调用 fetchOrders 保证一致性) + const startPolling = useCallback((_orderId: string) => { + // TODO(human): 实现此函数(使用 POLL_INTERVAL_MS、message、fetchOrders 等闭包变量) + void POLL_INTERVAL_MS; + void message; + void fetchOrders; + void ({} as OrderInfoResponseBody); + }, [message, fetchOrders]); + + // ======== 操作回调 ======== + + /** "继续支付":打开支付页面 + 启动轮询 */ + const handleContinuePay = useCallback( + (order: OrderListResponseBody) => { + window.open(order.pay_url, '_blank'); + startPolling(order.id); + message.info('已打开支付页面,请完成支付'); + }, + [startPolling, message], + ); + + // ======== 表格列定义 ======== + + const columns = [ + { + title: '订单编号', + dataIndex: 'id', + key: 'id', + width: 200, + ellipsis: true, + render: (id: string) => ( + + {id} + + ), + }, + { + title: '金额', + dataIndex: 'amount_cent', + key: 'amount_cent', + width: 90, + align: 'center' as const, + render: (amount: number) => ( + + ¥{centToYuan(amount)} + + ), + }, + { + title: '支付方式', + dataIndex: 'payment_channel', + key: 'payment_channel', + width: 90, + align: 'center' as const, + render: (channel: string) => ( + {getPaymentChannelLabel(channel)} + ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 90, + align: 'center' as const, + render: (status: PaymentStatus) => ( + + {getPaymentStatusText(status)} + + ), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + width: 120, + align: 'center' as const, + render: (date: Date) => ( + {formatDate(date)} + ), + }, + { + title: '操作', + key: 'action', + width: 110, + align: 'center' as const, + render: (_: unknown, record: OrderListResponseBody) => { + if (record.status === PAYMENT_STATUS.Pending && record.pay_url) { + return ( + + ); + } + return ( + + — + + ); + }, + }, + ]; + + // ============================================================ + // 渲染:错误态(无数据时) + // ============================================================ + + if (error && orders.length === 0) { + return ( +
+ + + 重试 + + } + /> + +
+ ); + } + + // ============================================================ + // 渲染:正常 + // ============================================================ + + return ( +
+
+ {/* ---- 状态筛选 ---- */} + { + setStatusFilter(val as string); + setPagination((prev) => ({ ...prev, page: 1 })); + }} + block + style={{ backgroundColor: token.colorFillSecondary }} + /> + + {/* ---- 手动刷新 ---- */} +
+ +
+ + {/* ---- 订单表格 ---- */} + + + columns={columns} + dataSource={orders} + rowKey="id" + loading={loading} + size="small" + pagination={{ + current: pagination.page, + pageSize: pagination.page_size, + total: + orders.length >= pagination.page_size + ? pagination.page * pagination.page_size + 1 + : pagination.page * pagination.page_size, + showSizeChanger: true, + pageSizeOptions: ['10', '20', '50'], + showTotal: (total: number, range: [number, number]) => + `${range[0]}-${range[1]},共 ${total}+ 条`, + onChange: (page: number, pageSize: number) => { + setPagination({ page, page_size: pageSize }); + }, + position: ['bottomCenter'], + }} + locale={{ emptyText: '暂无订单记录' }} + /> + +
+
+ ); +} diff --git a/src/pages/panels/RechargePage.tsx b/src/pages/panels/RechargePage.tsx new file mode 100644 index 0000000..0de3487 --- /dev/null +++ b/src/pages/panels/RechargePage.tsx @@ -0,0 +1,524 @@ +// ============================================================ +// RechargePage — 积分充值表单 + 支付状态跟踪 +// +// 通过 PageDispatcher 调度(Modal 容器),导航栏"充值"按钮触发。 +// 两阶段视图: +// form — 金额选择 → 自定义输入 → 支付方式 → 协议 → 提交 +// pending — 订单已创建,轮询支付状态,可重新打开支付页面 +// paid — 支付成功 +// expired — 订单过期,可重新下单 +// failed — 支付失败,可重新下单 +// ============================================================ + +import { useState, useMemo, useCallback, useEffect } from 'react'; +import { + Radio, + InputNumber, + Button, + Checkbox, + Typography, + App, + Space, + Divider, + theme as antTheme, + Result, +} from 'antd'; +import { + AlipayOutlined, + WechatOutlined, + WalletOutlined, + DollarOutlined, +} from '@ant-design/icons'; + +import { useTheme } from '@/components/providers/ThemeProvider'; +import { LegalTextModal } from '@/components/ui/LegalTextModal'; +import { + PaymentAPI, + PAYMENT_METHODS, + PAYMENT_STATUS, + getPaymentStatusText, + type PaymentStatus, +} from '@/services/modules/payments'; +import { emit, EVENTS } from '@/utils/event-bus'; + +const { Text } = Typography; + +// ---------- 常量 ---------- + +const PRESET_AMOUNTS = [10, 100, 500, 1000, 5000, 10000] as const; + +/** 单次充值上限(元) */ +const MAX_AMOUNT_YUAN = 100000; + +/** 订单状态轮询间隔(毫秒) */ +const POLL_INTERVAL_MS = 3000; + +/** 页面阶段 */ +type RechargePhase = 'form' | PaymentStatus; + +// ---------- 组件 ---------- + +export function RechargePage() { + const { isDark } = useTheme(); + const { message } = App.useApp(); + const { token } = antTheme.useToken(); + + // ======== 表单状态 ======== + + const [selectedAmount, setSelectedAmount] = useState(null); + const [customAmount, setCustomAmount] = useState(null); + const [paymentMethod, setPaymentMethod] = useState(PAYMENT_METHODS.Alipay); + const [agreedToTerms, setAgreedToTerms] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [errors, setErrors] = useState>({}); + const [legalOpen, setLegalOpen] = useState(false); + + // ======== 订单状态 ======== + + const [phase, setPhase] = useState('form'); + const [orderId, setOrderId] = useState(null); + const [orderStatus, setOrderStatus] = useState(null); + const [payUrl, setPayUrl] = useState(null); + + // ======== 派生值 ======== + + /** 最终充值金额(元):自定义金额优先,否则预设金额,否则 0 */ + const finalAmountYuan = useMemo(() => { + if (customAmount !== null && customAmount !== undefined) { + return customAmount; + } + return selectedAmount ?? 0; + }, [customAmount, selectedAmount]); + + /** 最终充值金额(分):元 × 100,用于 API 调用 */ + const finalAmountCent = finalAmountYuan * 100; + + // ======== 工具函数 ======== + + const clearError = useCallback((name: string) => { + setErrors((prev) => { + if (!(name in prev)) return prev; + const next = { ...prev }; + delete next[name]; + return next; + }); + }, []); + + // ======== 校验 ======== + + const validate = useCallback((): Record => { + const errs: Record = {}; + + if (!finalAmountYuan || finalAmountYuan <= 0) { + errs.amount = '请选择或输入充值金额'; + } else if (!Number.isInteger(finalAmountYuan) || finalAmountYuan % 10 !== 0) { + errs.customAmount = '金额必须为 10 的倍数'; + } else if (finalAmountYuan > MAX_AMOUNT_YUAN) { + errs.customAmount = `单次充值上限为 ${MAX_AMOUNT_YUAN.toLocaleString()} 元`; + } + + if (!agreedToTerms) { + errs.agreedToTerms = '请阅读并同意充值协议'; + } + + return errs; + }, [finalAmountYuan, agreedToTerms]); + + // ======== 支付方式切换 ======== + + const handlePaymentChange = useCallback( + (value: string) => { + if (value === PAYMENT_METHODS.WeChat) { + void message.info('微信支付正在接入中,敬请期待'); + return; // 不更新 state,保持支付宝选中 + } + setPaymentMethod(value); + }, + [message], + ); + + // ======== 提交流程 ======== + + const handleSubmit = useCallback(async () => { + const validationErrors = validate(); + if (Object.keys(validationErrors).length > 0) { + setErrors(validationErrors); + return; + } + + setSubmitting(true); + try { + const res = await PaymentAPI.createOrder({ + amount_cent: finalAmountCent, + payment_channel: PAYMENT_METHODS.Alipay, + }); + + // 保存订单信息,进入等待支付阶段 + setOrderId(res.id); + setOrderStatus(PAYMENT_STATUS.Pending); + setPayUrl(res.pay_url); + setPhase(PAYMENT_STATUS.Pending); + + window.open(res.pay_url, '_blank'); + void message.success('订单已创建,请在新页面中完成支付'); + } catch (err: unknown) { + message.error((err as Error)?.message || '创建订单失败,请重试'); + } finally { + setSubmitting(false); + } + }, [finalAmountCent, validate, message]); + + // ======== 订单状态轮询 ======== + + useEffect(() => { + if (phase !== PAYMENT_STATUS.Pending || !orderId) return; + + let timer: ReturnType; + let cancelled = false; + + const poll = async () => { + try { + const order = await PaymentAPI.getOrderInfo({ order_id: orderId }); + if (cancelled) return; + + const newStatus = order.status; + setOrderStatus(newStatus); + + if (newStatus === PAYMENT_STATUS.Pending) { + // 仍待支付 → 继续轮询 + timer = setTimeout(poll, POLL_INTERVAL_MS); + } else { + // 终态 → 停止轮询,切换阶段 + setPhase(newStatus); + if (newStatus === PAYMENT_STATUS.Paid) { + void message.success('支付成功,积分已到账'); + } + } + } catch { + // 网络异常 → 延长间隔后继续轮询 + if (!cancelled) timer = setTimeout(poll, POLL_INTERVAL_MS * 2); + } + }; + + timer = setTimeout(poll, POLL_INTERVAL_MS); + return () => { + cancelled = true; + clearTimeout(timer); + }; + }, [phase, orderId, message]); + + // ======== 页面锁定(待支付时禁止关闭容器) ======== + + useEffect(() => { + const locked = phase === PAYMENT_STATUS.Pending; + emit(EVENTS.PAGE_LOCK, { locked }); + // 组件卸载时确保解锁(防止锁定残留) + return () => { + if (locked) emit(EVENTS.PAGE_LOCK, { locked: false }); + }; + }, [phase]); + + // ======== 操作回调 ======== + + /** 重新下单:回到表单,保留之前填写的金额 */ + const handleRetry = useCallback(() => { + setPhase('form'); + setOrderId(null); + setOrderStatus(null); + setPayUrl(null); + }, []); + + /** 关闭充值页面 */ + const handleClose = useCallback(() => { + emit(EVENTS.CLOSE_PAGE); + }, []); + + // ================================================================ + // 渲染:支付状态视图(非 form 阶段) + // ================================================================ + + if (phase !== 'form') { + const statusText = orderStatus ? getPaymentStatusText(orderStatus) : ''; + const isPending = phase === PAYMENT_STATUS.Pending; + const isPaid = phase === PAYMENT_STATUS.Paid; + const isTerminal = !isPending; + + return ( + <> +
+ + {/* ---- 订单信息卡片 ---- */} + {isPending && ( +
+ + + 订单编号:{orderId} + + + + 充值金额: + ¥{finalAmountYuan.toLocaleString()} 元 + + + + 到账积分: + {finalAmountYuan.toLocaleString()} 积分 + + +
+ )} + + {/* ---- 操作按钮 ---- */} + + {isPending && payUrl && ( + + )} + {isTerminal && !isPaid && ( + + )} + + +
+
+ + {/* ---- 协议弹窗(待支付阶段仍可查看) ---- */} + setLegalOpen(false)} + /> + + ); + } + + // ================================================================ + // 渲染:表单视图(form 阶段) + // ================================================================ + + return ( + <> +
+ {/* ---- 页面副标题 ---- */} + + 选择充值金额或输入自定义金额(元) + + + {/* ---- Part 1:预设金额单选组 ---- */} +
+ { + setSelectedAmount(e.target.value); + setCustomAmount(null); + clearError('amount'); + }} + optionType="button" + buttonStyle="solid" + style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }} + > + {PRESET_AMOUNTS.map((amt) => ( + + {amt} 元 + + ))} + + {errors.amount && ( + + {errors.amount} + + )} +
+ + {/* ---- Part 2:自定义金额输入 ---- */} +
+ + { + setCustomAmount(val); + setSelectedAmount(null); + clearError('customAmount'); + }} + min={10} + step={10} + max={MAX_AMOUNT_YUAN} + placeholder="输入自定义金额(10的倍数)" + style={{ flex: 1 }} + size="large" + precision={0} + /> + + + {errors.customAmount && ( + + {errors.customAmount} + + )} +
+ + {/* ---- Part 3:到账信息 + 支付金额 ---- */} + {finalAmountYuan > 0 && ( +
+ + + + 到账积分: + {finalAmountYuan.toLocaleString()} 积分 + + + + 支付金额: + + ¥{finalAmountYuan.toLocaleString()} + + {' '}元 + + +
+ )} + + + + {/* ---- Part 4:支付方式 ---- */} +
+ + 选择支付方式 + + handlePaymentChange(e.target.value)} + optionType="button" + buttonStyle="solid" + > + + + + 支付宝 + + + + + + 微信支付 + + + +
+ + + + {/* ---- Part 5:充值协议 ---- */} +
+ { + setAgreedToTerms(e.target.checked); + clearError('agreedToTerms'); + }} + > + + 我已阅读并同意 + + + + {errors.agreedToTerms && ( + + {errors.agreedToTerms} + + )} +
+ + {/* ---- Part 6:操作按钮 ---- */} +
+ + +
+
+ + {/* ---- 协议弹窗 ---- */} + setLegalOpen(false)} + /> + + ); +} diff --git a/src/services/modules/payments.ts b/src/services/modules/payments.ts index e69de29..ff29dae 100644 --- a/src/services/modules/payments.ts +++ b/src/services/modules/payments.ts @@ -0,0 +1,99 @@ +import { get, post } from '@/services/request'; + +export const PAYMENT_METHODS = { + Alipay: 'alipay', + WeChat: 'wechat', + Mock: "mock" +} as const; + +export type PaymentMethod = typeof PAYMENT_METHODS[keyof typeof PAYMENT_METHODS]; + +export type AvailablePaymentMethod = typeof PAYMENT_METHODS.Alipay; + +export interface CreateOrderRequestBody { + amount_cent: number, + payment_channel: AvailablePaymentMethod +} + +export const PAYMENT_STATUS = { + Pending: 'pending', // 待支付 + Paid: 'paid', // 已支付 + Expired: 'expired', // 已过期 + Failed: 'failed', // 支付失败 +} as const; + +export type PaymentStatus = typeof PAYMENT_STATUS[keyof typeof PAYMENT_STATUS]; +// 中文映射表 +export const PAYMENT_STATUS_TEXT_MAP: Record = { + [PAYMENT_STATUS.Pending]: '待支付', + [PAYMENT_STATUS.Paid]: '已支付', + [PAYMENT_STATUS.Expired]: '已过期', + [PAYMENT_STATUS.Failed]: '支付失败', +}; + +// 辅助函数:根据状态获取中文 +export function getPaymentStatusText(status: PaymentStatus): string { + return PAYMENT_STATUS_TEXT_MAP[status]; +} + +export interface CreateOrderResponseBody { + id: string, + user_id: string, + amount_cent: number, + status: PaymentStatus, + payment_channel: AvailablePaymentMethod, + out_trade_no: string, + transaction_id: string, + pay_url: string, + expires_at: Date, + paid_at: Date, + created_at: Date, +} + +export interface OrderListRequestParam { + status?: PaymentStatus, + page: number, + page_size: number, +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface OrderListResponseBody extends CreateOrderResponseBody { + +} + +export interface OrderInfoRequestParam { + order_id: string, +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface OrderInfoResponseBody extends CreateOrderResponseBody { + +} + +const PaymentUrlObj = { + createOrder: "/api/v1/payments/orders", + getOrderList: "/api/v1/payments/orders", + getOrderInfo: "/api/v1/payments/orders/{order_id}", +}; + +// ============================================================ +// 调用方式:PaymentAPI.createOrder() / PaymentAPI.getOrdersListInfo() 等 +// ============================================================ + +export class PaymentAPI { + /** 创建支付订单 */ + static async createOrder(body: CreateOrderRequestBody): Promise { + return await post(PaymentUrlObj.createOrder, body); + } + + /** 获取订单列表(支持按状态筛选 + 分页) */ + static async getOrdersListInfo(param: OrderListRequestParam, signal?: AbortSignal): Promise { + return await get(PaymentUrlObj.getOrderList, param as unknown as Record, { signal }); + } + + /** 获取单个订单详情 */ + static async getOrderInfo(param: OrderInfoRequestParam): Promise { + const url = PaymentUrlObj.getOrderInfo.replace('{order_id}', param.order_id); + return await get(url); + } +} diff --git a/src/utils/event-bus.ts b/src/utils/event-bus.ts index 553921d..507daa9 100644 --- a/src/utils/event-bus.ts +++ b/src/utils/event-bus.ts @@ -75,4 +75,8 @@ export const EVENTS = { OPEN_SETTINGS: 'settings:open', /** 打开页面 Modal(导航栏点击页面导航项触发,携带 target 路径标识符) */ OPEN_PAGE: 'page:open', + /** 关闭当前页面(由页面组件内部按钮触发,PageDispatcher 响应) */ + CLOSE_PAGE: 'page:close', + /** 锁定/解锁页面关闭(页面组件内部操作进行中时禁止关闭容器,PageDispatcher 响应) */ + PAGE_LOCK: 'page:lock', } as const;