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