| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- /*
- Copyright (C) 2025 QuantumNous
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
- For commercial licensing, please contact support@quantumnous.com
- */
- 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;
|