feat: 迭代

This commit is contained in:
2026-06-01 09:36:52 +08:00
parent e8bce5e924
commit f021b43f05
38 changed files with 1785 additions and 88 deletions
+2
View File
@@ -29,6 +29,7 @@ import PlatformWithdrawals from '@/pages/finance/PlatformWithdrawals';
import BankCards from '@/pages/finance/BankCards';
import InviteManage from '@/pages/InviteManage';
import ReviewManage from '@/pages/ReviewManage';
import AdminManage from '@/pages/AdminManage';
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const token = localStorage.getItem('admin_token');
@@ -71,6 +72,7 @@ const App: React.FC = () => (
<Route path="coupons/create" element={<CouponForm />} />
<Route path="coupons/edit/:id" element={<CouponForm />} />
<Route path="promotions" element={<Promotion />} />
<Route path="admins" element={<AdminManage />} />
<Route path="settings" element={<SystemSettings />} />
</Route>
</Routes>
+69
View File
@@ -67,3 +67,72 @@ export function getPlatformStatistics() {
export function getOrderTrend(params: { startDate: string; endDate: string }) {
return request.get('/api/admin/finance/reports/trend', { params });
}
// 管理员管理
export interface Admin {
id: number;
username: string;
name: string;
phone?: string;
email?: string;
role: 'super_admin' | 'admin' | 'operator';
status: 'active' | 'frozen';
lastLoginAt?: string;
lastLoginIp?: string;
createdAt: string;
updatedAt: string;
}
export interface CreateAdminParams {
username: string;
password: string;
name: string;
phone?: string;
email?: string;
role: 'super_admin' | 'admin' | 'operator';
}
export interface UpdateAdminParams {
name?: string;
phone?: string;
email?: string;
role?: 'super_admin' | 'admin' | 'operator';
status?: 'active' | 'frozen';
}
export interface QueryAdminParams {
username?: string;
name?: string;
role?: 'super_admin' | 'admin' | 'operator';
status?: 'active' | 'frozen';
page?: number;
pageSize?: number;
}
export function getAdminList(params: QueryAdminParams) {
return request.get('/api/admin/admins', { params });
}
export function getAdminById(id: number) {
return request.get(`/api/admin/admins/${id}`);
}
export function createAdmin(data: CreateAdminParams) {
return request.post('/api/admin/admins', data);
}
export function updateAdmin(id: number, data: UpdateAdminParams) {
return request.put(`/api/admin/admins/${id}`, data);
}
export function updateAdminPassword(id: number, password: string) {
return request.put(`/api/admin/admins/${id}/password`, { password });
}
export function toggleAdminStatus(id: number) {
return request.put(`/api/admin/admins/${id}/toggle-status`);
}
export function deleteAdmin(id: number) {
return request.delete(`/api/admin/admins/${id}`);
}
+7 -1
View File
@@ -6,4 +6,10 @@ export const updateServiceFeeConfig = (rate: number) => request.put('/api/admin/
export const getStorageConfig = () => request.get('/api/admin/config/storage');
export const updateStorageConfig = (data: Record<string, string>) => request.put('/api/admin/config/storage', data);
export const updateStorageConfig = (data: Record<string, string>) => request.put('/api/admin/config/storage', data);
export const getWithdrawConfig = () => request.get('/api/admin/config/withdraw');
export const updateMerchantMinWithdrawAmount = (amount: number) => request.put('/api/admin/config/withdraw/merchant-min', { amount });
export const updatePlatformMinWithdrawAmount = (amount: number) => request.put('/api/admin/config/withdraw/platform-min', { amount });
@@ -21,6 +21,7 @@ import {
BarChartOutlined,
TagOutlined,
CreditCardOutlined,
SafetyOutlined,
} from '@ant-design/icons';
import { useAuthStore } from '@/store/auth';
@@ -53,6 +54,7 @@ const menuItems = [
{ key: '/invite', icon: <TrophyOutlined />, label: '邀请返现' },
{ key: '/coupons', icon: <TagOutlined />, label: '优惠券管理' },
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
{ key: '/admins', icon: <SafetyOutlined />, label: '管理员管理' },
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
];
@@ -0,0 +1,416 @@
import React, { useState } from 'react';
import { Card, Table, Button, Space, Tag, Modal, Form, Input, Select, message, Popconfirm } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, LockOutlined, StopOutlined, CheckCircleOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { formatDateTime } from '@rent/shared-utils';
import {
getAdminList,
createAdmin,
updateAdmin,
updateAdminPassword,
toggleAdminStatus,
deleteAdmin,
type Admin,
type CreateAdminParams,
type UpdateAdminParams,
type QueryAdminParams
} from '@/api/admin';
const ROLE_MAP = {
super_admin: { label: '超级管理员', color: 'red' },
admin: { label: '管理员', color: 'blue' },
operator: { label: '运营人员', color: 'green' },
};
const STATUS_MAP = {
active: { label: '正常', color: 'success' },
frozen: { label: '冻结', color: 'error' },
};
const AdminManage: React.FC = () => {
const [loading, setLoading] = useState(false);
const [dataSource, setDataSource] = useState<Admin[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [filters, setFilters] = useState<QueryAdminParams>({});
const [createModalVisible, setCreateModalVisible] = useState(false);
const [editModalVisible, setEditModalVisible] = useState(false);
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
const [currentAdmin, setCurrentAdmin] = useState<Admin | null>(null);
const [createForm] = Form.useForm();
const [editForm] = Form.useForm();
const [passwordForm] = Form.useForm();
React.useEffect(() => {
fetchData();
}, [page, pageSize, filters]);
const fetchData = async () => {
setLoading(true);
try {
const res: any = await getAdminList({ ...filters, page, pageSize });
setDataSource(res.data.items || []);
setTotal(res.data.total || 0);
} catch (error: any) {
message.error(error.response?.data?.message || '获取管理员列表失败');
} finally {
setLoading(false);
}
};
const handleCreate = () => {
createForm.resetFields();
setCreateModalVisible(true);
};
const handleCreateSubmit = async () => {
try {
const values = await createForm.validateFields();
await createAdmin(values);
message.success('创建成功');
setCreateModalVisible(false);
fetchData();
} catch (error: any) {
if (error.response) {
message.error(error.response?.data?.message || '创建失败');
}
}
};
const handleEdit = (record: Admin) => {
setCurrentAdmin(record);
editForm.setFieldsValue({
name: record.name,
phone: record.phone,
email: record.email,
role: record.role,
status: record.status,
});
setEditModalVisible(true);
};
const handleEditSubmit = async () => {
if (!currentAdmin) return;
try {
const values = await editForm.validateFields();
await updateAdmin(currentAdmin.id, values);
message.success('更新成功');
setEditModalVisible(false);
fetchData();
} catch (error: any) {
message.error(error.response?.data?.message || '更新失败');
}
};
const handleResetPassword = (record: Admin) => {
setCurrentAdmin(record);
passwordForm.resetFields();
setPasswordModalVisible(true);
};
const handlePasswordSubmit = async () => {
if (!currentAdmin) return;
try {
const values = await passwordForm.validateFields();
await updateAdminPassword(currentAdmin.id, values.password);
message.success('密码重置成功');
setPasswordModalVisible(false);
} catch (error: any) {
message.error(error.response?.data?.message || '密码重置失败');
}
};
const handleToggleStatus = async (record: Admin) => {
try {
await toggleAdminStatus(record.id);
message.success('状态切换成功');
fetchData();
} catch (error: any) {
message.error(error.response?.data?.message || '状态切换失败');
}
};
const handleDelete = async (record: Admin) => {
try {
await deleteAdmin(record.id);
message.success('删除成功');
fetchData();
} catch (error: any) {
message.error(error.response?.data?.message || '删除失败');
}
};
const columns: ColumnsType<Admin> = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 120,
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 100,
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
width: 120,
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
width: 180,
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
width: 120,
render: (role: string) => {
const roleInfo = ROLE_MAP[role as keyof typeof ROLE_MAP];
return <Tag color={roleInfo?.color}>{roleInfo?.label || role}</Tag>;
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
render: (status: string) => {
const statusInfo = STATUS_MAP[status as keyof typeof STATUS_MAP];
return <Tag color={statusInfo?.color}>{statusInfo?.label || status}</Tag>;
},
},
{
title: '最后登录时间',
dataIndex: 'lastLoginAt',
key: 'lastLoginAt',
width: 180,
render: (date: string) => date ? formatDateTime(date) : '-',
},
{
title: '最后登录IP',
dataIndex: 'lastLoginIp',
key: 'lastLoginIp',
width: 140,
render: (ip: string) => ip || '-',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (date: string) => formatDateTime(date),
},
{
title: '操作',
key: 'action',
width: 280,
fixed: 'right',
render: (_, record) => (
<Space size="small">
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>
</Button>
<Button type="link" size="small" icon={<LockOutlined />} onClick={() => handleResetPassword(record)}>
</Button>
{record.role !== 'super_admin' && (
<>
<Button
type="link"
size="small"
icon={record.status === 'active' ? <StopOutlined /> : <CheckCircleOutlined />}
onClick={() => handleToggleStatus(record)}
>
{record.status === 'active' ? '冻结' : '解冻'}
</Button>
<Popconfirm
title="确定要删除该管理员吗?"
onConfirm={() => handleDelete(record)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</>
)}
</Space>
),
},
];
return (
<div>
<h2 style={{ marginBottom: 24 }}></h2>
<Card>
<Space style={{ marginBottom: 16 }}>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button>
</Space>
<Table
columns={columns}
dataSource={dataSource}
rowKey="id"
loading={loading}
scroll={{ x: 1600 }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (total) => `${total}`,
onChange: (page, pageSize) => {
setPage(page);
setPageSize(pageSize);
},
}}
/>
</Card>
{/* 创建管理员 */}
<Modal
title="新增管理员"
open={createModalVisible}
onOk={handleCreateSubmit}
onCancel={() => setCreateModalVisible(false)}
width={600}
>
<Form form={createForm} layout="vertical">
<Form.Item
name="username"
label="用户名"
rules={[
{ required: true, message: '请输入用户名' },
{ min: 3, max: 50, message: '用户名长度为3-50个字符' },
]}
>
<Input placeholder="请输入用户名" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[
{ required: true, message: '请输入密码' },
{ min: 6, max: 50, message: '密码长度为6-50个字符' },
]}
>
<Input.Password placeholder="请输入密码" />
</Form.Item>
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="phone" label="手机号">
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select placeholder="请选择角色">
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="super_admin"></Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* 编辑管理员 */}
<Modal
title="编辑管理员"
open={editModalVisible}
onOk={handleEditSubmit}
onCancel={() => setEditModalVisible(false)}
width={600}
>
<Form form={editForm} layout="vertical">
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="phone" label="手机号">
<Input placeholder="请输入手机号" />
</Form.Item>
<Form.Item name="email" label="邮箱">
<Input placeholder="请输入邮箱" />
</Form.Item>
<Form.Item
name="role"
label="角色"
rules={[{ required: true, message: '请选择角色' }]}
>
<Select placeholder="请选择角色">
<Select.Option value="admin"></Select.Option>
<Select.Option value="operator"></Select.Option>
<Select.Option value="super_admin"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="status"
label="状态"
rules={[{ required: true, message: '请选择状态' }]}
>
<Select placeholder="请选择状态">
<Select.Option value="active"></Select.Option>
<Select.Option value="frozen"></Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* 重置密码 */}
<Modal
title="重置密码"
open={passwordModalVisible}
onOk={handlePasswordSubmit}
onCancel={() => setPasswordModalVisible(false)}
width={500}
>
<Form form={passwordForm} layout="vertical">
<Form.Item
name="password"
label="新密码"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, max: 50, message: '密码长度为6-50个字符' },
]}
>
<Input.Password placeholder="请输入新密码" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default AdminManage;
@@ -1,20 +1,33 @@
import React, { useEffect, useState } from 'react';
import { Card, Form, InputNumber, Button, message, Spin, Divider } from 'antd';
import { getServiceFeeConfig, updateServiceFeeConfig } from '@/api/config';
import { getServiceFeeConfig, updateServiceFeeConfig, getWithdrawConfig, updateMerchantMinWithdrawAmount, updatePlatformMinWithdrawAmount } from '@/api/config';
import StorageSettings from './StorageSettings';
const SystemSettings: React.FC = () => {
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [savingWithdraw, setSavingWithdraw] = useState(false);
const [form] = Form.useForm();
const [withdrawForm] = Form.useForm();
const fetchConfig = async () => {
setLoading(true);
try {
const res: any = await getServiceFeeConfig();
form.setFieldsValue({ serviceFeeRate: res.data?.rate || 0.05 });
const [feeRes, withdrawRes]: any = await Promise.all([
getServiceFeeConfig(),
getWithdrawConfig(),
]);
form.setFieldsValue({ serviceFeeRate: feeRes.data?.rate || 0.05 });
withdrawForm.setFieldsValue({
merchantMinWithdrawAmount: withdrawRes.data?.merchantMinWithdrawAmount || 100,
platformMinWithdrawAmount: withdrawRes.data?.platformMinWithdrawAmount || 10,
});
} catch (e) {
form.setFieldsValue({ serviceFeeRate: 0.05 });
withdrawForm.setFieldsValue({
merchantMinWithdrawAmount: 100,
platformMinWithdrawAmount: 10,
});
} finally {
setLoading(false);
}
@@ -36,6 +49,23 @@ const SystemSettings: React.FC = () => {
}
};
const handleSaveWithdraw = async () => {
try {
const values = await withdrawForm.validateFields();
setSavingWithdraw(true);
await Promise.all([
updateMerchantMinWithdrawAmount(values.merchantMinWithdrawAmount),
updatePlatformMinWithdrawAmount(values.platformMinWithdrawAmount),
]);
message.success('提现配置已保存');
fetchConfig();
} catch (e: any) {
if (e?.message) message.error(e.message);
} finally {
setSavingWithdraw(false);
}
};
if (loading) return <Spin size="large" style={{ display: 'block', marginTop: 100 }} />;
return (
@@ -83,6 +113,59 @@ const SystemSettings: React.FC = () => {
</div>
</Card>
<Card title="提现配置" style={{ maxWidth: 600, marginBottom: 24 }}>
<Form form={withdrawForm} layout="vertical">
<Form.Item
label="商家提现最低金额"
name="merchantMinWithdrawAmount"
rules={[
{ required: true, message: '请输入商家提现最低金额' },
{ type: 'number', min: 0, message: '金额不能为负数' },
]}
extra="商家申请提现时的最低金额限制"
>
<InputNumber
min={0}
step={10}
precision={2}
style={{ width: '100%' }}
addonAfter="元"
/>
</Form.Item>
<Form.Item
label="平台提现最低金额"
name="platformMinWithdrawAmount"
rules={[
{ required: true, message: '请输入平台提现最低金额' },
{ type: 'number', min: 0, message: '金额不能为负数' },
]}
extra="平台账户申请提现时的最低金额限制"
>
<InputNumber
min={0}
step={10}
precision={2}
style={{ width: '100%' }}
addonAfter="元"
/>
</Form.Item>
<Form.Item>
<Button type="primary" loading={savingWithdraw} onClick={handleSaveWithdraw}>
</Button>
</Form.Item>
</Form>
<Divider />
<div style={{ color: '#666', fontSize: 14 }}>
<p><strong></strong></p>
<p> </p>
<p> </p>
<p> "提现门槛"</p>
</div>
</Card>
<StorageSettings />
</div>
);
@@ -3,6 +3,7 @@ import { Card, Row, Col, Statistic, Descriptions, Button, Space, Modal, Form, In
import { WalletOutlined, ArrowUpOutlined, ArrowDownOutlined, DollarOutlined, ExportOutlined, HistoryOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getPlatformAccounts, createPlatformWithdrawal, getBankCards } from '@/api/finance';
import { getWithdrawConfig } from '@/api/config';
import { formatMoney, formatDateTime } from '@rent/shared-utils';
interface PlatformAccount {
@@ -34,11 +35,13 @@ const PlatformWallet: React.FC = () => {
const [loading, setLoading] = useState(false);
const [withdrawModalVisible, setWithdrawModalVisible] = useState(false);
const [bankCards, setBankCards] = useState<BankCard[]>([]);
const [minWithdrawAmount, setMinWithdrawAmount] = useState(10);
const [form] = Form.useForm();
useEffect(() => {
fetchAccount();
fetchBankCards();
fetchWithdrawConfig();
}, []);
const fetchAccount = async () => {
@@ -67,6 +70,17 @@ const PlatformWallet: React.FC = () => {
}
};
const fetchWithdrawConfig = async () => {
try {
const res: any = await getWithdrawConfig();
if (res.data?.platformMinWithdrawAmount) {
setMinWithdrawAmount(res.data.platformMinWithdrawAmount);
}
} catch (error) {
console.error('获取提现配置失败', error);
}
};
const handleWithdraw = () => {
if (bankCards.length === 0) {
message.warning('请先添加银行卡');
@@ -328,6 +342,9 @@ const PlatformWallet: React.FC = () => {
{ required: true, message: '请输入提现金额' },
{
validator: (_, value) => {
if (value && value < minWithdrawAmount) {
return Promise.reject(`提现金额不能小于最低提现金额(${minWithdrawAmount}元)`);
}
if (value && value > withdrawableAmount) {
return Promise.reject(`提现金额不能大于可提现金额(${withdrawableAmount.toFixed(2)}元)`);
}
@@ -338,11 +355,12 @@ const PlatformWallet: React.FC = () => {
},
},
]}
extra={`最低提现金额:${minWithdrawAmount}`}
>
<InputNumber
style={{ width: '100%' }}
placeholder="请输入提现金额"
min={0}
min={minWithdrawAmount}
max={withdrawableAmount}
precision={2}
addonAfter="元"