Files
video-gen/video-gen-admin/src/pages/AdminPaymentStats.tsx
T
2026-08-14 15:23:46 +08:00

75 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Card, Col, ConfigProvider, DatePicker, Input, Row, Select, Space, Statistic, Table, Tag, Typography } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import { DollarOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { getAdminPaymentOrders, getPaymentStats } from '../api';
const { RangePicker } = DatePicker;
const SOURCE_LABELS: Record<string, string> = { online_payment: '线上支付', admin_offline: '后台线下成交' };
const METHOD_LABELS: Record<string, string> = { alipay: '支付宝', wechat: '微信支付', bank_transfer: '银行转账', cash: '现金', other: '其他-线下收款' };
const STATUS_LABELS: Record<string, string> = { pending: '待支付', paid: '已支付', refunded: '已退款', cancelled: '已取消', expired: '已过期', failed: '失败' };
const AdminPaymentStats: React.FC = () => {
const [stats, setStats] = useState<any>(null);
const [orders, setOrders] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [filters, setFilters] = useState<any>({ paymentMethod: undefined, orderSource: undefined, status: undefined, phone: '', startDate: dayjs().startOf('month').format('YYYY-MM-DD'), endDate: dayjs().format('YYYY-MM-DD') });
const load = useCallback(async () => {
setLoading(true);
try {
const [s, o] = await Promise.all([
getPaymentStats({ paymentMethod: filters.paymentMethod, orderSource: filters.orderSource, status: filters.status, startDate: filters.startDate, endDate: filters.endDate }),
getAdminPaymentOrders({ method: filters.paymentMethod, orderSource: filters.orderSource, status: filters.status, phone: filters.phone || undefined, startDate: filters.startDate, endDate: filters.endDate, page, pageSize }),
]);
setStats(s); setOrders(o.items || []); setTotal(Number(o.total || 0));
} finally { setLoading(false); }
}, [filters, page, pageSize]);
useEffect(() => { void load(); }, [load]);
const columns = [
{ title: '订单号', dataIndex: 'orderNo', width: 210, render: (v: string) => <Typography.Text copyable code>{v}</Typography.Text> },
{ title: '用户', width: 150, render: (_: any, r: any) => <div>{r.username || '-'}<div style={{ fontSize: 12, color: '#94a3b8' }}>{r.phone || '-'}</div></div> },
{ title: '来源', dataIndex: 'orderSource', width: 120, render: (v: string, r: any) => <Tag color={v === 'admin_offline' ? 'purple' : 'blue'}>{r.orderSourceLabel || SOURCE_LABELS[v] || '其他来源'}</Tag> },
{ title: '商品', dataIndex: 'productNameSnapshot', width: 170, ellipsis: true, render: (v: string, r: any) => `${v || '-'}${Number(r.quantity || 1) > 1 ? ` × ${r.quantity}` : ''}` },
{ title: '支付方式', dataIndex: 'paymentMethod', width: 135, render: (v: string, r: any) => r.paymentMethodLabel || METHOD_LABELS[v] || '其他支付方式' },
{ title: '系统报价', dataIndex: 'quotedAmountSnapshot', width: 110, align: 'right' as const, render: (v: number) => v == null ? '-' : ${Number(v).toFixed(2)}` },
{ title: '实际成交', dataIndex: 'amount', width: 110, align: 'right' as const, render: (v: number) => <b>¥{Number(v || 0).toFixed(2)}</b> },
{ title: '状态', dataIndex: 'status', width: 100, render: (v: string, r: any) => <Tag>{r.statusLabel || STATUS_LABELS[v] || '其他状态'}</Tag> },
{ title: '履约', dataIndex: 'fulfillmentStatusLabel', width: 100, render: (v: string) => v || '-' },
{ title: '创建时间', dataIndex: 'createdAt', width: 180, render: (v: string) => v ? new Date(v).toLocaleString('zh-CN', { hour12: false }) : '-' },
// 当前版本退款按钮明确不开放。后端退款 API 仍保留兼容入口并固定返回中文阻止提示,后续重新开放时再恢复此处按钮。
];
const reset = () => { setFilters({ paymentMethod: undefined, orderSource: undefined, status: undefined, phone: '', startDate: dayjs().startOf('month').format('YYYY-MM-DD'), endDate: dayjs().format('YYYY-MM-DD') }); setPage(1); };
const today = stats?.today || {};
const month = stats?.month || {};
const bySource = stats?.bySource || {};
return <ConfigProvider locale={zhCN}><div>
<Row gutter={[16,16]} style={{ marginBottom: 18 }}>
<Col xs={24} md={8}><Card><Statistic title="今日总真实收入" value={Number(today.paidAmount || 0)} precision={2} suffix="元" prefix={<DollarOutlined />} /><Typography.Text type="secondary">线上 {Number(today.onlinePaidAmount || 0).toFixed(2)} · 线下 {Number(today.offlinePaidAmount || 0).toFixed(2)} </Typography.Text></Card></Col>
<Col xs={24} md={8}><Card><Statistic title="本月总真实收入" value={Number(month.paidAmount || 0)} precision={2} suffix="元" prefix={<DollarOutlined />} /><Typography.Text type="secondary">线上 {Number(month.onlinePaidAmount || 0).toFixed(2)} · 线下 {Number(month.offlinePaidAmount || 0).toFixed(2)} </Typography.Text></Card></Col>
<Col xs={24} md={8}><Card><Statistic title="当前筛选总收入" value={Number(stats?.totalIncome?.amount || 0)} precision={2} suffix="元" prefix={<DollarOutlined />} /><Typography.Text type="secondary">{Number(stats?.totalIncome?.count || 0)} 笔已支付成交</Typography.Text></Card></Col>
</Row>
<Card title="收入来源" style={{ marginBottom: 16 }}><Row gutter={16}>{['online_payment','admin_offline'].map((source) => <Col span={12} key={source}><div style={{ padding: 14, background: '#fafafa', borderRadius: 10 }}><Tag color={source === 'online_payment' ? 'blue' : 'purple'}>{bySource[source]?.label || SOURCE_LABELS[source]}</Tag><div style={{ fontSize: 24, fontWeight: 700, marginTop: 8 }}>¥{Number(bySource[source]?.amount || 0).toFixed(2)}</div><div style={{ color: '#94a3b8' }}>{Number(bySource[source]?.count || 0)} </div></div></Col>)}</Row></Card>
<Card title="订单列表">
<Space wrap style={{ marginBottom: 16 }}>
<Select allowClear placeholder="订单来源" style={{ width: 160 }} value={filters.orderSource} onChange={(v) => { setFilters((x:any) => ({...x, orderSource:v})); setPage(1); }} options={Object.entries(SOURCE_LABELS).map(([value,label]) => ({value,label}))} />
<Select allowClear placeholder="支付方式" style={{ width: 150 }} value={filters.paymentMethod} onChange={(v) => { setFilters((x:any) => ({...x, paymentMethod:v})); setPage(1); }} options={Object.entries(METHOD_LABELS).map(([value,label]) => ({value,label}))} />
<Select allowClear placeholder="订单状态" style={{ width: 130 }} value={filters.status} onChange={(v) => { setFilters((x:any) => ({...x, status:v})); setPage(1); }} options={Object.entries(STATUS_LABELS).map(([value,label]) => ({value,label}))} />
<Input allowClear placeholder="手机号" style={{ width: 150 }} value={filters.phone} onChange={(e) => setFilters((x:any) => ({...x, phone:e.target.value}))} onPressEnter={() => { setPage(1); void load(); }} suffix={<SearchOutlined />} />
<RangePicker value={[dayjs(filters.startDate), dayjs(filters.endDate)]} onChange={(v) => { setFilters((x:any) => ({...x, startDate:v?.[0]?.format('YYYY-MM-DD'), endDate:v?.[1]?.format('YYYY-MM-DD')})); setPage(1); }} />
<Button icon={<ReloadOutlined />} onClick={reset}>重置</Button>
</Space>
<Table rowKey="id" loading={loading} columns={columns} dataSource={orders} scroll={{ x: 1450 }} pagination={{ current:page, pageSize, total, showSizeChanger:true, showTotal:(t)=>`共 ${t} 条`, onChange:(p,ps)=>{setPage(p);setPageSize(ps);} }} />
</Card>
</div></ConfigProvider>;
};
export default AdminPaymentStats;