| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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; |
| |
|