firsh push
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>平台管理后台 - 短租预订平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@rent/platform-admin",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.5.0",
|
||||
"antd": "^5.22.0",
|
||||
"axios": "^1.7.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"typescript": "^5.5.0",
|
||||
"vite": "^5.4.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { Suspense } from 'react';
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import MainLayout from '@/layouts/MainLayout';
|
||||
import Login from '@/pages/Login';
|
||||
import Dashboard from '@/pages/Dashboard';
|
||||
import MerchantList from '@/pages/MerchantList';
|
||||
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';
|
||||
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
if (!token) return <Navigate to="/login" replace />;
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
const App: React.FC = () => (
|
||||
<ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#1890ff' } }}>
|
||||
<BrowserRouter>
|
||||
<Suspense fallback={<div style={{ textAlign: 'center', padding: 100 }}>加载中...</div>}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={<ProtectedRoute><MainLayout /></ProtectedRoute>}>
|
||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="merchants" element={<MerchantList />} />
|
||||
<Route path="users" element={<UserList />} />
|
||||
<Route path="orders" element={<OrderList />} />
|
||||
<Route path="finance" element={<Finance />} />
|
||||
<Route path="promotions" element={<Promotion />} />
|
||||
<Route path="settings" element={<SystemSettings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
</BrowserRouter>
|
||||
</ConfigProvider>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,56 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
export function loginByPassword(username: string, password: string) {
|
||||
return request.post('/admin/auth/login', { username, password });
|
||||
}
|
||||
|
||||
export function getAdminProfile() {
|
||||
return request.get('/admin/auth/profile');
|
||||
}
|
||||
|
||||
export function changeAdminPassword(data: { oldPassword: string; newPassword: string }) {
|
||||
return request.put('/admin/auth/password', data);
|
||||
}
|
||||
|
||||
// 商家管理
|
||||
export function getMerchantList(params: any) {
|
||||
return request.get('/admin/merchants', { params });
|
||||
}
|
||||
|
||||
export function approveMerchant(id: number) {
|
||||
return request.put(`/admin/merchants/${id}/approve`);
|
||||
}
|
||||
|
||||
export function rejectMerchant(id: number, reason: string) {
|
||||
return request.put(`/admin/merchants/${id}/reject`, { reason });
|
||||
}
|
||||
|
||||
export function freezeMerchant(id: number) {
|
||||
return request.put(`/admin/merchants/${id}/freeze`);
|
||||
}
|
||||
|
||||
export function unfreezeMerchant(id: number) {
|
||||
return request.put(`/admin/merchants/${id}/unfreeze`);
|
||||
}
|
||||
|
||||
// 用户管理
|
||||
export function getUserList(params: any) {
|
||||
return request.get('/admin/users', { params });
|
||||
}
|
||||
|
||||
export function freezeUser(id: number) {
|
||||
return request.put(`/admin/users/${id}/freeze`);
|
||||
}
|
||||
|
||||
export function unfreezeUser(id: number) {
|
||||
return request.put(`/admin/users/${id}/unfreeze`);
|
||||
}
|
||||
|
||||
// 订单管理
|
||||
export function getOrderList(params: any) {
|
||||
return request.get('/admin/orders', { params });
|
||||
}
|
||||
|
||||
export function getOrderDetail(id: number) {
|
||||
return request.get(`/admin/orders/${id}`);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown } from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
ShopOutlined,
|
||||
TeamOutlined,
|
||||
UnorderedListOutlined,
|
||||
WalletOutlined,
|
||||
GiftOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
const menuItems = [
|
||||
{ key: '/dashboard', icon: <DashboardOutlined />, label: '数据概览' },
|
||||
{ key: '/merchants', icon: <ShopOutlined />, label: '商家管理' },
|
||||
{ key: '/users', icon: <TeamOutlined />, label: '用户管理' },
|
||||
{ key: '/orders', icon: <UnorderedListOutlined />, label: '订单管理' },
|
||||
{ key: '/finance', icon: <WalletOutlined />, label: '财务管理' },
|
||||
{ key: '/promotions', icon: <GiftOutlined />, label: '推广管理' },
|
||||
{ key: '/settings', icon: <SettingOutlined />, label: '系统设置' },
|
||||
];
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { userInfo, logout } = useAuthStore();
|
||||
|
||||
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>
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
style={{ borderRight: 0 }}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header style={{ background: '#fff', padding: '0 24px', display: 'flex', justifyContent: 'flex-end', alignItems: 'center', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ key: 'logout', icon: <LogoutOutlined />, label: '退出登录', onClick: logout },
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button type="text">
|
||||
<Avatar icon={<UserOutlined />} style={{ marginRight: 8 }} />
|
||||
{userInfo?.name || '管理员'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Header>
|
||||
<Content style={{ margin: 24, padding: 24, background: '#fff', borderRadius: 8, minHeight: 360, overflow: 'auto' }}>
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './global.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Card, Row, Col, Statistic } from 'antd';
|
||||
import {
|
||||
ArrowUpOutlined,
|
||||
ShopOutlined,
|
||||
TeamOutlined,
|
||||
DollarOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
const Dashboard: React.FC = () => (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>数据概览</h2>
|
||||
<Row gutter={[24, 24]}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="商家总数" value={0} prefix={<ShopOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="用户总数" value={0} prefix={<TeamOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="订单总数" value={0} prefix={<UnorderedListOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="平台收入"
|
||||
value={0}
|
||||
prefix={<DollarOutlined />}
|
||||
suffix="元"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[24, 24]} style={{ marginTop: 24 }}>
|
||||
<Col span={12}>
|
||||
<Card title="近7日订单趋势">
|
||||
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}>
|
||||
图表区域(接入ECharts后展示)
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card title="商家入驻统计">
|
||||
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }}>
|
||||
图表区域(接入ECharts后展示)
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Empty } from 'antd';
|
||||
|
||||
const Finance: React.FC = () => (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>财务管理</h2>
|
||||
<Empty description="财务管理功能开发中" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Finance;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Form, Input, Button, Card, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { loginByPassword } from '@/api/admin';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const setAuth = useAuthStore((s) => s.setAuth);
|
||||
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await loginByPassword(values.username, values.password);
|
||||
setAuth(res.data.accessToken, res.data.adminInfo);
|
||||
message.success('登录成功');
|
||||
navigate('/dashboard');
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '登录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', background: '#001529' }}>
|
||||
<Card style={{ width: 400 }} title={<div style={{ textAlign: 'center', fontSize: 20, fontWeight: 600, color: '#1890ff' }}>平台管理后台</div>}>
|
||||
<Form onFinish={onFinish} size="large">
|
||||
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
|
||||
<Input prefix={<UserOutlined />} placeholder="管理员用户名" />
|
||||
</Form.Item>
|
||||
<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
|
||||
<Input.Password prefix={<LockOutlined />} placeholder="密码" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Button, Space, Select, Modal, Input, message, Popconfirm } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getMerchantList, approveMerchant, rejectMerchant, freezeMerchant, unfreezeMerchant } from '@/api/admin';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const statusMap: Record<string, { color: string; label: string }> = {
|
||||
pending: { color: 'gold', label: '待审核' },
|
||||
approved: { color: 'green', label: '已通过' },
|
||||
rejected: { color: 'red', label: '已拒绝' },
|
||||
frozen: { color: 'default', label: '已冻结' },
|
||||
};
|
||||
|
||||
const MerchantList: 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 [rejectModal, setRejectModal] = useState<{ visible: boolean; id: number | null; reason: string }>({ visible: false, id: null, reason: '' });
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getMerchantList({ 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) => {
|
||||
await approveMerchant(id);
|
||||
message.success('已通过审核');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!rejectModal.reason) { message.warning('请填写拒绝原因'); return; }
|
||||
await rejectMerchant(rejectModal.id!, rejectModal.reason);
|
||||
message.success('已拒绝');
|
||||
setRejectModal({ visible: false, id: null, reason: '' });
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleFreeze = async (id: number) => {
|
||||
await freezeMerchant(id);
|
||||
message.success('已冻结');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleUnfreeze = async (id: number) => {
|
||||
await unfreezeMerchant(id);
|
||||
message.success('已解冻');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '店铺名称', dataIndex: 'shopName', width: 160, ellipsis: true },
|
||||
{ title: '联系电话', dataIndex: 'phone', width: 130 },
|
||||
{ title: '城市', dataIndex: 'city', width: 100 },
|
||||
{ title: '评分', dataIndex: 'rating', width: 80 },
|
||||
{ title: '评价数', dataIndex: 'reviewCount', width: 80 },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 100,
|
||||
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
|
||||
},
|
||||
{ title: '入驻时间', dataIndex: 'createdAt', width: 180 },
|
||||
{
|
||||
title: '操作', width: 260, fixed: 'right',
|
||||
render: (_, r) => (
|
||||
<Space size="small">
|
||||
{r.status === 'pending' && (
|
||||
<>
|
||||
<Button type="primary" size="small" onClick={() => handleApprove(r.id)}>通过</Button>
|
||||
<Button danger size="small" onClick={() => setRejectModal({ visible: true, id: r.id, reason: '' })}>拒绝</Button>
|
||||
</>
|
||||
)}
|
||||
{r.status === 'approved' && (
|
||||
<Popconfirm title="确定冻结该商家?" onConfirm={() => handleFreeze(r.id)}>
|
||||
<Button danger size="small">冻结</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{r.status === 'frozen' && (
|
||||
<Button type="primary" size="small" onClick={() => handleUnfreeze(r.id)}>解冻</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={rejectModal.visible} onOk={handleReject} onCancel={() => setRejectModal({ visible: false, id: null, reason: '' })}>
|
||||
<Input.TextArea rows={3} placeholder="请输入拒绝原因" value={rejectModal.reason} onChange={(e) => setRejectModal({ ...rejectModal, reason: e.target.value })} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MerchantList;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Select, Space, Input } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getOrderList } from '@/api/admin';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const statusMap: Record<string, { color: string; label: string }> = {
|
||||
pending_pay: { color: 'gold', label: '待支付' },
|
||||
pending_confirm: { color: 'blue', label: '待确认' },
|
||||
pending_checkin: { color: 'cyan', label: '待入住' },
|
||||
checked_in: { color: 'orange', label: '已入住' },
|
||||
completed: { color: 'green', label: '已完成' },
|
||||
cancelled: { color: 'default', label: '已取消' },
|
||||
refunding: { color: 'red', label: '退款中' },
|
||||
refunded: { color: 'purple', label: '已退款' },
|
||||
};
|
||||
|
||||
const OrderList: 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 fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getOrderList({ page, pageSize: 10, status: status || undefined });
|
||||
setData(res.data?.list || []);
|
||||
setTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [page, status]);
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: '订单号', dataIndex: 'orderNo', width: 200 },
|
||||
{ title: '用户', dataIndex: ['user', 'nickname'], width: 100 },
|
||||
{ title: '商家', dataIndex: ['merchant', 'shopName'], width: 140, ellipsis: true },
|
||||
{ title: '房源', dataIndex: ['room', 'name'], width: 140, ellipsis: true },
|
||||
{ title: '入住', dataIndex: 'checkInDate', width: 110 },
|
||||
{ title: '离店', dataIndex: 'checkOutDate', width: 110 },
|
||||
{ title: '金额', dataIndex: 'payAmount', width: 100, render: (v) => `¥${v}` },
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 100,
|
||||
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
|
||||
},
|
||||
{ title: '下单时间', dataIndex: 'createdAt', width: 180 },
|
||||
];
|
||||
|
||||
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 }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderList;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Empty } from 'antd';
|
||||
|
||||
const Promotion: React.FC = () => (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>推广管理</h2>
|
||||
<Empty description="推广管理功能开发中" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Promotion;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Empty } from 'antd';
|
||||
|
||||
const SystemSettings: React.FC = () => (
|
||||
<div>
|
||||
<h2 style={{ marginBottom: 24 }}>系统设置</h2>
|
||||
<Empty description="系统设置功能开发中" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default SystemSettings;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Table, Tag, Button, Space, Select, Input, Popconfirm, message } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { getUserList, freezeUser, unfreezeUser } from '@/api/admin';
|
||||
|
||||
const { Option } = Select;
|
||||
const { Search } = Input;
|
||||
|
||||
const statusMap: Record<string, { color: string; label: string }> = {
|
||||
active: { color: 'green', label: '正常' },
|
||||
frozen: { color: 'red', label: '已冻结' },
|
||||
deleted: { color: 'default', label: '已注销' },
|
||||
};
|
||||
|
||||
const UserList: React.FC = () => {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [role, setRole] = useState<string>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res: any = await getUserList({ page, pageSize: 10, keyword: keyword || undefined, role: role || undefined });
|
||||
setData(res.data?.list || []);
|
||||
setTotal(res.data?.total || 0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [page, role]);
|
||||
|
||||
const handleFreeze = async (id: number) => {
|
||||
await freezeUser(id);
|
||||
message.success('已冻结');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleUnfreeze = async (id: number) => {
|
||||
await unfreezeUser(id);
|
||||
message.success('已解冻');
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setKeyword(value);
|
||||
setPage(1);
|
||||
fetchData();
|
||||
};
|
||||
|
||||
const columns: ColumnsType<any> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||
{ title: '昵称', dataIndex: 'nickname', width: 120, ellipsis: true },
|
||||
{ title: '手机号', dataIndex: 'phone', width: 130 },
|
||||
{
|
||||
title: '角色', dataIndex: 'role', width: 100,
|
||||
render: (r) => <Tag color={r === 'admin' ? 'blue' : r === 'merchant' ? 'orange' : 'default'}>{r === 'admin' ? '管理员' : r === 'merchant' ? '商家' : '用户'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', width: 100,
|
||||
render: (s) => <Tag color={statusMap[s]?.color}>{statusMap[s]?.label || s}</Tag>,
|
||||
},
|
||||
{ title: '注册时间', dataIndex: 'createdAt', width: 180 },
|
||||
{
|
||||
title: '操作', width: 150, fixed: 'right',
|
||||
render: (_, r) => (
|
||||
<Space size="small">
|
||||
{r.status === 'active' && (
|
||||
<Popconfirm title="确定冻结该用户?" onConfirm={() => handleFreeze(r.id)}>
|
||||
<Button danger size="small">冻结</Button>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{r.status === 'frozen' && (
|
||||
<Button type="primary" size="small" onClick={() => handleUnfreeze(r.id)}>解冻</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<h2>用户管理</h2>
|
||||
<Space>
|
||||
<Search placeholder="搜索手机号/昵称" allowClear onSearch={handleSearch} style={{ width: 220 }} />
|
||||
<Select value={role || undefined} placeholder="角色" style={{ width: 120 }} allowClear onChange={(v) => { setRole(v || ''); setPage(1); }}>
|
||||
<Option value="user">用户</Option>
|
||||
<Option value="merchant">商家</Option>
|
||||
<Option value="admin">管理员</Option>
|
||||
</Select>
|
||||
</Space>
|
||||
</div>
|
||||
<Table rowKey="id" columns={columns} dataSource={data} loading={loading} scroll={{ x: 900 }} pagination={{ current: page, total, pageSize: 10, onChange: setPage }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserList;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface AdminInfo {
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
role: string;
|
||||
phone?: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
userInfo: AdminInfo | null;
|
||||
setAuth: (token: string, userInfo: AdminInfo) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
token: localStorage.getItem('admin_token'),
|
||||
userInfo: JSON.parse(localStorage.getItem('admin_userInfo') || 'null'),
|
||||
setAuth: (token, userInfo) => {
|
||||
localStorage.setItem('admin_token', token);
|
||||
localStorage.setItem('admin_userInfo', JSON.stringify(userInfo));
|
||||
set({ token, userInfo });
|
||||
},
|
||||
logout: () => {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_userInfo');
|
||||
set({ token: null, userInfo: null });
|
||||
window.location.href = '/login';
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,34 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
request.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const { data } = response;
|
||||
if (data.code >= 200 && data.code < 300) {
|
||||
return data;
|
||||
}
|
||||
return Promise.reject(new Error(data.message || '请求失败'));
|
||||
},
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_userInfo');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export default request;
|
||||
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user