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

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-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"redux": "^4.1.2"
"redux": "^4.1.2",
"zustand": "^4.5.2"
},
"devDependencies": {
"@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 {
Tooltip,
Input,
Avatar,
Select,
Dropdown,
@ -10,9 +9,9 @@ import {
Message,
Button,
} from '@arco-design/web-react';
import cs from 'classnames';
import {
IconLanguage,
IconNotification,
IconSunFill,
IconMoonFill,
IconUser,
@ -20,33 +19,56 @@ import {
IconPoweroff,
IconLoading,
} from '@arco-design/web-react/icon';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { GlobalState } from '@/store';
import { GlobalContext } from '@/context';
import useLocale from '@/utils/useLocale';
import Logo from '@/assets/logo.svg';
import MessageBox from '@/components/MessageBox';
import IconButton from './IconButton';
import Settings from '../Settings';
import styles from './style/index.module.less';
import defaultLocale from '@/locale';
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 history = useHistory();
const { userInfo, userLoading } = useSelector((state: GlobalState) => state);
const dispatch = useDispatch();
const [_, setUserStatus] = useStorage('userStatus');
const [role, setRole] = useStorage('userRole', 'admin');
// 使用 Zustand 获取用户信息
const { userInfo, userLoading, clearUserInfo } = useUserStore();
const [, setUserStatus] = useStorage('userStatus');
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() {
setUserStatus('logout');
clearUserInfo(); // 清除 Zustand 中的用户信息
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) {
return (
<div className={styles['fixed-settings']}>
@ -103,14 +113,43 @@ function Navbar({ show }: { show: boolean }) {
<Logo />
<div className={styles['logo-name']}>Codernew React Pro</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>
<ul className={styles.right}>
<li>
<Input.Search
className={styles.round}
placeholder={t['navbar.search.placeholder']}
/>
</li>
<li>
<Select
triggerElement={<IconButton icon={<IconLanguage />} />}
@ -132,11 +171,6 @@ function Navbar({ show }: { show: boolean }) {
}}
/>
</li>
<li>
<MessageBox>
<IconButton icon={<IconNotification />} />
</MessageBox>
</li>
<li>
<Tooltip
content={
@ -152,19 +186,25 @@ function Navbar({ show }: { show: boolean }) {
</Tooltip>
</li>
<Settings />
{userInfo && (
<li>
<Dropdown droplist={droplist} position="br" disabled={userLoading}>
<Avatar size={32} style={{ cursor: 'pointer' }}>
{userLoading ? (
<IconLoading />
) : (
<img alt="avatar" src={userInfo.avatar} />
)}
</Avatar>
</Dropdown>
</li>
)}
<li>
<Dropdown droplist={droplist} position="br" disabled={userLoading}>
<Avatar size={32} style={{ cursor: 'pointer' }}>
{userLoading ? (
<IconLoading />
) : avatar && !avatarError ? (
<img
alt="avatar"
src={avatar}
onError={() => setAvatarError(true)}
/>
) : nickname ? (
nickname.charAt(0)
) : (
<IconUser />
)}
</Avatar>
</Dropdown>
</li>
</ul>
</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