This commit is contained in:
2026-04-23 18:51:49 +08:00
parent 3284321919
commit 6be9d4afb7
74 changed files with 7660 additions and 688 deletions
+16 -2
View File
@@ -10,9 +10,13 @@ import MerchantList from '@/pages/MerchantList';
import RoomAudit from '@/pages/RoomAudit';
import UserList from '@/pages/UserList';
import OrderList from '@/pages/OrderList';
import Finance from '@/pages/Finance';
import Promotion from '@/pages/Promotion';
import SystemSettings from '@/pages/SystemSettings';
import FinanceSettlements from '@/pages/finance/Settlements';
import FinanceWithdrawals from '@/pages/finance/Withdrawals';
import FinanceEarnings from '@/pages/finance/Earnings';
import ActivityList from '@/pages/activity/ActivityList';
import ActivityDetail from '@/pages/activity/ActivityDetail';
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const token = localStorage.getItem('admin_token');
@@ -33,7 +37,17 @@ const App: React.FC = () => (
<Route path="room-audit" element={<RoomAudit />} />
<Route path="users" element={<UserList />} />
<Route path="orders" element={<OrderList />} />
<Route path="finance" element={<Finance />} />
<Route path="finance">
<Route index element={<Navigate to="/finance/settlements" replace />} />
<Route path="settlements" element={<FinanceSettlements />} />
<Route path="withdrawals" element={<FinanceWithdrawals />} />
<Route path="earnings" element={<FinanceEarnings />} />
</Route>
<Route path="activity">
<Route index element={<Navigate to="/activity/list" replace />} />
<Route path="list" element={<ActivityList />} />
<Route path="detail/:id" element={<ActivityDetail />} />
</Route>
<Route path="promotions" element={<Promotion />} />
<Route path="settings" element={<SystemSettings />} />
</Route>
+49
View File
@@ -0,0 +1,49 @@
import request from '@/utils/request';
// 活动列表
export const getActivities = (params?: { page?: number; pageSize?: number; type?: string; enabled?: boolean }) =>
request.get('/admin/activity/list', { params });
// 活动详情
export const getActivityById = (id: number) =>
request.get(`/admin/activity/${id}`);
// 创建活动
export const createActivity = (data: any) =>
request.post('/admin/activity/create', data);
// 编辑活动
export const updateActivity = (id: number, data: any) =>
request.put(`/admin/activity/${id}/update`, data);
// 启用/停用活动
export const toggleActivity = (id: number) =>
request.put(`/admin/activity/${id}/toggle`);
// 邀请数据总览
export const getInviteStats = () =>
request.get('/admin/activity/invite/stats');
// 邀请记录列表
export const getInviteRecords = (params?: { page?: number; pageSize?: number }) =>
request.get('/admin/activity/invite/records', { params });
// 返现记录列表
export const getCashbackRecords = (params?: { page?: number; pageSize?: number }) =>
request.get('/admin/activity/invite/cashbacks', { params });
// 邀请提现列表
export const getInviteWithdrawals = (params?: { page?: number; pageSize?: number; status?: string }) =>
request.get('/admin/activity/invite/withdrawals', { params });
// 审核通过提现
export const approveInviteWithdrawal = (id: number) =>
request.put(`/admin/activity/invite/withdrawals/${id}/approve`);
// 审核拒绝提现
export const rejectInviteWithdrawal = (id: number, reason: string) =>
request.put(`/admin/activity/invite/withdrawals/${id}/reject`, { reason });
// 确认打款
export const payInviteWithdrawal = (id: number) =>
request.put(`/admin/activity/invite/withdrawals/${id}/pay`);
+37
View File
@@ -0,0 +1,37 @@
import request from '@/utils/request';
// 对账单列表
export const getSettlements = (params?: { page?: number; pageSize?: number; status?: string; merchantId?: number }) =>
request.get('/admin/finance/settlements', { params });
// 对账单详情
export const getSettlementDetail = (id: number) =>
request.get(`/admin/finance/settlements/${id}`);
// 审核通过对账单
export const approveSettlement = (id: number) =>
request.put(`/admin/finance/settlements/${id}/approve`, {});
// 拒绝对账单
export const rejectSettlement = (id: number, rejectReason: string) =>
request.put(`/admin/finance/settlements/${id}/reject`, { rejectReason });
// 提现列表
export const getWithdrawals = (params?: { page?: number; pageSize?: number; status?: string; merchantId?: number }) =>
request.get('/admin/finance/withdrawals', { params });
// 审核通过提现
export const approveWithdrawal = (id: number) =>
request.put(`/admin/finance/withdrawals/${id}/approve`, {});
// 拒绝提现
export const rejectWithdrawal = (id: number, rejectReason: string) =>
request.put(`/admin/finance/withdrawals/${id}/reject`, { rejectReason });
// 确认打款
export const payWithdrawal = (id: number) =>
request.put(`/admin/finance/withdrawals/${id}/pay`, {});
// 平台收益统计
export const getEarnings = (params?: { startDate?: string; endDate?: string }) =>
request.get('/admin/finance/earnings', { params });
+34 -3
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { Layout, Menu, Button, Avatar, Dropdown } from 'antd';
import {
@@ -12,6 +12,12 @@ import {
LogoutOutlined,
UserOutlined,
HomeOutlined,
AuditOutlined,
PayCircleOutlined,
FundOutlined,
RocketOutlined,
OrderedListOutlined,
TrophyOutlined,
} from '@ant-design/icons';
import { useAuthStore } from '@/store/auth';
@@ -23,7 +29,24 @@ const menuItems = [
{ key: '/room-audit', icon: <HomeOutlined />, label: '房源审核' },
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
{ key: '/orders', icon: <UnorderedListOutlined />, label: '订单管理' },
{ key: '/finance', icon: <WalletOutlined />, label: '财务管理' },
{
key: '/finance',
icon: <WalletOutlined />,
label: '财务管理',
children: [
{ key: '/finance/settlements', icon: <AuditOutlined />, label: '对账审核' },
{ key: '/finance/withdrawals', icon: <PayCircleOutlined />, label: '提现审核' },
{ key: '/finance/earnings', icon: <FundOutlined />, label: '平台收益' },
],
},
{
key: '/activity',
icon: <RocketOutlined />,
label: '活动管理',
children: [
{ key: '/activity/list', icon: <GiftOutlined />, label: '活动列表' },
],
},
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
];
@@ -33,16 +56,24 @@ const MainLayout: React.FC = () => {
const location = useLocation();
const { userInfo, logout } = useAuthStore();
const openKeys = useMemo(() => {
const path = location.pathname;
if (path.startsWith('/finance')) return ['/finance'];
if (path.startsWith('/activity')) return ['/activity'];
return [];
}, [location.pathname]);
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider width={220} theme="dark">
<div style={{ height: 64, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<h2 style={{ margin: 0, color: '#fff', fontSize: 18 }}></h2>
<h2 style={{ margin: 0, color: '#fff', fontSize: 18 }}></h2>
</div>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
defaultOpenKeys={openKeys}
items={menuItems}
onClick={({ key }) => navigate(key)}
style={{ borderRight: 0 }}
@@ -0,0 +1,258 @@
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Descriptions, Tag, Button, Tabs, Table, Modal, Input, Space, message, Statistic, Row, Col } from 'antd';
import {
getActivityById, createActivity, updateActivity,
getInviteStats, getInviteRecords, getCashbackRecords,
getInviteWithdrawals, approveInviteWithdrawal, rejectInviteWithdrawal, payInviteWithdrawal,
} from '@/api/activity';
import dayjs from 'dayjs';
const ActivityDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const isCreate = id === 'create';
const [activity, setActivity] = useState<any>(null);
const [loading, setLoading] = useState(!isCreate);
// 邀请返现数据
const [stats, setStats] = useState<any>({});
const [invitations, setInvitations] = useState<any[]>([]);
const [invTotal, setInvTotal] = useState(0);
const [invPage, setInvPage] = useState(1);
const [invLoading, setInvLoading] = useState(false);
const [cashbacks, setCashbacks] = useState<any[]>([]);
const [cbTotal, setCbTotal] = useState(0);
const [cbPage, setCbPage] = useState(1);
const [cbLoading, setCbLoading] = useState(false);
const [withdrawals, setWithdrawals] = useState<any[]>([]);
const [wdTotal, setWdTotal] = useState(0);
const [wdPage, setWdPage] = useState(1);
const [wdLoading, setWdLoading] = useState(false);
const [rejectModalOpen, setRejectModalOpen] = useState(false);
const [currentWdId, setCurrentWdId] = useState<number | null>(null);
const [rejectReason, setRejectReason] = useState('');
const fetchActivity = async () => {
if (isCreate) return;
setLoading(true);
try {
const res = await getActivityById(Number(id));
setActivity(res.data);
} finally {
setLoading(false);
}
};
const fetchInviteData = async () => {
try {
const [statsRes, invRes, cbRes, wdRes] = await Promise.all([
getInviteStats(),
getInviteRecords({ page: invPage, pageSize: 10 }),
getCashbackRecords({ page: cbPage, pageSize: 10 }),
getInviteWithdrawals({ page: wdPage, pageSize: 10 }),
]);
setStats(statsRes.data || {});
setInvitations(invRes.data?.list || []);
setInvTotal(invRes.data?.total || 0);
setCashbacks(cbRes.data?.list || []);
setCbTotal(cbRes.data?.total || 0);
setWithdrawals(wdRes.data?.list || []);
setWdTotal(wdRes.data?.total || 0);
} catch {}
};
useEffect(() => { fetchActivity(); }, [id]);
useEffect(() => {
if (!isCreate && activity?.type === 'invite_cashback') fetchInviteData();
}, [activity, invPage, cbPage, wdPage]);
// 创建固定活动
const handleCreate = async (type: string) => {
const typeMap: Record<string, { name: string; config: any }> = {
invite_cashback: {
name: '邀请返现',
config: { firstOrderRate: 0.05, secondOrderRate: 0.005, minCashback: 0.01, maxCashback: 50, withdrawThreshold: 10 },
},
};
const preset = typeMap[type];
if (!preset) return;
try {
await createActivity({ name: preset.name, type, enabled: true, config: preset.config });
message.success('创建成功');
navigate('/activity/list');
} catch {}
};
const handleApprove = async (wid: number) => {
try { await approveInviteWithdrawal(wid); message.success('审核通过'); fetchInviteData(); } catch {}
};
const handleReject = async () => {
if (!rejectReason.trim()) { message.error('请输入拒绝原因'); return; }
try {
await rejectInviteWithdrawal(currentWdId!, rejectReason);
message.success('已拒绝');
setRejectModalOpen(false);
setRejectReason('');
fetchInviteData();
} catch {}
};
const handlePay = async (wid: number) => {
try { await payInviteWithdrawal(wid); message.success('已确认打款'); fetchInviteData(); } catch {}
};
if (isCreate) {
return (
<Card title="创建活动">
<div style={{ display: 'flex', gap: 24 }}>
<Card hoverable style={{ width: 300 }} onClick={() => handleCreate('invite_cashback')}>
<Card.Meta title="邀请返现" description="邀请好友下单,邀请人获得返现奖励。首单5%,二单0.5%。" />
</Card>
</div>
<div style={{ marginTop: 24 }}>
<Button onClick={() => navigate('/activity/list')}></Button>
</div>
</Card>
);
}
if (loading) return <Card loading />;
if (!activity) return <Card></Card>;
const invColumns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '邀请人', render: (_: any, r: any) => r.inviter?.nickname || `用户${r.inviterId}` },
{ title: '被邀请人', render: (_: any, r: any) => r.invitee?.nickname || `用户${r.inviteeId}` },
{ title: '邀请码', dataIndex: 'inviteCode' },
{ title: '邀请时间', dataIndex: 'createdAt', render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
const cbColumns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '邀请人', render: (_: any, r: any) => r.inviter?.nickname || `用户${r.inviterId}` },
{ title: '被邀请人', render: (_: any, r: any) => r.invitee?.nickname || `用户${r.inviteeId}` },
{ title: '订单号', dataIndex: 'orderNo' },
{ title: '订单金额', dataIndex: 'orderAmount', render: (v: number) => `¥${v}` },
{ title: '第几单', dataIndex: 'orderIndex', render: (v: number) => `${v}` },
{ title: '返现比例', dataIndex: 'rate', render: (v: number) => `${(v * 100).toFixed(1)}%` },
{ title: '返现金额', dataIndex: 'amount', render: (v: number) => <Tag color="orange">¥{v}</Tag> },
{ title: '状态', dataIndex: 'status', render: (v: string) => {
const map: any = { pending: '待结算', settled: '已到账', cancelled: '已取消' };
const colors: any = { pending: 'default', settled: 'green', cancelled: 'red' };
return <Tag color={colors[v]}>{map[v]}</Tag>;
}},
{ title: '创建时间', dataIndex: 'createdAt', render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
];
const wdColumns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '用户', render: (_: any, r: any) => r.user?.nickname || `用户${r.userId}` },
{ title: '提现金额', dataIndex: 'amount', render: (v: number) => <Tag color="orange">¥{v}</Tag> },
{ title: '状态', dataIndex: 'status', render: (v: string) => {
const map: any = { pending: '待审核', approved: '已通过', rejected: '已拒绝', paid: '已打款' };
const colors: any = { pending: 'default', approved: 'blue', rejected: 'red', paid: 'green' };
return <Tag color={colors[v]}>{map[v]}</Tag>;
}},
{ title: '拒绝原因', dataIndex: 'rejectReason' },
{ title: '申请时间', dataIndex: 'createdAt', render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{
title: '操作',
render: (_: any, r: any) => (
<Space>
{r.status === 'pending' && (
<>
<Button type="primary" size="small" onClick={() => handleApprove(r.id)}></Button>
<Button danger size="small" onClick={() => { setCurrentWdId(r.id); setRejectModalOpen(true); }}></Button>
</>
)}
{r.status === 'approved' && (
<Button type="primary" size="small" onClick={() => handlePay(r.id)}></Button>
)}
</Space>
),
},
];
const tabItems = [
{
key: 'info',
label: '活动信息',
children: (
<Descriptions column={2} bordered>
<Descriptions.Item label="活动名称">{activity.name}</Descriptions.Item>
<Descriptions.Item label="活动类型"></Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={activity.enabled ? 'green' : 'default'}>{activity.enabled ? '启用' : '停用'}</Tag>
</Descriptions.Item>
<Descriptions.Item label="活动时间">
{activity.startTime
? `${dayjs(activity.startTime).format('YYYY-MM-DD')} ~ ${dayjs(activity.endTime).format('YYYY-MM-DD')}`
: '不限'}
</Descriptions.Item>
{activity.config && (
<>
<Descriptions.Item label="首单返现比例">{(activity.config.firstOrderRate * 100).toFixed(1)}%</Descriptions.Item>
<Descriptions.Item label="二单返现比例">{(activity.config.secondOrderRate * 100).toFixed(1)}%</Descriptions.Item>
<Descriptions.Item label="最低返现">¥{activity.config.minCashback}</Descriptions.Item>
<Descriptions.Item label="最高返现">¥{activity.config.maxCashback}</Descriptions.Item>
<Descriptions.Item label="提现门槛">¥{activity.config.withdrawThreshold}</Descriptions.Item>
</>
)}
</Descriptions>
),
},
{
key: 'stats',
label: '数据概览',
children: (
<div>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={4}><Card><Statistic title="邀请总数" value={stats.totalInvitations || 0} /></Card></Col>
<Col span={4}><Card><Statistic title="返现总笔数" value={stats.totalCashbacks || 0} /></Card></Col>
<Col span={4}><Card><Statistic title="返现总金额" value={stats.totalCashbackAmount || 0} prefix="¥" precision={2} /></Card></Col>
<Col span={4}><Card><Statistic title="提现申请数" value={stats.totalWithdrawals || 0} /></Card></Col>
<Col span={4}><Card><Statistic title="待审核提现" value={stats.pendingWithdrawals || 0} valueStyle={{ color: '#faad14' }} /></Card></Col>
</Row>
<Card title={`邀请记录 (${invTotal})`} style={{ marginBottom: 16 }}>
<Table rowKey="id" columns={invColumns} dataSource={invitations} loading={invLoading} size="small"
pagination={{ current: invPage, total: invTotal, pageSize: 10, onChange: setInvPage }} />
</Card>
<Card title={`返现记录 (${cbTotal})`}>
<Table rowKey="id" columns={cbColumns} dataSource={cashbacks} loading={cbLoading} size="small"
pagination={{ current: cbPage, total: cbTotal, pageSize: 10, onChange: setCbPage }} />
</Card>
</div>
),
},
{
key: 'withdrawals',
label: `提现审核${stats.pendingWithdrawals ? ` (${stats.pendingWithdrawals})` : ''}`,
children: (
<Table rowKey="id" columns={wdColumns} dataSource={withdrawals} loading={wdLoading}
pagination={{ current: wdPage, total: wdTotal, pageSize: 10, onChange: setWdPage }} />
),
},
];
return (
<div>
<div style={{ marginBottom: 16 }}>
<Button onClick={() => navigate('/activity/list')}></Button>
</div>
<Card>
<Tabs items={tabItems} />
</Card>
<Modal title="拒绝原因" open={rejectModalOpen} onOk={handleReject} onCancel={() => setRejectModalOpen(false)}>
<Input.TextArea rows={3} value={rejectReason} onChange={e => setRejectReason(e.target.value)} placeholder="请输入拒绝原因" />
</Modal>
</div>
);
};
export default ActivityDetail;
@@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import { Card, Table, Button, Tag, Space, message } from 'antd';
import { useNavigate } from 'react-router-dom';
import { getActivities, toggleActivity } from '@/api/activity';
import dayjs from 'dayjs';
const ACTIVITY_TYPES: Record<string, string> = {
invite_cashback: '邀请返现',
};
const ActivityList: React.FC = () => {
const navigate = useNavigate();
const [list, setList] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const fetchList = async () => {
setLoading(true);
try {
const res = await getActivities({ page, pageSize: 10 });
setList(res.data?.list || []);
setTotal(res.data?.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchList(); }, [page]);
const handleToggle = async (record: any) => {
try {
await toggleActivity(record.id);
message.success(record.enabled ? '已停用' : '已启用');
fetchList();
} catch {}
};
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '活动名称', dataIndex: 'name' },
{ title: '活动类型', dataIndex: 'type', render: (v: string) => ACTIVITY_TYPES[v] || v },
{ title: '状态', dataIndex: 'enabled', render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag> },
{
title: '活动时间',
render: (_: any, r: any) => {
if (!r.startTime) return '不限';
return `${dayjs(r.startTime).format('YYYY-MM-DD')} ~ ${dayjs(r.endTime).format('YYYY-MM-DD')}`;
},
},
{ title: '创建时间', dataIndex: 'createdAt', render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '-' },
{
title: '操作',
render: (_: any, r: any) => (
<Space>
<Button type="primary" size="small" onClick={() => navigate(`/activity/detail/${r.id}`)}></Button>
<Button size="small" danger={r.enabled} type={r.enabled ? 'primary' : 'default'} onClick={() => handleToggle(r)}>
{r.enabled ? '停用' : '启用'}
</Button>
</Space>
),
},
];
return (
<Card title="活动列表" extra={<Button type="primary" onClick={() => navigate('/activity/detail/create')}></Button>}>
<Table
rowKey="id"
columns={columns}
dataSource={list}
loading={loading}
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
/>
</Card>
);
};
export default ActivityList;
@@ -0,0 +1,136 @@
import React, { useEffect, useState } from 'react';
import { Card, Row, Col, Statistic, DatePicker, Spin } from 'antd';
import { ArrowUpOutlined, DollarOutlined, TeamOutlined, TransactionOutlined } from '@ant-design/icons';
import { getEarnings } from '@/api/finance';
import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
const Earnings: React.FC = () => {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(false);
const [dateRange, setDateRange] = useState<any>(null);
const fetchData = async (startDate?: string, endDate?: string) => {
setLoading(true);
try {
const res: any = await getEarnings({ startDate, endDate });
setData(res.data);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, []);
const handleDateChange = (dates: any) => {
setDateRange(dates);
if (dates) {
fetchData(dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD'));
} else {
fetchData();
}
};
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 24 }}>
<h2></h2>
<RangePicker onChange={handleDateChange} value={dateRange} />
</div>
<Row gutter={24} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="订单总金额"
value={data?.orderAmount || 0}
precision={2}
prefix="¥"
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="佣金收入"
value={data?.commission || 0}
precision={2}
prefix="¥"
valueStyle={{ color: '#52c41a' }}
prefix={<ArrowUpOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="结算金额"
value={data?.settlementAmount || 0}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="对账单数"
value={data?.totalSettlements || 0}
prefix={<TransactionOutlined />}
/>
</Card>
</Col>
</Row>
<Row gutter={24}>
<Col span={6}>
<Card>
<Statistic
title="已打款金额"
value={data?.paidAmount || 0}
precision={2}
prefix="¥"
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="提现手续费"
value={data?.totalFee || 0}
precision={2}
prefix="¥"
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="提现佣金"
value={data?.totalWithdrawCommission || 0}
precision={2}
prefix="¥"
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="提现申请数"
value={data?.totalWithdrawals || 0}
prefix={<TeamOutlined />}
/>
</Card>
</Col>
</Row>
</div>
);
};
export default Earnings;
@@ -0,0 +1,174 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Button, Select, Modal, Descriptions, Input, Space, message, Popconfirm } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getSettlements, getSettlementDetail, approveSettlement, rejectSettlement } from '@/api/finance';
const { Option } = Select;
const statusMap: Record<string, { color: string; label: string }> = {
pending: { color: 'gold', label: '待审核' },
approved: { color: 'green', label: '已审核' },
rejected: { color: 'red', label: '已拒绝' },
};
const Settlements: React.FC = () => {
const [data, setData] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [status, setStatus] = useState<string>('');
const [loading, setLoading] = useState(false);
const [detailVisible, setDetailVisible] = useState(false);
const [detail, setDetail] = useState<any>(null);
const [rejectVisible, setRejectVisible] = useState(false);
const [rejectId, setRejectId] = useState<number>(0);
const [rejectReason, setRejectReason] = useState('');
const fetchData = async () => {
setLoading(true);
try {
const res: any = await getSettlements({ page, pageSize: 10, status: status || undefined });
setData(res.data?.list || []);
setTotal(res.data?.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, [page, status]);
const showDetail = async (id: number) => {
try {
const res: any = await getSettlementDetail(id);
setDetail(res.data);
setDetailVisible(true);
} catch {
message.error('获取详情失败');
}
};
const handleApprove = async (id: number) => {
try {
await approveSettlement(id);
message.success('审核通过');
fetchData();
} catch (e: any) {
message.error(e.message || '操作失败');
}
};
const handleReject = async () => {
if (!rejectReason.trim()) { message.warning('请输入拒绝原因'); return; }
try {
await rejectSettlement(rejectId, rejectReason);
message.success('已拒绝');
setRejectVisible(false);
setRejectReason('');
fetchData();
} catch (e: any) {
message.error(e.message || '操作失败');
}
};
const columns: ColumnsType<any> = [
{ title: '对账单号', dataIndex: 'settlementNo', width: 200 },
{ title: '商家', dataIndex: ['merchant', 'shopName'], width: 120, ellipsis: true },
{ title: '周期', width: 200, render: (_, r) => `${r.periodStart} ~ ${r.periodEnd}` },
{ title: '订单数', dataIndex: 'orderCount', width: 80, align: 'center' },
{ title: '订单金额', dataIndex: 'orderAmount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '佣金', dataIndex: 'commissionAmount', width: 100, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '结算金额', dataIndex: 'settlementAmount', width: 120, render: (v) => <span style={{ color: '#1890ff', fontWeight: 600 }}>¥{Number(v).toFixed(2)}</span> },
{
title: '状态', dataIndex: 'status', width: 100,
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
},
{
title: '操作', width: 200, fixed: 'right',
render: (_, r) => (
<Space size="small">
<Button type="link" size="small" onClick={() => showDetail(r.id)}></Button>
{r.status === 'pending' && (
<>
<Popconfirm title="确认审核通过?" onConfirm={() => handleApprove(r.id)}>
<Button type="link" size="small" style={{ color: '#52c41a' }}></Button>
</Popconfirm>
<Button type="link" size="small" danger onClick={() => { setRejectId(r.id); setRejectReason(''); setRejectVisible(true); }}></Button>
</>
)}
</Space>
),
},
];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h2></h2>
<Select value={status || undefined} placeholder="状态筛选" style={{ width: 150 }} allowClear onChange={(v) => { setStatus(v || ''); setPage(1); }}>
{Object.entries(statusMap).map(([k, v]) => <Option key={k} value={k}>{v.label}</Option>)}
</Select>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: 1200 }}
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
/>
{/* 详情弹窗 */}
<Modal
title="对账单详情"
open={detailVisible}
footer={null}
width={720}
onCancel={() => setDetailVisible(false)}
>
{detail && (
<>
<Descriptions bordered size="small" column={2} style={{ marginBottom: 16 }}>
<Descriptions.Item label="对账单号">{detail.settlementNo}</Descriptions.Item>
<Descriptions.Item label="商家">{detail.merchant?.shopName}</Descriptions.Item>
<Descriptions.Item label="状态"><Tag color={statusMap[detail.status]?.color}>{statusMap[detail.status]?.label}</Tag></Descriptions.Item>
<Descriptions.Item label="周期">{detail.periodStart} ~ {detail.periodEnd}</Descriptions.Item>
<Descriptions.Item label="订单数">{detail.orderCount}</Descriptions.Item>
<Descriptions.Item label="订单金额">¥{Number(detail.orderAmount).toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="佣金比例">{Number(detail.commissionRate) * 100}%</Descriptions.Item>
<Descriptions.Item label="佣金金额">¥{Number(detail.commissionAmount).toFixed(2)}</Descriptions.Item>
<Descriptions.Item label="结算金额" span={2}><span style={{ color: '#1890ff', fontWeight: 600 }}>¥{Number(detail.settlementAmount).toFixed(2)}</span></Descriptions.Item>
</Descriptions>
{detail.rejectReason && (
<div style={{ color: 'red', marginBottom: 16 }}>{detail.rejectReason}</div>
)}
{detail.items?.length > 0 && (
<Table
rowKey="id"
size="small"
pagination={false}
dataSource={detail.items}
columns={[
{ title: '订单号', dataIndex: 'orderNo', width: 200 },
{ title: '订单金额', dataIndex: 'orderAmount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '创建时间', dataIndex: 'createdAt' },
]}
/>
)}
</>
)}
</Modal>
{/* 拒绝弹窗 */}
<Modal
title="拒绝对账单"
open={rejectVisible}
onOk={handleReject}
onCancel={() => setRejectVisible(false)}
okText="确认拒绝"
>
<Input.TextArea rows={3} placeholder="请输入拒绝原因" value={rejectReason} onChange={(e) => setRejectReason(e.target.value)} />
</Modal>
</div>
);
};
export default Settlements;
@@ -0,0 +1,140 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Button, Select, Space, Modal, Input, message, Popconfirm } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { getWithdrawals, approveWithdrawal, rejectWithdrawal, payWithdrawal } from '@/api/finance';
const { Option } = Select;
const statusMap: Record<string, { color: string; label: string }> = {
pending: { color: 'gold', label: '待审核' },
approved: { color: 'blue', label: '审核通过' },
rejected: { color: 'red', label: '已拒绝' },
paid: { color: 'green', label: '已打款' },
};
const Withdrawals: React.FC = () => {
const [data, setData] = useState<any[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [status, setStatus] = useState<string>('');
const [loading, setLoading] = useState(false);
const [rejectVisible, setRejectVisible] = useState(false);
const [rejectId, setRejectId] = useState<number>(0);
const [rejectReason, setRejectReason] = useState('');
const fetchData = async () => {
setLoading(true);
try {
const res: any = await getWithdrawals({ page, pageSize: 10, status: status || undefined });
setData(res.data?.list || []);
setTotal(res.data?.total || 0);
} finally {
setLoading(false);
}
};
useEffect(() => { fetchData(); }, [page, status]);
const handleApprove = async (id: number) => {
try {
await approveWithdrawal(id);
message.success('审核通过');
fetchData();
} catch (e: any) {
message.error(e.message || '操作失败');
}
};
const handleReject = async () => {
if (!rejectReason.trim()) { message.warning('请输入拒绝原因'); return; }
try {
await rejectWithdrawal(rejectId, rejectReason);
message.success('已拒绝');
setRejectVisible(false);
setRejectReason('');
fetchData();
} catch (e: any) {
message.error(e.message || '操作失败');
}
};
const handlePay = async (id: number) => {
try {
await payWithdrawal(id);
message.success('已确认打款');
fetchData();
} catch (e: any) {
message.error(e.message || '操作失败');
}
};
const columns: ColumnsType<any> = [
{ title: '商家', dataIndex: ['merchant', 'shopName'], width: 120, ellipsis: true },
{ title: '提现金额', dataIndex: 'amount', width: 120, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '手续费', dataIndex: 'fee', width: 90, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '佣金', dataIndex: 'commissionAmount', width: 90, render: (v) => `¥${Number(v).toFixed(2)}` },
{ title: '实际到账', dataIndex: 'actualAmount', width: 120, render: (v) => <span style={{ color: '#1890ff', fontWeight: 600 }}>¥{Number(v).toFixed(2)}</span> },
{ title: '开户银行', dataIndex: 'bankName', width: 120 },
{ title: '银行账号', dataIndex: 'bankAccount', width: 150 },
{ title: '账户名', dataIndex: 'accountName', width: 100 },
{
title: '状态', dataIndex: 'status', width: 100,
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
},
{
title: '操作', width: 220, fixed: 'right',
render: (_, r) => (
<Space size="small">
{r.status === 'pending' && (
<>
<Popconfirm title="确认审核通过?" onConfirm={() => handleApprove(r.id)}>
<Button type="link" size="small" style={{ color: '#52c41a' }}></Button>
</Popconfirm>
<Button type="link" size="small" danger onClick={() => { setRejectId(r.id); setRejectReason(''); setRejectVisible(true); }}></Button>
</>
)}
{r.status === 'approved' && (
<Popconfirm title="确认已打款?" onConfirm={() => handlePay(r.id)}>
<Button type="link" size="small" style={{ color: '#1890ff' }}></Button>
</Popconfirm>
)}
{r.status === 'rejected' && r.rejectReason && (
<span style={{ color: '#999', fontSize: 12 }}>{r.rejectReason}</span>
)}
</Space>
),
},
];
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<h2></h2>
<Select value={status || undefined} placeholder="状态筛选" style={{ width: 150 }} allowClear onChange={(v) => { setStatus(v || ''); setPage(1); }}>
{Object.entries(statusMap).map(([k, v]) => <Option key={k} value={k}>{v.label}</Option>)}
</Select>
</div>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: 1300 }}
pagination={{ current: page, total, pageSize: 10, onChange: setPage }}
/>
{/* 拒绝弹窗 */}
<Modal
title="拒绝提现"
open={rejectVisible}
onOk={handleReject}
onCancel={() => setRejectVisible(false)}
okText="确认拒绝"
>
<Input.TextArea rows={3} placeholder="请输入拒绝原因" value={rejectReason} onChange={(e) => setRejectReason(e.target.value)} />
</Modal>
</div>
);
};
export default Withdrawals;