This commit is contained in:
2026-04-24 00:28:52 +08:00
parent 6be9d4afb7
commit 524ff8cd32
27 changed files with 568 additions and 314 deletions
+2 -7
View File
@@ -15,8 +15,7 @@ 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';
import InviteManage from '@/pages/InviteManage';
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const token = localStorage.getItem('admin_token');
@@ -43,11 +42,7 @@ const App: React.FC = () => (
<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="invite" element={<InviteManage />} />
<Route path="promotions" element={<Promotion />} />
<Route path="settings" element={<SystemSettings />} />
</Route>
+5
View File
@@ -0,0 +1,5 @@
import request from '@/utils/request';
export const getServiceFeeConfig = () => request.get('/admin/config/service-fee');
export const updateServiceFeeConfig = (rate: number) => request.put('/admin/config/service-fee', { rate });
@@ -1,25 +1,5 @@
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');
@@ -47,3 +27,18 @@ export const rejectInviteWithdrawal = (id: number, reason: string) =>
// 确认打款
export const payInviteWithdrawal = (id: number) =>
request.put(`/admin/activity/invite/withdrawals/${id}/pay`);
// 获取邀请活动配置
export const getInviteConfig = () =>
request.get('/admin/activity/invite/config');
// 更新邀请活动配置
export const updateInviteConfig = (data: {
firstOrderRate?: number;
secondOrderRate?: number;
minCashback?: number;
maxCashback?: number;
withdrawThreshold?: number;
enabled?: boolean;
}) =>
request.put('/admin/activity/invite/config', data);
+1 -11
View File
@@ -15,8 +15,6 @@ import {
AuditOutlined,
PayCircleOutlined,
FundOutlined,
RocketOutlined,
OrderedListOutlined,
TrophyOutlined,
} from '@ant-design/icons';
import { useAuthStore } from '@/store/auth';
@@ -39,14 +37,7 @@ const menuItems = [
{ key: '/finance/earnings', icon: <FundOutlined />, label: '平台收益' },
],
},
{
key: '/activity',
icon: <RocketOutlined />,
label: '活动管理',
children: [
{ key: '/activity/list', icon: <GiftOutlined />, label: '活动列表' },
],
},
{ key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' },
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
];
@@ -59,7 +50,6 @@ const MainLayout: React.FC = () => {
const openKeys = useMemo(() => {
const path = location.pathname;
if (path.startsWith('/finance')) return ['/finance'];
if (path.startsWith('/activity')) return ['/activity'];
return [];
}, [location.pathname]);
@@ -1,22 +1,13 @@
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 { Card, Tabs, Table, Tag, Button, Modal, Input, Space, message, Statistic, Row, Col, Switch, Form, InputNumber } from 'antd';
import {
getActivityById, createActivity, updateActivity,
getInviteStats, getInviteRecords, getCashbackRecords,
getInviteWithdrawals, approveInviteWithdrawal, rejectInviteWithdrawal, payInviteWithdrawal,
} from '@/api/activity';
getInviteConfig, updateInviteConfig,
} from '@/api/invite';
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 InviteManage: React.FC = () => {
const [stats, setStats] = useState<any>({});
const [invitations, setInvitations] = useState<any[]>([]);
const [invTotal, setInvTotal] = useState(0);
@@ -37,59 +28,70 @@ const ActivityDetail: React.FC = () => {
const [currentWdId, setCurrentWdId] = useState<number | null>(null);
const [rejectReason, setRejectReason] = useState('');
const fetchActivity = async () => {
if (isCreate) return;
setLoading(true);
const [config, setConfig] = useState<any>(null);
const [configLoading, setConfigLoading] = useState(false);
const [form] = Form.useForm();
const fetchStats = async () => {
try {
const res = await getActivityById(Number(id));
setActivity(res.data);
const res = await getInviteStats();
setStats(res.data || {});
} catch {}
};
const fetchInvitations = async () => {
setInvLoading(true);
try {
const res = await getInviteRecords({ page: invPage, pageSize: 10 });
setInvitations(res.data?.list || []);
setInvTotal(res.data?.total || 0);
} finally {
setLoading(false);
setInvLoading(false);
}
};
const fetchInviteData = async () => {
const fetchCashbacks = async () => {
setCbLoading(true);
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 {}
const res = await getCashbackRecords({ page: cbPage, pageSize: 10 });
setCashbacks(res.data?.list || []);
setCbTotal(res.data?.total || 0);
} finally {
setCbLoading(false);
}
};
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;
const fetchWithdrawals = async () => {
setWdLoading(true);
try {
await createActivity({ name: preset.name, type, enabled: true, config: preset.config });
message.success('创建成功');
navigate('/activity/list');
} catch {}
const res = await getInviteWithdrawals({ page: wdPage, pageSize: 10 });
setWithdrawals(res.data?.list || []);
setWdTotal(res.data?.total || 0);
} finally {
setWdLoading(false);
}
};
const fetchConfig = async () => {
setConfigLoading(true);
try {
const res = await getInviteConfig();
setConfig(res.data);
if (res.data) {
form.setFieldsValue(res.data);
}
} finally {
setConfigLoading(false);
}
};
useEffect(() => { fetchStats(); fetchConfig(); }, []);
useEffect(() => { fetchInvitations(); }, [invPage]);
useEffect(() => { fetchCashbacks(); }, [cbPage]);
useEffect(() => { fetchWithdrawals(); }, [wdPage]);
const handleApprove = async (wid: number) => {
try { await approveInviteWithdrawal(wid); message.success('审核通过'); fetchInviteData(); } catch {}
try { await approveInviteWithdrawal(wid); message.success('审核通过'); fetchWithdrawals(); fetchStats(); } catch {}
};
const handleReject = async () => {
@@ -99,31 +101,31 @@ const ActivityDetail: React.FC = () => {
message.success('已拒绝');
setRejectModalOpen(false);
setRejectReason('');
fetchInviteData();
fetchWithdrawals();
fetchStats();
} catch {}
};
const handlePay = async (wid: number) => {
try { await payInviteWithdrawal(wid); message.success('已确认打款'); fetchInviteData(); } catch {}
try { await payInviteWithdrawal(wid); message.success('已确认打款'); fetchWithdrawals(); fetchStats(); } 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>
);
}
const handleToggle = async (enabled: boolean) => {
try {
await updateInviteConfig({ enabled });
setConfig({ ...config, enabled });
message.success(enabled ? '已启用' : '已停用');
} catch {}
};
if (loading) return <Card loading />;
if (!activity) return <Card></Card>;
const handleSaveConfig = async () => {
try {
const values = await form.validateFields();
await updateInviteConfig(values);
message.success('配置已保存');
setConfig({ ...config, ...values });
} catch {}
};
const invColumns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
@@ -181,34 +183,7 @@ const ActivityDetail: React.FC = () => {
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',
key: 'overview',
label: '数据概览',
children: (
<div>
@@ -238,13 +213,45 @@ const ActivityDetail: React.FC = () => {
pagination={{ current: wdPage, total: wdTotal, pageSize: 10, onChange: setWdPage }} />
),
},
{
key: 'config',
label: '活动配置',
children: (
<Card loading={configLoading}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<span style={{ fontSize: 16, fontWeight: 600 }}></span>
<Switch checked={config?.enabled} onChange={handleToggle} checkedChildren="启用" unCheckedChildren="停用" />
</div>
<Form form={form} layout="vertical" style={{ maxWidth: 500 }}>
<Form.Item label="首单返现比例" name="firstOrderRate" rules={[{ required: true }]}>
<InputNumber min={0} max={1} step={0.01} addonAfter="%" style={{ width: '100%' }}
formatter={v => `${(Number(v) * 100).toFixed(1)}`} parser={v => Number(v?.replace('%', '')) / 100} />
</Form.Item>
<Form.Item label="二单返现比例" name="secondOrderRate" rules={[{ required: true }]}>
<InputNumber min={0} max={1} step={0.001} addonAfter="%" style={{ width: '100%' }}
formatter={v => `${(Number(v) * 100).toFixed(1)}`} parser={v => Number(v?.replace('%', '')) / 100} />
</Form.Item>
<Form.Item label="最低返现金额" name="minCashback" rules={[{ required: true }]}>
<InputNumber min={0} step={0.01} prefix="¥" style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="最高返现金额" name="maxCashback" rules={[{ required: true }]}>
<InputNumber min={0} step={1} prefix="¥" style={{ width: '100%' }} />
</Form.Item>
<Form.Item label="提现门槛" name="withdrawThreshold" rules={[{ required: true }]}>
<InputNumber min={0} step={1} prefix="¥" style={{ width: '100%' }} />
</Form.Item>
<Form.Item>
<Button type="primary" onClick={handleSaveConfig}></Button>
</Form.Item>
</Form>
</Card>
),
},
];
return (
<div>
<div style={{ marginBottom: 16 }}>
<Button onClick={() => navigate('/activity/list')}></Button>
</div>
<h2 style={{ marginBottom: 16 }}></h2>
<Card>
<Tabs items={tabItems} />
</Card>
@@ -255,4 +262,4 @@ const ActivityDetail: React.FC = () => {
);
};
export default ActivityDetail;
export default InviteManage;
@@ -1,11 +1,88 @@
import React from 'react';
import { Empty } from 'antd';
import React, { useEffect, useState } from 'react';
import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd';
import { getServiceFeeConfig, updateServiceFeeConfig } from '@/api/config';
const SystemSettings: React.FC = () => (
<div>
<h2 style={{ marginBottom: 24 }}></h2>
<Empty description="系统设置功能开发中" />
</div>
);
const SystemSettings: React.FC = () => {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [form] = Form.useForm();
export default SystemSettings;
const fetchConfig = async () => {
setLoading(true);
try {
const res: any = await getServiceFeeConfig();
form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 });
} catch (e) {
form.setFieldsValue({ serviceFeeRate: 0.05 });
} finally {
setLoading(false);
}
};
useEffect(() => { fetchConfig(); }, []);
const handleSave = async () => {
try {
const values = await form.validateFields();
setSaving(true);
await updateServiceFeeConfig(values.serviceFeeRate);
message.success('服务费配置已保存');
fetchConfig();
} catch (e: any) {
if (e?.message) message.error(e.message);
} finally {
setSaving(false);
}
};
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
return (
<div>
<h2 style={{ marginBottom: 24 }}></h2>
<Card title="服务费配置" style={{ maxWidth: 600 }}>
<Form form={form} layout="vertical">
<Form.Item
label="软件服务费比例"
name="serviceFeeRate"
rules={[
{ required: true, message: '请输入服务费比例' },
{ type: 'number', min: 0, max: 1, message: '比例必须在0-1之间' },
]}
extra="用户支付金额中,该比例将作为软件服务费归平台所有,剩余部分为商家预计收入"
>
<InputNumber
min={0}
max={1}
step={0.01}
precision={4}
style={{ width: '100%' }}
formatter={(v) => `${(Number(v) * 100).toFixed(2)}%`}
parser={(v) => Number(v?.replace('%', '')) / 100}
/>
</Form.Item>
<Form.Item>
<Button type="primary" loading={saving} onClick={handleSave}>
</Button>
</Form.Item>
</Form>
<Divider />
<div style={{ color: '#666', fontSize: 14 }}>
<p><strong></strong></p>
<p> = × </p>
<p> = - </p>
<p style={{ marginTop: 12 }}>
<strong></strong> ¥100.00 5%
</p>
<p> = ¥5.00 = ¥95.00</p>
</div>
</Card>
</div>
);
};
export default SystemSettings;
@@ -1,78 +0,0 @@
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;
@@ -100,19 +100,8 @@ const Earnings: React.FC = () => {
<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}
title="服务费收入"
value={data?.totalServiceFee || 0}
precision={2}
prefix="¥"
valueStyle={{ color: '#722ed1' }}
@@ -71,8 +71,6 @@ const Withdrawals: React.FC = () => {
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 },