增加支付管理页面的搜索和逻辑
This commit is contained in:
@@ -211,12 +211,25 @@ export async function batchUpdatePaymentConfigs(configs: Record<string, string>)
|
||||
await api.put('/admin/payment-configs/batch', configs);
|
||||
}
|
||||
|
||||
export async function getPaymentStats(): Promise<{
|
||||
byStatus: Record<string, { count: number; amount: number }>;
|
||||
today: { paidCount: number; paidAmount: number };
|
||||
recent: any[];
|
||||
export async function getPaymentStats(params?: {
|
||||
paymentMethod?: string;
|
||||
status?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}): Promise<{
|
||||
byStatus: Record<string, { count: number; amount: number }>;
|
||||
today: { paidCount: number; paidAmount: number };
|
||||
month: { paidCount: number; paidAmount: number };
|
||||
recent: any[];
|
||||
}> {
|
||||
return api.get('/admin/payment-stats');
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.paymentMethod) searchParams.set('payment_method', params.paymentMethod);
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
if (params?.startDate) searchParams.set('start_date', params.startDate);
|
||||
if (params?.endDate) searchParams.set('end_date', params.endDate);
|
||||
const queryString = searchParams.toString();
|
||||
const url = queryString ? `/admin/payment-stats?${queryString}` : '/admin/payment-stats';
|
||||
return api.get(url);
|
||||
}
|
||||
|
||||
export async function getAdminPaymentOrders(params?: { method?: string; status?: string }): Promise<{ items: any[] }> {
|
||||
|
||||
@@ -1,193 +1,272 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Card, Col, Row, Space, Table, Tag, Typography, Statistic, message,
|
||||
Card, Col, Row, Space, Table, Tag, Typography, Statistic, message, Select, DatePicker, Button
|
||||
} from 'antd';
|
||||
import {
|
||||
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined,
|
||||
DollarOutlined, CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined, ReloadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { getPaymentStats } from '../api';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const AdminPaymentStats: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [filters, setFilters] = useState<{
|
||||
paymentMethod?: string;
|
||||
status?: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}>({
|
||||
startDate: dayjs().format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getPaymentStats();
|
||||
setStats(data);
|
||||
} catch {
|
||||
message.error('加载支付统计失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
const load = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await getPaymentStats(filters);
|
||||
setStats(data);
|
||||
} catch {
|
||||
message.error('加载支付统计失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, [filters]);
|
||||
|
||||
const handleReset = () => {
|
||||
setFilters({
|
||||
startDate: dayjs().format('YYYY-MM-DD'),
|
||||
endDate: dayjs().format('YYYY-MM-DD'),
|
||||
});
|
||||
};
|
||||
|
||||
const handleDateChange = (dates: any) => {
|
||||
if (dates && dates.length === 2) {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
startDate: dates[0].format('YYYY-MM-DD'),
|
||||
endDate: dates[1].format('YYYY-MM-DD'),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const statusConfig: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
|
||||
paid: { color: 'green', label: '已支付', icon: <CheckCircleOutlined /> },
|
||||
pending: { color: 'gold', label: '待支付', icon: <ClockCircleOutlined /> },
|
||||
cancelled: { color: 'default', label: '已取消', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
|
||||
const methodConfig: Record<string, { color: string; label: string }> = {
|
||||
alipay: { color: 'blue', label: '支付宝' },
|
||||
wechat: { color: 'green', label: '微信' },
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo', key: 'orderNo', width: 200 },
|
||||
{
|
||||
title: '支付方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 100,
|
||||
render: (m: string) => {
|
||||
const c = methodConfig[m] || { color: 'default', label: m };
|
||||
return <Tag color={c.color}>{c.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '金额', dataIndex: 'amount', key: 'amount', width: 100,
|
||||
render: (a: number) => <Typography.Text strong style={{ color: '#10b981' }}>¥{a.toFixed(2)}</Typography.Text>,
|
||||
},
|
||||
{ title: '积分', dataIndex: 'credits', key: 'credits', width: 80 },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 100,
|
||||
render: (s: string) => {
|
||||
const c = statusConfig[s] || { color: 'default', label: s, icon: null };
|
||||
return <Tag color={c.color} icon={c.icon}>{c.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '支付宝交易号', dataIndex: 'tradeNo', key: 'tradeNo', width: 200, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160,
|
||||
render: (d: string) => <span className="date-display" style={{ color: '#94a3b8' }}>{d ? formatDate(d) : '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '支付时间', dataIndex: 'paidAt', key: 'paidAt', width: 160,
|
||||
render: (d: string) => <span className="date-display" style={{ color: '#94a3b8' }}>{d ? formatDate(d) : '-'}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
if (!stats) {
|
||||
return <div style={{ padding: 24, color: '#94a3b8' }}>加载中…</div>;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
const paidInfo = stats.byStatus?.paid || { count: 0, amount: 0 };
|
||||
const pendingInfo = stats.byStatus?.pending || { count: 0, amount: 0 };
|
||||
const cancelledInfo = stats.byStatus?.cancelled || { count: 0, amount: 0 };
|
||||
const totalOrders = paidInfo.count + pendingInfo.count + cancelledInfo.count;
|
||||
const monthInfo = stats.month || { count: 0, amount: 0 };
|
||||
|
||||
const statusConfig: Record<string, { color: string; label: string; icon: React.ReactNode }> = {
|
||||
paid: { color: 'green', label: '已支付', icon: <CheckCircleOutlined /> },
|
||||
pending: { color: 'gold', label: '待支付', icon: <ClockCircleOutlined /> },
|
||||
cancelled: { color: 'default', label: '已取消', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{/* Filters */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 24 }}>
|
||||
<Row gutter={[16, 16]} align="middle">
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<span style={{ marginRight: 8 }}>支付方式:</span>
|
||||
<Select
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
value={filters.paymentMethod}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, paymentMethod: value }))}
|
||||
>
|
||||
<Option value="alipay">支付宝</Option>
|
||||
<Option value="wechat">微信</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<span style={{ marginRight: 8 }}>状态:</span>
|
||||
<Select
|
||||
placeholder="全部"
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
value={filters.status}
|
||||
onChange={(value) => setFilters(prev => ({ ...prev, status: value }))}
|
||||
>
|
||||
<Option value="paid">已支付</Option>
|
||||
<Option value="pending">待支付</Option>
|
||||
<Option value="cancelled">已取消</Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<span style={{ marginRight: 8 }}>日期范围:</span>
|
||||
<RangePicker
|
||||
value={[
|
||||
dayjs(filters.startDate),
|
||||
dayjs(filters.endDate),
|
||||
]}
|
||||
onChange={handleDateChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={4}>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>
|
||||
重置
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
const methodConfig: Record<string, { color: string; label: string }> = {
|
||||
alipay: { color: 'blue', label: '支付宝' },
|
||||
wechat: { color: 'green', label: '微信' },
|
||||
};
|
||||
{/* Summary cards */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="今日收入"
|
||||
value={stats.today.paidAmount}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined style={{ color: '#10b981' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#10b981', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{stats.today.paidCount} 笔订单
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="本月累计"
|
||||
value={monthInfo.paidAmount}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined style={{ color: '#6366f1' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#6366f1', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{monthInfo.paidCount} 笔订单
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="待支付"
|
||||
value={pendingInfo.count}
|
||||
prefix={<ClockCircleOutlined style={{ color: '#f59e0b' }} />}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#f59e0b', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
¥{pendingInfo.amount.toFixed(2)} 待付
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="已取消"
|
||||
value={cancelledInfo.count}
|
||||
prefix={<CloseCircleOutlined style={{ color: '#94a3b8' }} />}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#94a3b8', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
¥{cancelledInfo.amount.toFixed(2)} 已取消
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
const columns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo', key: 'orderNo', width: 200 },
|
||||
{
|
||||
title: '支付方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 100,
|
||||
render: (m: string) => {
|
||||
const c = methodConfig[m] || { color: 'default', label: m };
|
||||
return <Tag color={c.color}>{c.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '金额', dataIndex: 'amount', key: 'amount', width: 100,
|
||||
render: (a: number) => <Typography.Text strong style={{ color: '#10b981' }}>¥{a.toFixed(2)}</Typography.Text>,
|
||||
},
|
||||
{ title: '积分', dataIndex: 'credits', key: 'credits', width: 80 },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 100,
|
||||
render: (s: string) => {
|
||||
const c = statusConfig[s] || { color: 'default', label: s, icon: null };
|
||||
return <Tag color={c.color} icon={c.icon}>{c.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{ title: '支付宝交易号', dataIndex: 'tradeNo', key: 'tradeNo', width: 200, render: (v: string) => v || '-' },
|
||||
{
|
||||
title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 160,
|
||||
render: (d: string) => <span className="date-display" style={{ color: '#94a3b8' }}>{d ? formatDate(d) : '-'}</span>,
|
||||
},
|
||||
{
|
||||
title: '支付时间', dataIndex: 'paidAt', key: 'paidAt', width: 160,
|
||||
render: (d: string) => <span className="date-display" style={{ color: '#94a3b8' }}>{d ? formatDate(d) : '-'}</span>,
|
||||
},
|
||||
];
|
||||
{/* Status breakdown */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}
|
||||
title={<Space><DollarOutlined />订单状态分布</Space>}>
|
||||
<Row gutter={16}>
|
||||
{['paid', 'pending', 'cancelled'].map(s => {
|
||||
const info = stats.byStatus?.[s] || { count: 0, amount: 0 };
|
||||
const c = statusConfig[s];
|
||||
const pct = totalOrders > 0 ? ((info.count / totalOrders) * 100).toFixed(1) : '0.0';
|
||||
return (
|
||||
<Col span={8} key={s}>
|
||||
<div style={{
|
||||
padding: 16, borderRadius: 10,
|
||||
background: '#fafbff', border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<Space>
|
||||
<Tag color={c.color} icon={c.icon}>{c.label}</Tag>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{pct}%</Typography.Text>
|
||||
</Space>
|
||||
<div style={{ marginTop: 8, fontSize: 20, fontWeight: 700 }}>
|
||||
{info.count} <span style={{ fontSize: 13, color: '#94a3b8', fontWeight: 400 }}>笔</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#64748b', marginTop: 4 }}>
|
||||
¥{info.amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
if (!stats) {
|
||||
return <div style={{ padding: 24, color: '#94a3b8' }}>加载中…</div>;
|
||||
}
|
||||
|
||||
const paidInfo = stats.byStatus?.paid || { count: 0, amount: 0 };
|
||||
const pendingInfo = stats.byStatus?.pending || { count: 0, amount: 0 };
|
||||
const cancelledInfo = stats.byStatus?.cancelled || { count: 0, amount: 0 };
|
||||
const totalOrders = paidInfo.count + pendingInfo.count + cancelledInfo.count;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Summary cards */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="今日收入"
|
||||
value={stats.today.paidAmount}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined style={{ color: '#10b981' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#10b981', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{stats.today.paidCount} 笔订单
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="累计已支付"
|
||||
value={paidInfo.amount}
|
||||
precision={2}
|
||||
prefix={<DollarOutlined style={{ color: '#6366f1' }} />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#6366f1', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{paidInfo.count} 笔订单
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="待支付"
|
||||
value={pendingInfo.count}
|
||||
prefix={<ClockCircleOutlined style={{ color: '#f59e0b' }} />}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#f59e0b', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
¥{pendingInfo.amount.toFixed(2)} 待付
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} sm={12} lg={6}>
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||
<Statistic
|
||||
title="已取消"
|
||||
value={cancelledInfo.count}
|
||||
prefix={<CloseCircleOutlined style={{ color: '#94a3b8' }} />}
|
||||
suffix="笔"
|
||||
valueStyle={{ color: '#94a3b8', fontWeight: 700 }}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
¥{cancelledInfo.amount.toFixed(2)} 已取消
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Status breakdown */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}
|
||||
title={<Space><DollarOutlined />订单状态分布</Space>}>
|
||||
<Row gutter={16}>
|
||||
{['paid', 'pending', 'cancelled'].map(s => {
|
||||
const info = stats.byStatus?.[s] || { count: 0, amount: 0 };
|
||||
const c = statusConfig[s];
|
||||
const pct = totalOrders > 0 ? ((info.count / totalOrders) * 100).toFixed(1) : '0.0';
|
||||
return (
|
||||
<Col span={8} key={s}>
|
||||
<div style={{
|
||||
padding: 16, borderRadius: 10,
|
||||
background: '#fafbff', border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<Space>
|
||||
<Tag color={c.color} icon={c.icon}>{c.label}</Tag>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>{pct}%</Typography.Text>
|
||||
</Space>
|
||||
<div style={{ marginTop: 8, fontSize: 20, fontWeight: 700 }}>
|
||||
{info.count} <span style={{ fontSize: 13, color: '#94a3b8', fontWeight: 400 }}>笔</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, color: '#64748b', marginTop: 4 }}>
|
||||
¥{info.amount.toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* Recent orders table */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}
|
||||
title={<Space><DollarOutlined />最近 50 笔订单</Space>}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={stats.recent}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
scroll={{ x: 1000 }}
|
||||
size="middle"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
{/* Recent orders table */}
|
||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}
|
||||
title={<Space><DollarOutlined />订单列表</Space>}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={stats.recent}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
scroll={{ x: 1000 }}
|
||||
size="middle"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminPaymentStats;
|
||||
|
||||
@@ -118,9 +118,10 @@ export interface AdminStats {
|
||||
}
|
||||
|
||||
export interface PaymentStats {
|
||||
byStatus: Record<string, { count: number; amount: number }>;
|
||||
today: { paidCount: number; paidAmount: number };
|
||||
recent: PaymentOrder[];
|
||||
byStatus: Record<string, { count: number; amount: number }>;
|
||||
today: { paidCount: number; paidAmount: number };
|
||||
month: { paidCount: number; paidAmount: number };
|
||||
recent: PaymentOrder[];
|
||||
}
|
||||
|
||||
export interface PaymentOrder {
|
||||
|
||||
Reference in New Issue
Block a user