| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import React, { useState, useEffect, useMemo } from 'react'; |
| import { |
| Modal, |
| Table, |
| Badge, |
| Typography, |
| Toast, |
| Empty, |
| Button, |
| Input, |
| } from '@douyinfe/semi-ui'; |
| import { |
| IllustrationNoResult, |
| IllustrationNoResultDark, |
| } from '@douyinfe/semi-illustrations'; |
| import { Coins } from 'lucide-react'; |
| import { IconSearch } from '@douyinfe/semi-icons'; |
| import { API, timestamp2string } from '../../../helpers'; |
| import { isAdmin } from '../../../helpers/utils'; |
| import { useIsMobile } from '../../../hooks/common/useIsMobile'; |
|
|
| const { Text } = Typography; |
|
|
| |
| const STATUS_CONFIG = { |
| success: { type: 'success', key: '成功' }, |
| pending: { type: 'warning', key: '待支付' }, |
| expired: { type: 'danger', key: '已过期' }, |
| }; |
|
|
| |
| const PAYMENT_METHOD_MAP = { |
| stripe: 'Stripe', |
| alipay: '支付宝', |
| wxpay: '微信', |
| }; |
|
|
| const TopupHistoryModal = ({ visible, onCancel, t }) => { |
| const [loading, setLoading] = useState(false); |
| const [topups, setTopups] = useState([]); |
| const [total, setTotal] = useState(0); |
| const [page, setPage] = useState(1); |
| const [pageSize, setPageSize] = useState(10); |
| const [keyword, setKeyword] = useState(''); |
|
|
| const isMobile = useIsMobile(); |
|
|
| const loadTopups = async (currentPage, currentPageSize) => { |
| setLoading(true); |
| try { |
| const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self'; |
| const qs = |
| `p=${currentPage}&page_size=${currentPageSize}` + |
| (keyword ? `&keyword=${encodeURIComponent(keyword)}` : ''); |
| const endpoint = `${base}?${qs}`; |
| const res = await API.get(endpoint); |
| const { success, message, data } = res.data; |
| if (success) { |
| setTopups(data.items || []); |
| setTotal(data.total || 0); |
| } else { |
| Toast.error({ content: message || t('加载失败') }); |
| } |
| } catch (error) { |
| console.error('Load topups error:', error); |
| Toast.error({ content: t('加载账单失败') }); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| useEffect(() => { |
| if (visible) { |
| loadTopups(page, pageSize); |
| } |
| }, [visible, page, pageSize, keyword]); |
|
|
| const handlePageChange = (currentPage) => { |
| setPage(currentPage); |
| }; |
|
|
| const handlePageSizeChange = (currentPageSize) => { |
| setPageSize(currentPageSize); |
| setPage(1); |
| }; |
|
|
| const handleKeywordChange = (value) => { |
| setKeyword(value); |
| setPage(1); |
| }; |
|
|
| |
| const handleAdminComplete = async (tradeNo) => { |
| try { |
| const res = await API.post('/api/user/topup/complete', { |
| trade_no: tradeNo, |
| }); |
| const { success, message } = res.data; |
| if (success) { |
| Toast.success({ content: t('补单成功') }); |
| await loadTopups(page, pageSize); |
| } else { |
| Toast.error({ content: message || t('补单失败') }); |
| } |
| } catch (e) { |
| Toast.error({ content: t('补单失败') }); |
| } |
| }; |
|
|
| const confirmAdminComplete = (tradeNo) => { |
| Modal.confirm({ |
| title: t('确认补单'), |
| content: t('是否将该订单标记为成功并为用户入账?'), |
| onOk: () => handleAdminComplete(tradeNo), |
| }); |
| }; |
|
|
| |
| const renderStatusBadge = (status) => { |
| const config = STATUS_CONFIG[status] || { type: 'primary', key: status }; |
| return ( |
| <span className='flex items-center gap-2'> |
| <Badge dot type={config.type} /> |
| <span>{t(config.key)}</span> |
| </span> |
| ); |
| }; |
|
|
| |
| const renderPaymentMethod = (pm) => { |
| const displayName = PAYMENT_METHOD_MAP[pm]; |
| return <Text>{displayName ? t(displayName) : pm || '-'}</Text>; |
| }; |
|
|
| |
| const userIsAdmin = useMemo(() => isAdmin(), []); |
|
|
| const columns = useMemo(() => { |
| const baseColumns = [ |
| { |
| title: t('订单号'), |
| dataIndex: 'trade_no', |
| key: 'trade_no', |
| render: (text) => <Text copyable>{text}</Text>, |
| }, |
| { |
| title: t('支付方式'), |
| dataIndex: 'payment_method', |
| key: 'payment_method', |
| render: renderPaymentMethod, |
| }, |
| { |
| title: t('充值额度'), |
| dataIndex: 'amount', |
| key: 'amount', |
| render: (amount) => ( |
| <span className='flex items-center gap-1'> |
| <Coins size={16} /> |
| <Text>{amount}</Text> |
| </span> |
| ), |
| }, |
| { |
| title: t('支付金额'), |
| dataIndex: 'money', |
| key: 'money', |
| render: (money) => <Text type='danger'>¥{money.toFixed(2)}</Text>, |
| }, |
| { |
| title: t('状态'), |
| dataIndex: 'status', |
| key: 'status', |
| render: renderStatusBadge, |
| }, |
| ]; |
|
|
| |
| if (userIsAdmin) { |
| baseColumns.push({ |
| title: t('操作'), |
| key: 'action', |
| render: (_, record) => { |
| if (record.status !== 'pending') return null; |
| return ( |
| <Button |
| size='small' |
| type='primary' |
| theme='outline' |
| onClick={() => confirmAdminComplete(record.trade_no)} |
| > |
| {t('补单')} |
| </Button> |
| ); |
| }, |
| }); |
| } |
|
|
| baseColumns.push({ |
| title: t('创建时间'), |
| dataIndex: 'create_time', |
| key: 'create_time', |
| render: (time) => timestamp2string(time), |
| }); |
|
|
| return baseColumns; |
| }, [t, userIsAdmin]); |
|
|
| return ( |
| <Modal |
| title={t('充值账单')} |
| visible={visible} |
| onCancel={onCancel} |
| footer={null} |
| size={isMobile ? 'full-width' : 'large'} |
| > |
| <div className='mb-3'> |
| <Input |
| prefix={<IconSearch />} |
| placeholder={t('订单号')} |
| value={keyword} |
| onChange={handleKeywordChange} |
| showClear |
| /> |
| </div> |
| <Table |
| columns={columns} |
| dataSource={topups} |
| loading={loading} |
| rowKey='id' |
| pagination={{ |
| currentPage: page, |
| pageSize: pageSize, |
| total: total, |
| showSizeChanger: true, |
| pageSizeOpts: [10, 20, 50, 100], |
| onPageChange: handlePageChange, |
| onPageSizeChange: handlePageSizeChange, |
| }} |
| size='small' |
| empty={ |
| <Empty |
| image={<IllustrationNoResult style={{ width: 150, height: 150 }} />} |
| darkModeImage={ |
| <IllustrationNoResultDark style={{ width: 150, height: 150 }} /> |
| } |
| description={t('暂无充值记录')} |
| style={{ padding: 30 }} |
| /> |
| } |
| /> |
| </Modal> |
| ); |
| }; |
|
|
| export default TopupHistoryModal; |
|
|