firsh push

This commit is contained in:
2026-04-21 20:12:05 +08:00
commit 25db7ecd66
165 changed files with 24094 additions and 0 deletions
+13
View File
@@ -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>
+29
View File
@@ -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"
}
}
+44
View File
@@ -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;
+56
View File
@@ -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}`);
}
+15
View File
@@ -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;
+6
View File
@@ -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;
+11
View File
@@ -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;
+48
View File
@@ -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;
+102
View File
@@ -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;
+32
View File
@@ -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';
},
}));
+34
View File
@@ -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
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+24
View File
@@ -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"]
}
+21
View File
@@ -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,
},
},
},
});