fix
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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`);
|
||||
@@ -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 });
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user