chore(依赖): 添加项目依赖和构建配置

- 添加 Zustand 状态管理库
- 配置开发和生产环境变量
- 更新 Vite 构建配置
- 更新 Git 钩子配置
- 修复 NavBar 组件的 React Hooks 规则问题
This commit is contained in:
gaoziman 2025-11-18 20:44:07 +08:00
parent c8c20299d9
commit 3ea18a417d
8 changed files with 14296 additions and 1391 deletions

10
.env.development Normal file
View File

@ -0,0 +1,10 @@
# 开发环境配置
# API 基础地址(使用代理)
VITE_API_BASE_URL=/api
# API 请求超时时间(毫秒)
VITE_API_TIMEOUT=30000
# 应用标题
VITE_APP_TITLE=Codernew React Pro

10
.env.production Normal file
View File

@ -0,0 +1,10 @@
# 生产环境配置
# API 基础地址(生产环境需要修改为实际地址)
VITE_API_BASE_URL=http://localhost:8080
# API 请求超时时间(毫秒)
VITE_API_TIMEOUT=30000
# 应用标题
VITE_APP_TITLE=Codernew React Pro

View File

@ -4,3 +4,4 @@
npm run pre-commit npm run pre-commit
npm run pre-commit npm run pre-commit
npm run pre-commit npm run pre-commit
npm run pre-commit

12585
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,8 @@
"react-redux": "^7.2.6", "react-redux": "^7.2.6",
"react-router": "^5.2.0", "react-router": "^5.2.0",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"redux": "^4.1.2" "redux": "^4.1.2",
"zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@arco-design/webpack-plugin": "^1.6.0", "@arco-design/webpack-plugin": "^1.6.0",

View File

@ -1,7 +1,6 @@
import React, { useContext, useEffect } from 'react'; import React, { useContext } from 'react';
import { import {
Tooltip, Tooltip,
Input,
Avatar, Avatar,
Select, Select,
Dropdown, Dropdown,
@ -10,9 +9,9 @@ import {
Message, Message,
Button, Button,
} from '@arco-design/web-react'; } from '@arco-design/web-react';
import cs from 'classnames';
import { import {
IconLanguage, IconLanguage,
IconNotification,
IconSunFill, IconSunFill,
IconMoonFill, IconMoonFill,
IconUser, IconUser,
@ -20,33 +19,56 @@ import {
IconPoweroff, IconPoweroff,
IconLoading, IconLoading,
} from '@arco-design/web-react/icon'; } from '@arco-design/web-react/icon';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { GlobalState } from '@/store';
import { GlobalContext } from '@/context'; import { GlobalContext } from '@/context';
import useLocale from '@/utils/useLocale'; import useLocale from '@/utils/useLocale';
import Logo from '@/assets/logo.svg'; import Logo from '@/assets/logo.svg';
import MessageBox from '@/components/MessageBox';
import IconButton from './IconButton'; import IconButton from './IconButton';
import Settings from '../Settings'; import Settings from '../Settings';
import styles from './style/index.module.less'; import styles from './style/index.module.less';
import defaultLocale from '@/locale'; import defaultLocale from '@/locale';
import useStorage from '@/utils/useStorage'; import useStorage from '@/utils/useStorage';
import { generatePermission } from '@/routes'; import { useUserStore } from '@/store/userStore';
function Navbar({ show }: { show: boolean }) { interface NavBreadcrumbNode {
key: string;
name: string;
icon?: React.ReactNode;
}
interface NavbarProps {
show: boolean;
breadcrumb?: NavBreadcrumbNode[];
onBreadcrumbClick?: (node: NavBreadcrumbNode, index: number) => void;
}
function Navbar({ show, breadcrumb = [], onBreadcrumbClick }: NavbarProps) {
const t = useLocale(); const t = useLocale();
const history = useHistory(); const history = useHistory();
const { userInfo, userLoading } = useSelector((state: GlobalState) => state);
const dispatch = useDispatch();
const [_, setUserStatus] = useStorage('userStatus'); // 使用 Zustand 获取用户信息
const [role, setRole] = useStorage('userRole', 'admin'); const { userInfo, userLoading, clearUserInfo } = useUserStore();
const [, setUserStatus] = useStorage('userStatus');
const { setLang, lang, theme, setTheme } = useContext(GlobalContext); const { setLang, lang, theme, setTheme } = useContext(GlobalContext);
// Hooks 必须在所有条件语句之前调用
const [avatarError, setAvatarError] = React.useState(false);
// 使用 Zustand 后,数据已自动持久化,无需手动合并
const avatar = userInfo?.avatar;
const nickname =
userInfo?.nickname || userInfo?.name || userInfo?.username || '';
// 当 avatar 改变时,重置 avatarError
React.useEffect(() => {
setAvatarError(false);
}, [avatar]);
function logout() { function logout() {
setUserStatus('logout'); setUserStatus('logout');
clearUserInfo(); // 清除 Zustand 中的用户信息
window.location.href = '/login'; window.location.href = '/login';
} }
@ -58,18 +80,6 @@ function Navbar({ show }: { show: boolean }) {
} }
} }
useEffect(() => {
dispatch({
type: 'update-userInfo',
payload: {
userInfo: {
...userInfo,
permissions: generatePermission(role),
},
},
});
}, [role]);
if (!show) { if (!show) {
return ( return (
<div className={styles['fixed-settings']}> <div className={styles['fixed-settings']}>
@ -103,14 +113,43 @@ function Navbar({ show }: { show: boolean }) {
<Logo /> <Logo />
<div className={styles['logo-name']}>Codernew React Pro</div> <div className={styles['logo-name']}>Codernew React Pro</div>
</div> </div>
{breadcrumb.length > 0 && (
<div className={styles['navbar-breadcrumb']}>
{breadcrumb.map((node, index) => {
const label = t[node.name] || node.name;
return (
<React.Fragment key={`${node.key}-${index}`}>
<span
className={cs(styles['breadcrumb-node'], {
[styles['breadcrumb-clickable']]:
index !== breadcrumb.length - 1,
})}
onClick={() => {
if (
onBreadcrumbClick &&
index !== breadcrumb.length - 1
) {
onBreadcrumbClick(node, index);
}
}}
>
{node.icon && (
<span className={styles['breadcrumb-icon']}>
{node.icon}
</span>
)}
<span>{label}</span>
</span>
{index !== breadcrumb.length - 1 && (
<span className={styles['breadcrumb-divider']}>/</span>
)}
</React.Fragment>
);
})}
</div>
)}
</div> </div>
<ul className={styles.right}> <ul className={styles.right}>
<li>
<Input.Search
className={styles.round}
placeholder={t['navbar.search.placeholder']}
/>
</li>
<li> <li>
<Select <Select
triggerElement={<IconButton icon={<IconLanguage />} />} triggerElement={<IconButton icon={<IconLanguage />} />}
@ -132,11 +171,6 @@ function Navbar({ show }: { show: boolean }) {
}} }}
/> />
</li> </li>
<li>
<MessageBox>
<IconButton icon={<IconNotification />} />
</MessageBox>
</li>
<li> <li>
<Tooltip <Tooltip
content={ content={
@ -152,19 +186,25 @@ function Navbar({ show }: { show: boolean }) {
</Tooltip> </Tooltip>
</li> </li>
<Settings /> <Settings />
{userInfo && (
<li> <li>
<Dropdown droplist={droplist} position="br" disabled={userLoading}> <Dropdown droplist={droplist} position="br" disabled={userLoading}>
<Avatar size={32} style={{ cursor: 'pointer' }}> <Avatar size={32} style={{ cursor: 'pointer' }}>
{userLoading ? ( {userLoading ? (
<IconLoading /> <IconLoading />
) : avatar && !avatarError ? (
<img
alt="avatar"
src={avatar}
onError={() => setAvatarError(true)}
/>
) : nickname ? (
nickname.charAt(0)
) : ( ) : (
<img alt="avatar" src={userInfo.avatar} /> <IconUser />
)} )}
</Avatar> </Avatar>
</Dropdown> </Dropdown>
</li> </li>
)}
</ul> </ul>
</div> </div>
); );

View File

@ -28,4 +28,13 @@ export default defineConfig({
}, },
}, },
}, },
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
}); });

2933
yarn.lock

File diff suppressed because it is too large Load Diff