登录框架搭建完成
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@@ -21,6 +21,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"nprogress": "^0.2.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -4094,6 +4095,12 @@
|
||||
"integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nprogress": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.1",
|
||||
"@google/genai": "^1.29.0",
|
||||
"@reduxjs/toolkit": "^2.11.2",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
@@ -24,6 +23,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.546.0",
|
||||
"motion": "^12.23.24",
|
||||
"nprogress": "^0.2.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -3,5 +3,6 @@ import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './router';
|
||||
|
||||
export default function App() {
|
||||
return <RouterProvider router={router} />;
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'ax
|
||||
import envConfig from '../../env';
|
||||
import Cookies from 'js-cookie';
|
||||
import { message } from 'antd';
|
||||
import store from '../store';
|
||||
import { logout } from '../store/userSlice';
|
||||
|
||||
// 🔥 删掉这两行!!!
|
||||
// import store from '../store';
|
||||
// import { logout } from '../store/userSlice';
|
||||
|
||||
const API_BASE_URL = envConfig.baseURL;
|
||||
|
||||
@@ -13,19 +14,15 @@ const api = axios.create({
|
||||
timeout: 20000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
},
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// 请求拦截器:自动添加token
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
// 从 localStorage 获取 token
|
||||
const token = localStorage.getItem('token');
|
||||
if (token && config.headers) {
|
||||
// 将 token 添加到请求头
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
@@ -38,43 +35,23 @@ api.interceptors.request.use(
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
// 处理成功响应
|
||||
const { data } = response;
|
||||
// 如果后端规范定义了 code,可以在这里统一处理
|
||||
// if (data.code !== 200) {
|
||||
// message.error(data.message || '请求失败');
|
||||
// return Promise.reject(new Error(data.message || 'Error'));
|
||||
// }
|
||||
return data;
|
||||
return response.data;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// 处理错误响应
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,清除本地信息并跳转到登录页
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('isAuthenticated');
|
||||
localStorage.removeItem('user');
|
||||
store.dispatch(logout());
|
||||
window.location.href = '/login';
|
||||
message.error('会话已过期,请重新登录');
|
||||
break;
|
||||
case 403:
|
||||
message.error('拒绝访问:权限不足');
|
||||
break;
|
||||
case 404:
|
||||
message.error('资源不存在');
|
||||
break;
|
||||
case 500:
|
||||
message.error('服务器内部错误');
|
||||
break;
|
||||
default:
|
||||
message.error(error.message || '网络错误');
|
||||
}
|
||||
} else {
|
||||
message.error('连接服务器失败');
|
||||
async (error: AxiosError) => { // 👈 加 async
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('isAuthenticated');
|
||||
localStorage.removeItem('user');
|
||||
|
||||
// 🔥 🔥 🔥 在这里动态导入!!!
|
||||
const store = await import('../store');
|
||||
const { logout } = await import('../store/userSlice');
|
||||
|
||||
store.default.dispatch(logout());
|
||||
window.location.href = '/login';
|
||||
message.error('会话已过期,请重新登录');
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
ConfigProvider,
|
||||
theme,
|
||||
Breadcrumb,
|
||||
Badge
|
||||
Badge,
|
||||
message
|
||||
} from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
@@ -26,6 +27,8 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { MOCK_USER } from '../constants';
|
||||
import logo from '../assets/logo.png';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../store';
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
export default function AppLayout() {
|
||||
@@ -33,10 +36,7 @@ export default function AppLayout() {
|
||||
const [time, setTime] = useState(new Date().toLocaleString());
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// 从 localStorage 获取真实用户信息
|
||||
const userJson = localStorage.getItem('user');
|
||||
const user = userJson ? JSON.parse(userJson) : MOCK_USER;
|
||||
const { userInfo } = useSelector((state: RootState) => state.user);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setTime(new Date().toLocaleString()), 1000);
|
||||
@@ -61,7 +61,7 @@ export default function AppLayout() {
|
||||
{ key: '/serialNumGenerate', icon: <UserOutlined />, label: '序列号生成', roles: ['admin'] },
|
||||
];
|
||||
|
||||
const filteredMenu = menuItems.filter(item => item.roles.includes(user.role));
|
||||
const filteredMenu = menuItems.filter(item => item.roles.includes(userInfo.role));
|
||||
|
||||
const currentPathLabel = menuItems.find(i => i.key === location.pathname)?.label || '概览控制台';
|
||||
|
||||
@@ -85,6 +85,7 @@ export default function AppLayout() {
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider trigger={null} collapsible collapsed={collapsed} width={220} theme="dark">
|
||||
<div className="h-16 flex items-center justify-center border-b border-white/10 overflow-hidden">
|
||||
@@ -100,11 +101,7 @@ export default function AppLayout() {
|
||||
items={filteredMenu}
|
||||
onClick={({ key }) => navigate(key)}
|
||||
/>
|
||||
{!collapsed && (
|
||||
<div className="absolute bottom-4 left-4 right-4 p-2 bg-white/5 rounded text-[12px] text-gray-400">
|
||||
当前权限: <span className="text-white font-bold">{user.role === 'admin' ? '系统管理员' : user.role === 'operator' ? '操作员' : '观察员'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Sider>
|
||||
<Layout>
|
||||
<Header className="px-6 flex items-center justify-between shadow-sm border-b border-gray-100 h-16 bg-white shrink-0 z-10">
|
||||
@@ -127,8 +124,7 @@ export default function AppLayout() {
|
||||
<div className="flex items-center gap-3 pr-6 border-r border-gray-100">
|
||||
<span className="text-gray-400 font-mono text-[13px]">{time}</span>
|
||||
<Badge status="success" />
|
||||
<span className="text-gray-800 font-medium">{user.name}</span>
|
||||
<Avatar src={user.avatar} size="small" icon={<UserOutlined />} />
|
||||
<span className="text-gray-800 font-medium">{userInfo.username}</span>
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
|
||||
@@ -1,14 +1,53 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const whiteList = ['/login', '/auth-redirect', '/bind', '/register', '/oauthLogin'];
|
||||
|
||||
export default function ProtectedRoute() {
|
||||
const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true';
|
||||
//const { isAuthenticated } = useSelector(state => state.user);
|
||||
const { isAuthenticated } = useSelector((state) => state.user);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { pathname, search } = location;
|
||||
|
||||
|
||||
|
||||
// 白名单
|
||||
if (whiteList.includes(pathname)) {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
// 未登录
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
return <Navigate to={`/login?redirect=${pathname}`} replace />;
|
||||
}
|
||||
|
||||
// 已登录,访问登录页 → 跳首页
|
||||
if (pathname === '/login') {
|
||||
const queryParams = new URLSearchParams(search);
|
||||
const goRemote = queryParams.get('goRemote');
|
||||
const clear = queryParams.get('clear');
|
||||
|
||||
if (goRemote) {
|
||||
if (clear === '1') {
|
||||
Cookies.remove('userId');
|
||||
window.location.href = 'http://ri.satabot.com/#/login?goRemote=1';
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawUserId = Cookies.get('userId');
|
||||
if (!rawUserId) {
|
||||
navigate('/');
|
||||
} else {
|
||||
const encoded = btoa(unescape(encodeURIComponent(rawUserId)));
|
||||
window.location.href = `http://ai.satabot.com/?userId=${encoded}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
|
||||
@@ -21,5 +21,12 @@
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-bg text-text-main font-sans;
|
||||
width:100vm;
|
||||
height: 100vh;
|
||||
}
|
||||
#root{
|
||||
width:100%;
|
||||
height:100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
const TokenKey = 'Admin-Token'
|
||||
const TokenKeyAi = 'ai-Token'
|
||||
const UserId = 'userId'
|
||||
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(TokenKey)
|
||||
}
|
||||
|
||||
export function setToken(token) {
|
||||
return Cookies.set(TokenKey, token)
|
||||
|
||||
}
|
||||
export function setTokenAi(token) {
|
||||
return Cookies.set(TokenKeyAi, token, {
|
||||
domain: "ai.satabot.com",
|
||||
path: "/"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import { Provider } from 'react-redux';
|
||||
import { store } from './store';
|
||||
import { store } from './store/index.ts';
|
||||
import { App as Appantd } from 'antd';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<Provider store={store}>
|
||||
<StrictMode>
|
||||
<App />
|
||||
<Appantd>
|
||||
<App />
|
||||
|
||||
</Appantd>
|
||||
</StrictMode>,
|
||||
</Provider>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Card,
|
||||
message,
|
||||
Checkbox,
|
||||
App,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
@@ -17,47 +18,48 @@ import { motion } from 'motion/react';
|
||||
import bgcImg from '../assets/bgc2.png';
|
||||
import logincard from '../assets/logincard.png';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { login } from '../store/userSlice';
|
||||
import { setUserInfo, userLogin } from '../store/userSlice';
|
||||
import { useAppDispatch } from '../store';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
const { message, modal, notification } = App.useApp();
|
||||
|
||||
|
||||
|
||||
// 账号密码登录
|
||||
const onFinish = (values: any) => {
|
||||
console.log('Login attempt:', values);
|
||||
const onFinish = async (values: any) => {
|
||||
try {
|
||||
// 👇 调用 dispatch,并用 unwrap() 拿到 return 的数据
|
||||
const loginResult = await dispatch(userLogin(values)).unwrap();
|
||||
|
||||
let role = 'viewer';
|
||||
let name = '观察员';
|
||||
// loginResult 就是你 userLogin 里 return 的 res
|
||||
console.log("登录成功返回的数据:", loginResult);
|
||||
|
||||
if (values.username === 'admin') {
|
||||
role = 'admin';
|
||||
name = '系统管理员';
|
||||
} else if (values.username === 'operator') {
|
||||
role = 'operator';
|
||||
name = '操作员';
|
||||
if (loginResult.code == 200) {
|
||||
//dispatch(setUserInfo(loginResult))
|
||||
|
||||
message.success(`登录成功`);
|
||||
navigate('/');
|
||||
} else {
|
||||
|
||||
message.error("登录失败:" + loginResult.msg);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 登录成功后跳转
|
||||
|
||||
} catch (err) {
|
||||
// 接口失败、密码错误都会进这里
|
||||
//console.error("登录失败:", err);
|
||||
//message.error('登录失败,请检查账号密码');
|
||||
}
|
||||
|
||||
const userInfo = {
|
||||
username: values.username,
|
||||
name: name,
|
||||
role: role,
|
||||
avatar: `https://api.dicebear.com7.x/avataaars/svg?seed=${values.username}`
|
||||
};
|
||||
|
||||
localStorage.setItem('isAuthenticated', 'true');
|
||||
localStorage.setItem('token', 'mock_token_' + Date.now());
|
||||
localStorage.setItem('user', JSON.stringify(userInfo));
|
||||
|
||||
message.success(`欢迎回来, ${name}`);
|
||||
dispatch(login({
|
||||
token: 'mock_token_' + Date.now(),
|
||||
userInfo: userInfo
|
||||
}));
|
||||
|
||||
navigate('/');
|
||||
//navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import userReducer from './userSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
user: userReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export default store;
|
||||
18
src/store/index.ts
Normal file
18
src/store/index.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
// 👇 保留 import,但是把类型定义放到 store 后面
|
||||
import userReducer from './userSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
user: userReducer,
|
||||
},
|
||||
});
|
||||
|
||||
// 🔥 核心:类型必须放在 store 后面!
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
|
||||
export default store;
|
||||
13
src/store/types.ts
Normal file
13
src/store/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
// store/types.ts
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import userReducer from './userSlice';
|
||||
|
||||
// 先创建一个临时 reducer 用于类型推导,不导出
|
||||
const dummyStore = configureStore({
|
||||
reducer: {
|
||||
user: userReducer,
|
||||
},
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof dummyStore.getState>;
|
||||
export type AppDispatch = typeof dummyStore.dispatch;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
const initialState = {
|
||||
token: localStorage.getItem('token') || null,
|
||||
userInfo: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')) : null,
|
||||
isAuthenticated: localStorage.getItem('isAuthenticated') === 'true',
|
||||
};
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
// 登录
|
||||
login: (state, action) => {
|
||||
state.token = action.payload.token;
|
||||
state.userInfo = action.payload.userInfo;
|
||||
state.isAuthenticated = true;
|
||||
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
localStorage.setItem('user', JSON.stringify(action.payload.userInfo));
|
||||
localStorage.setItem('isAuthenticated', 'true');
|
||||
},
|
||||
// 退出
|
||||
logout: (state) => {
|
||||
state.token = null;
|
||||
state.userInfo = null;
|
||||
state.isAuthenticated = false;
|
||||
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
localStorage.removeItem('isAuthenticated');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { login, logout } = userSlice.actions;
|
||||
export default userSlice.reducer;
|
||||
77
src/store/userSlice.ts
Normal file
77
src/store/userSlice.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { login } from '@/api/login';
|
||||
import Cookies from 'js-cookie';
|
||||
import { setToken, setTokenAi } from '@/lib/utils';
|
||||
|
||||
export const userLogin = createAsyncThunk(
|
||||
'user/login',
|
||||
async (userInfo: any, { rejectWithValue }) => {
|
||||
try {
|
||||
const data = {
|
||||
username: userInfo.username.trim(),
|
||||
password: userInfo.password,
|
||||
sourceType: 1
|
||||
}
|
||||
|
||||
const res = await login(data);
|
||||
|
||||
localStorage.setItem('username', userInfo.username.trim());
|
||||
localStorage.setItem('password', userInfo.password);
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const goRemote = searchParams.get('goRemote');
|
||||
|
||||
if (goRemote) {
|
||||
setTokenAi(res.token);
|
||||
} else {
|
||||
setToken(res.token);
|
||||
}
|
||||
|
||||
document.cookie = `sharetoken=${res.token}; path=/; domain=.satabot.com; secure`;
|
||||
localStorage.setItem('token', res.token);
|
||||
Cookies.set('userId', res.userId);
|
||||
|
||||
return res;
|
||||
|
||||
} catch (err) {
|
||||
return rejectWithValue(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
token: localStorage.getItem('token') || null,
|
||||
userInfo: null,
|
||||
isAuthenticated: !!localStorage.getItem('token'),
|
||||
};
|
||||
|
||||
const userSlice = createSlice({
|
||||
name: 'user',
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.token = null;
|
||||
state.userInfo = null;
|
||||
state.isAuthenticated = false;
|
||||
localStorage.clear();
|
||||
Cookies.remove('userId');
|
||||
},
|
||||
setUserInfo: (state, action) => {
|
||||
state.userInfo = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(userLogin.fulfilled, (state, action) => {
|
||||
state.userInfo = action.payload;
|
||||
//state.token = action.payload.token;
|
||||
state.isAuthenticated = true;
|
||||
})
|
||||
.addCase(userLogin.rejected, (state, action) => {
|
||||
console.error('登录失败', action.payload);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { logout, setUserInfo } = userSlice.actions;
|
||||
export default userSlice.reducer;
|
||||
@@ -1,24 +1,25 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig, loadEnv} from 'vite';
|
||||
import path, { resolve } from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
|
||||
export default defineConfig(({mode}) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
},
|
||||
};
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
define: {
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
//'@': path.resolve(__dirname, '.'),
|
||||
'@': resolve('src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user