chore(依赖): 添加项目依赖和构建配置
- 添加 Zustand 状态管理库 - 配置开发和生产环境变量 - 更新 Vite 构建配置 - 更新 Git 钩子配置 - 修复 NavBar 组件的 React Hooks 规则问题
This commit is contained in:
parent
c8c20299d9
commit
3ea18a417d
10
.env.development
Normal file
10
.env.development
Normal 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
10
.env.production
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
|
||||||
|
# API 基础地址(生产环境需要修改为实际地址)
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# API 请求超时时间(毫秒)
|
||||||
|
VITE_API_TIMEOUT=30000
|
||||||
|
|
||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=Codernew React Pro
|
||||||
@ -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
12585
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -28,4 +28,13 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user