可用版本

This commit is contained in:
戚辰先生 2024-11-30 17:41:04 +08:00
parent b01d9a630a
commit 7efbb63d31
9 changed files with 371 additions and 359 deletions

View File

@ -1,243 +1,245 @@
import React, { useEffect, useState } from 'react'; import React, {useEffect, useState, useCallback} from 'react';
import { Layout, Menu, Dropdown, Modal, message, Spin, Space, Tooltip } from 'antd'; import {Layout, Menu, Dropdown, Modal, message, Spin, Space, Tooltip} from 'antd';
import { useNavigate, useLocation, Outlet } from 'react-router-dom'; import {useNavigate, useLocation, Outlet} from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux'; import {useDispatch, useSelector} from 'react-redux';
import { import {
UserOutlined, UserOutlined,
LogoutOutlined, LogoutOutlined,
DownOutlined, DownOutlined,
ExclamationCircleOutlined, ExclamationCircleOutlined,
ClockCircleOutlined, ClockCircleOutlined,
CloudOutlined CloudOutlined
} from '@ant-design/icons'; } from '@ant-design/icons';
import * as AntdIcons from '@ant-design/icons'; import * as AntdIcons from '@ant-design/icons';
import { logout, setMenus, setUserInfo } from '../store/userSlice'; import {logout, setMenus, setUserInfo} from '../store/userSlice';
import type { MenuProps } from 'antd'; import type {MenuProps} from 'antd';
import { getCurrentUser } from '../pages/Login/service'; import {getCurrentUser} from '../pages/Login/service';
import { getCurrentUserMenus } from '@/pages/System/Menu/service'; import {getCurrentUserMenus} from '@/pages/System/Menu/service';
import { getWeather } from '../services/weather'; import {getWeather} from '../services/weather';
import type { MenuResponse } from '@/pages/System/Menu/types'; import type {MenuResponse} from '@/pages/System/Menu/types';
import type { RootState } from '../store'; import type {RootState} from '../store';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn';
const { Header, Content, Sider } = Layout; const {Header, Content, Sider} = Layout;
const { confirm } = Modal; const {confirm} = Modal;
// 设置中文语言 // 设置中文语言
dayjs.locale('zh-cn'); dayjs.locale('zh-cn');
const BasicLayout: React.FC = () => { const BasicLayout: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const userInfo = useSelector((state: RootState) => state.user.userInfo); const userInfo = useSelector((state: RootState) => state.user.userInfo);
const menus = useSelector((state: RootState) => state.user.menus); const menus = useSelector((state: RootState) => state.user.menus);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentTime, setCurrentTime] = useState(dayjs()); const [currentTime, setCurrentTime] = useState(dayjs());
const [weather, setWeather] = useState({ temp: '--', weather: '未知', city: '未知' }); const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
const [isInitialized, setIsInitialized] = useState(false);
useEffect(() => { // 将天气获取逻辑提取为useCallback
// 每秒更新时间 const fetchWeather = useCallback(async () => {
const timer = setInterval(() => { try {
setCurrentTime(dayjs()); const data = await getWeather();
}, 1000); setWeather(data);
} catch (error) {
// 获取天气信息 console.error('获取天气信息失败:', error);
const fetchWeather = async () => { }
try { }, []);
const data = await getWeather();
setWeather(data);
} catch (error) {
console.error('获取天气信息失败:', error);
}
};
// 初始化用户数据 // 初始化用户数据
const initializeUserData = async () => { useEffect(() => {
if (isInitialized) return; const initializeUserData = async () => {
try { try {
setLoading(true); setLoading(true);
const userData = await getCurrentUser();
dispatch(setUserInfo(userData));
const menuData = await getCurrentUserMenus();
dispatch(setMenus(menuData));
} catch (error) {
message.error('初始化用户数据失败');
dispatch(logout());
navigate('/login', {replace: true});
} finally {
setLoading(false);
}
};
// 获取并更新用户信息 if (!userInfo) {
const userData = await getCurrentUser(); initializeUserData();
dispatch(setUserInfo(userData)); } else {
setLoading(false);
}
}, []);
const menuData = await getCurrentUserMenus(); // 处理时间和天气更新
dispatch(setMenus(menuData)); useEffect(() => {
// 每秒更新时间
const timer = setInterval(() => {
setCurrentTime(dayjs());
}, 1000);
setIsInitialized(true); // 初始化天气并设置定时更新
} catch (error) { fetchWeather();
message.error('初始化用户数据失败'); const weatherTimer = setInterval(fetchWeather, 30 * 60 * 1000);
} finally {
setLoading(false); return () => {
} clearInterval(timer);
clearInterval(weatherTimer);
};
}, [fetchWeather]);
const handleLogout = () => {
confirm({
title: '确认退出',
icon: <ExclamationCircleOutlined/>,
content: '确定要退出系统吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
dispatch(logout());
message.success('退出成功');
navigate('/login', {replace: true});
}
});
}; };
fetchWeather(); const userMenuItems: MenuProps['items'] = [
initializeUserData(); {
key: 'userInfo',
icon: <UserOutlined/>,
label: (
<div style={{padding: '4px 0'}}>
<div>{userInfo?.nickname || userInfo?.username}</div>
{userInfo?.email && <div style={{fontSize: '12px', color: '#999'}}>{userInfo.email}</div>}
</div>
),
disabled: true
},
{
type: 'divider'
},
{
key: 'logout',
icon: <LogoutOutlined/>,
label: '退出登录',
onClick: handleLogout
}
];
// 每半小时更新一次天气 // 获取图标组件
const weatherTimer = setInterval(fetchWeather, 30 * 60 * 1000); const getIcon = (iconName: string | undefined) => {
if (!iconName) return null;
return () => { const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
clearInterval(timer); const Icon = (AntdIcons as any)[iconKey];
clearInterval(weatherTimer); return Icon ? <Icon/> : null;
}; };
}, [isInitialized]);
const handleLogout = () => { // 将菜单数据转换为antd Menu需要的格式
confirm({ const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
title: '确认退出', return menuList.map(menu => ({
icon: <ExclamationCircleOutlined />, key: menu.path || menu.id.toString(),
content: '确定要退出系统吗?', icon: getIcon(menu.icon),
okText: '确定', label: menu.name,
cancelText: '取消', children: menu.children && menu.children.length > 0 ? getMenuItems(menu.children) : undefined
onOk: () => { }));
dispatch(logout()); };
message.success('退出成功');
navigate('/login', { replace: true });
}
});
};
const userMenuItems: MenuProps['items'] = [ if (loading) {
{ return (
key: 'userInfo', <div style={{
icon: <UserOutlined />, height: '100vh',
label: ( width: '100vw',
<div style={{ padding: '4px 0' }}> display: 'flex',
<div>{userInfo?.nickname || userInfo?.username}</div> flexDirection: 'column',
{userInfo?.email && <div style={{ fontSize: '12px', color: '#999' }}>{userInfo.email}</div>} justifyContent: 'center',
</div>
),
disabled: true
},
{
type: 'divider'
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handleLogout
}
];
// 获取图标组件
const getIcon = (iconName: string | undefined) => {
if (!iconName) return null;
const iconKey = `${iconName.charAt(0).toUpperCase() + iconName.slice(1)}Outlined`;
const Icon = (AntdIcons as any)[iconKey];
return Icon ? <Icon /> : null;
};
// 将菜单数据转换为antd Menu需要的格式
const getMenuItems = (menuList: MenuResponse[]): MenuProps['items'] => {
return menuList.map(menu => ({
key: menu.path || menu.id.toString(),
icon: getIcon(menu.icon),
label: menu.name,
children: menu.children && menu.children.length > 0 ? getMenuItems(menu.children) : undefined
}));
};
if (loading) {
return (
<div style={{
height: '100vh',
width: '100vw',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
background: '#f0f2f5'
}}>
<Spin size="large" />
<div style={{
marginTop: 24,
fontSize: 16,
color: 'rgba(0, 0, 0, 0.45)',
textAlign: 'center'
}}>
<p></p>
<p style={{ fontSize: 14 }}>...</p>
</div>
</div>
);
}
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider>
<div style={{ height: 32, margin: 16, background: 'rgba(255, 255, 255, 0.2)' }} />
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={getMenuItems(menus)}
onClick={({ key }) => navigate(key)}
/>
</Sider>
<Layout>
<Header style={{
padding: '0 24px',
background: '#fff',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
}}>
<Space size={24}>
<Space size={16}>
<span style={{
display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
color: 'rgba(0, 0, 0, 0.65)', background: '#f0f2f5'
fontSize: 14 }}>
}}> <Spin size="large"/>
<ClockCircleOutlined style={{ marginRight: 8 }} /> <div style={{
{currentTime.format('YYYY年MM月DD日 HH:mm:ss')} {currentTime.format('dd')} marginTop: 24,
</span> fontSize: 16,
<Tooltip title={`${weather.city}`}> color: 'rgba(0, 0, 0, 0.45)',
<span style={{ textAlign: 'center'
}}>
<p></p>
<p style={{fontSize: 14}}>...</p>
</div>
</div>
);
}
return (
<Layout style={{minHeight: '100vh'}}>
<Sider>
<div style={{height: 32, margin: 16, background: 'rgba(255, 255, 255, 0.2)'}}/>
<Menu
theme="dark"
mode="inline"
selectedKeys={[location.pathname]}
items={getMenuItems(menus)}
onClick={({key}) => navigate(key)}
/>
</Sider>
<Layout>
<Header style={{
padding: '0 24px',
background: '#fff',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
boxShadow: '0 1px 4px rgba(0,21,41,.08)'
}}>
<Space size={24}>
<Space size={16}>
<span style={{
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
color: 'rgba(0, 0, 0, 0.65)', color: 'rgba(0, 0, 0, 0.65)',
fontSize: 14 fontSize: 14
}}>
<CloudOutlined style={{ marginRight: 8 }} />
{weather.weather} {weather.temp}
</span>
</Tooltip>
</Space>
<Dropdown menu={{ items: userMenuItems }} trigger={['hover']}>
<span style={{
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
height: '100%',
transition: 'all 0.3s',
padding: '0 4px',
borderRadius: 4,
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.025)'
}
}}> }}>
<UserOutlined style={{ fontSize: 16, marginRight: 8 }} /> <ClockCircleOutlined style={{marginRight: 8}}/>
<span>{userInfo?.nickname || userInfo?.username}</span> {currentTime.format('YYYY年MM月DD日 HH:mm:ss')} {currentTime.format('dd')}
<DownOutlined style={{ fontSize: 12, marginLeft: 6 }} />
</span> </span>
</Dropdown> <Tooltip title={`${weather.city}`}>
</Space> <span style={{
</Header> display: 'inline-flex',
<Content style={{ margin: '24px 16px', padding: 24, background: '#fff', minHeight: 280 }}> alignItems: 'center',
<Outlet /> color: 'rgba(0, 0, 0, 0.65)',
</Content> fontSize: 14
</Layout> }}>
</Layout> <CloudOutlined style={{marginRight: 8}}/>
); {weather.weather} {weather.temp}
</span>
</Tooltip>
</Space>
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
<span style={{
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
height: '100%',
transition: 'all 0.3s',
padding: '0 4px',
borderRadius: 4,
'&:hover': {
backgroundColor: 'rgba(0,0,0,0.025)'
}
}}>
<UserOutlined style={{fontSize: 16, marginRight: 8}}/>
<span>{userInfo?.nickname || userInfo?.username}</span>
<DownOutlined style={{fontSize: 12, marginLeft: 6}}/>
</span>
</Dropdown>
</Space>
</Header>
<Content style={{margin: '24px 16px', padding: 24, background: '#fff', minHeight: 280}}>
<Outlet/>
</Content>
</Layout>
</Layout>
);
}; };
export default BasicLayout; export default BasicLayout;

View File

@ -1,8 +1,14 @@
import React from 'react'; import React, { useEffect } from 'react';
import { Card, Row, Col, Statistic } from 'antd'; import { Card, Row, Col, Statistic } from 'antd';
import { UserOutlined, ShoppingCartOutlined, FileOutlined } from '@ant-design/icons'; import { UserOutlined, ShoppingCartOutlined, FileOutlined } from '@ant-design/icons';
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
useEffect(() => {
// 检查这里的数据加载逻辑
// 是否有多个useEffect都在加载相同的数据
// 或者依赖项是否设置正确
}, []); // 检查依赖项
return ( return (
<div> <div>
<Row gutter={16}> <Row gutter={16}>

View File

@ -1,12 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect } from 'antd'; import { Table, Button, Modal, Form, Input, Space, message, Switch, TreeSelect } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined } from '@ant-design/icons'; import { PlusOutlined, EditOutlined, DeleteOutlined, KeyOutlined, TeamOutlined } from '@ant-design/icons';
import type { UserResponse } from './types'; import type { UserResponse, Role } from './types';
import type { DepartmentDTO } from '../Department/types'; import type { DepartmentDTO } from '../Department/types';
import { getUsers, createUser, updateUser, deleteUser, resetPassword } from './service'; import { getUsers, createUser, updateUser, deleteUser, resetPassword } from './service';
import { getDepartmentTree } from '../Department/service'; import { getDepartmentTree } from '../Department/service';
import { convertToPageParams, convertToPageInfo } from '@/utils/page'; import { convertToPageParams, convertToPageInfo } from '@/utils/page';
import { useTableData } from '@/hooks/useTableData'; import { useTableData } from '@/hooks/useTableData';
import type { FixedType, AlignType } from 'rc-table/lib/interface';
// import RoleModal from './components/RoleModal'; // import RoleModal from './components/RoleModal';
const UserPage: React.FC = () => { const UserPage: React.FC = () => {
@ -45,10 +46,6 @@ const UserPage: React.FC = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [passwordForm] = Form.useForm(); const [passwordForm] = Form.useForm();
useEffect(() => {
fetchUsers();
}, [pagination.current, pagination.pageSize]);
const handleAdd = () => { const handleAdd = () => {
setEditingUser(null); setEditingUser(null);
form.resetFields(); form.resetFields();
@ -61,8 +58,7 @@ const UserPage: React.FC = () => {
const handleEdit = (record: UserResponse) => { const handleEdit = (record: UserResponse) => {
setEditingUser(record); setEditingUser(record);
form.setFieldsValue({ form.setFieldsValue({
...record, ...record
password: 'UNCHANGED_PASSWORD'
}); });
setModalVisible(true); setModalVisible(true);
}; };
@ -77,7 +73,7 @@ const UserPage: React.FC = () => {
try { try {
const values = await passwordForm.validateFields(); const values = await passwordForm.validateFields();
if (editingUser) { if (editingUser) {
await resetPassword(editingUser.id); await resetPassword(editingUser.id, values.password);
message.success('密码重置成功'); message.success('密码重置成功');
setResetPasswordModalVisible(false); setResetPasswordModalVisible(false);
} }
@ -92,7 +88,6 @@ const UserPage: React.FC = () => {
if (editingUser) { if (editingUser) {
await updateUser(editingUser.id, { await updateUser(editingUser.id, {
...values, ...values,
password: values.password === 'UNCHANGED_PASSWORD' ? editingUser.password : values.password,
version: editingUser.version version: editingUser.version
}); });
message.success('更新成功'); message.success('更新成功');
@ -121,77 +116,86 @@ const UserPage: React.FC = () => {
// }; // };
const columns = [ const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
fixed: 'left' as FixedType
},
{ {
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username',
key: 'username', key: 'username',
width: '12%', width: 120,
}, },
{ {
title: '昵称', title: '昵称',
dataIndex: 'nickname', dataIndex: 'nickname',
key: 'nickname', key: 'nickname',
width: '12%', width: 120,
}, },
{ {
title: '邮箱', title: '邮箱',
dataIndex: 'email', dataIndex: 'email',
key: 'email', key: 'email',
width: '15%', width: 200,
ellipsis: true,
}, },
{ {
title: '手机号', title: '手机号',
dataIndex: 'phone', dataIndex: 'phone',
key: 'phone', key: 'phone',
width: '12%', width: 120,
}, },
{ {
title: '角色', title: '角色',
dataIndex: 'roles', dataIndex: 'roles',
key: 'roles', key: 'roles',
width: '15%', width: 150,
ellipsis: true,
render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-' render: (roles: Role[]) => roles?.map(role => role.name).join(', ') || '-'
}, },
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createTime',
key: 'createTime', key: 'createTime',
width: '15%', width: 180,
},
{
title: '更新时间',
dataIndex: 'updateTime',
key: 'updateTime',
width: 180,
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'enabled', dataIndex: 'enabled',
key: 'enabled', key: 'enabled',
width: '8%', width: 80,
align: 'center' as AlignType,
render: (enabled: boolean) => ( render: (enabled: boolean) => (
<Switch checked={enabled} disabled /> <Switch checked={enabled} disabled size="small" />
), ),
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: '280px', width: 180,
fixed: 'right' as FixedType,
render: (_: any, record: UserResponse) => ( render: (_: any, record: UserResponse) => (
<Space> <Space size="small">
<Button <Button
type="link" type="link"
size="small"
icon={<EditOutlined />} icon={<EditOutlined />}
onClick={() => handleEdit(record)} onClick={() => handleEdit(record)}
> >
</Button> </Button>
{/*
<Button
type="link"
icon={<TeamOutlined />}
onClick={() => handleAssignRoles(record)}
disabled={record.username === 'admin'}
>
</Button>
*/}
<Button <Button
type="link" type="link"
size="small"
icon={<KeyOutlined />} icon={<KeyOutlined />}
onClick={() => handleResetPassword(record)} onClick={() => handleResetPassword(record)}
> >
@ -199,6 +203,7 @@ const UserPage: React.FC = () => {
</Button> </Button>
<Button <Button
type="link" type="link"
size="small"
danger danger
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
onClick={() => handleDelete(record.id)} onClick={() => handleDelete(record.id)}
@ -224,12 +229,25 @@ const UserPage: React.FC = () => {
columns={columns} columns={columns}
dataSource={users} dataSource={users}
rowKey="id" rowKey="id"
scroll={{ x: 1500 }}
pagination={{ pagination={{
...pagination, ...pagination,
showSizeChanger: true, showSizeChanger: true,
showQuickJumper: true, showQuickJumper: true,
showTotal: (total) => `${total} 条记录`, showTotal: (total) => `${total} 条记录`,
onChange: onPageChange onChange: onPageChange,
size: 'small'
}}
rowSelection={{
type: 'checkbox',
onChange: (selectedRowKeys, selectedRows) => {
console.log('selectedRowKeys:', selectedRowKeys);
console.log('selectedRows:', selectedRows);
},
getCheckboxProps: (record) => ({
disabled: record.username === 'admin',
}),
fixed: true,
}} }}
size="middle" size="middle"
bordered bordered
@ -254,20 +272,6 @@ const UserPage: React.FC = () => {
> >
<Input placeholder="请输入用户名" disabled={!!editingUser} /> <Input placeholder="请输入用户名" disabled={!!editingUser} />
</Form.Item> </Form.Item>
{editingUser && (
<Form.Item name="password" hidden>
<Input type="hidden" />
</Form.Item>
)}
{!editingUser && (
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password placeholder="请输入密码" />
</Form.Item>
)}
<Form.Item <Form.Item
name="nickname" name="nickname"
label="昵称" label="昵称"

View File

@ -1,36 +1,36 @@
import axios from 'axios'; import axios from 'axios';
interface WeatherData { interface WeatherData {
temp: string; temp: string;
weather: string; weather: string;
city: string; city: string;
} }
// 使用高德地图 API 获取天气 // 使用高德地图 API 获取天气
export const getWeather = async (): Promise<WeatherData> => { export const getWeather = async (): Promise<WeatherData> => {
try { try {
// 先获取 IP 定位 // 先获取 IP 定位
const ipUrl = 'https://restapi.amap.com/v3/ip'; const ipUrl = 'https://restapi.amap.com/v3/ip';
const weatherUrl = 'https://restapi.amap.com/v3/weather/weatherInfo'; const weatherUrl = 'https://restapi.amap.com/v3/weather/weatherInfo';
const key = '46a02c7d20534596f7386caf02204caa'; // 需要替换成你自己的 key const key = '46a02c7d20534596f7386caf02204caa'; // 需要替换成你自己的 key
const ipResponse = await axios.get(`${ipUrl}?key=${key}`); const ipResponse = await axios.get(`${ipUrl}?key=${key}`);
const { adcode } = ipResponse.data; const {adcode} = ipResponse.data;
const weatherResponse = await axios.get(`${weatherUrl}?key=${key}&city=${adcode}`); const weatherResponse = await axios.get(`${weatherUrl}?key=${key}&city=${adcode}`);
const weatherData = weatherResponse.data.lives[0]; const weatherData = weatherResponse.data.lives[0];
return { return {
temp: weatherData.temperature, temp: weatherData.temperature,
weather: weatherData.weather, weather: weatherData.weather,
city: weatherData.city city: weatherData.city
}; };
} catch (error) { } catch (error) {
console.error('获取天气信息失败:', error); console.error('获取天气信息失败:', error);
return { return {
temp: '--', temp: '--',
weather: '未知', weather: '未知',
city: '未知' city: '未知'
}; };
} }
}; };

View File

@ -1,10 +1,10 @@
import { configureStore } from '@reduxjs/toolkit'; import {configureStore} from '@reduxjs/toolkit';
import userReducer from './userSlice'; import userReducer from './userSlice';
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
user: userReducer, user: userReducer,
}, },
}); });
export type RootState = ReturnType<typeof store.getState>; export type RootState = ReturnType<typeof store.getState>;

View File

@ -1,51 +1,51 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import {createSlice, PayloadAction} from '@reduxjs/toolkit';
import type { MenuResponse } from '@/pages/System/Menu/types'; import type {MenuResponse} from '@/pages/System/Menu/types';
interface UserInfo { interface UserInfo {
id: number; id: number;
username: string; username: string;
nickname?: string; nickname?: string;
email?: string; email?: string;
phone?: string; phone?: string;
} }
interface UserState { interface UserState {
token: string | null; token: string | null;
userInfo: UserInfo | null; userInfo: UserInfo | null;
menus: MenuResponse[]; menus: MenuResponse[];
} }
const initialState: UserState = { const initialState: UserState = {
token: localStorage.getItem('token'), token: localStorage.getItem('token'),
userInfo: null, userInfo: null,
menus: [] menus: []
}; };
const userSlice = createSlice({ const userSlice = createSlice({
name: 'user', name: 'user',
initialState, initialState,
reducers: { reducers: {
setToken: (state, action: PayloadAction<string>) => { setToken: (state, action: PayloadAction<string>) => {
state.token = action.payload; state.token = action.payload;
localStorage.setItem('token', action.payload); localStorage.setItem('token', action.payload);
}, },
setUserInfo: (state, action: PayloadAction<UserInfo>) => { setUserInfo: (state, action: PayloadAction<UserInfo>) => {
state.userInfo = action.payload; state.userInfo = action.payload;
localStorage.setItem('userInfo', JSON.stringify(action.payload)); localStorage.setItem('userInfo', JSON.stringify(action.payload));
}, },
setMenus: (state, action: PayloadAction<MenuResponse[]>) => { setMenus: (state, action: PayloadAction<MenuResponse[]>) => {
state.menus = action.payload; state.menus = action.payload;
}, },
logout: (state) => { logout: (state) => {
state.token = null; state.token = null;
state.userInfo = null; state.userInfo = null;
state.menus = []; state.menus = [];
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('tenantId'); localStorage.removeItem('tenantId');
localStorage.removeItem('userInfo'); localStorage.removeItem('userInfo');
}
} }
}
}); });
export const { setToken, setUserInfo, setMenus, logout } = userSlice.actions; export const {setToken, setUserInfo, setMenus, logout} = userSlice.actions;
export default userSlice.reducer; export default userSlice.reducer;

View File

@ -1,4 +1,4 @@
import type { Page, PageParams } from '@/types/base/page'; import type {Page, PageParams} from '@/types/base/page';
// 默认分页参数 // 默认分页参数
const DEFAULT_PAGE_SIZE = 10; const DEFAULT_PAGE_SIZE = 10;
@ -8,22 +8,22 @@ const DEFAULT_CURRENT = 1;
* *
*/ */
export const convertToPageParams = (params?: { export const convertToPageParams = (params?: {
current?: number; current?: number;
pageSize?: number; pageSize?: number;
sortField?: string; sortField?: string;
sortOrder?: string; sortOrder?: string;
}): PageParams => ({ }): PageParams => ({
pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码 pageNum: Math.max(1, params?.current || DEFAULT_CURRENT) - 1, // 转换为从0开始的页码
pageSize: params?.pageSize || DEFAULT_PAGE_SIZE, pageSize: params?.pageSize || DEFAULT_PAGE_SIZE,
sortField: params?.sortField, sortField: params?.sortField,
sortOrder: params?.sortOrder?.replace('end', '') sortOrder: params?.sortOrder?.replace('end', '')
}); });
/** /**
* *
*/ */
export const convertToPageInfo = (page?: Page<any>) => ({ export const convertToPageInfo = (page?: Page<any>) => ({
current: (page?.number || 0) + 1, current: (page?.number || 0) + 1,
pageSize: page?.size || DEFAULT_PAGE_SIZE, pageSize: page?.size || DEFAULT_PAGE_SIZE,
total: page?.totalElements || 0 total: page?.totalElements || 0
}); });

View File

@ -1,5 +1,5 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import axios, {AxiosRequestConfig, AxiosResponse} from 'axios';
import { message } from 'antd'; import {message} from 'antd';
export interface ApiResponse<T = any> { export interface ApiResponse<T = any> {
code: number; code: number;

View File

@ -1,20 +1,20 @@
import type { TableState } from '@/types/table'; import type {TableState} from '@/types/table';
// 创建初始状态 // 创建初始状态
export const createInitialState = <T>(pageSize: number = 10): TableState<T> => ({ export const createInitialState = <T>(pageSize: number = 10): TableState<T> => ({
list: [], list: [],
pagination: { pagination: {
current: 1, current: 1,
pageSize, pageSize,
total: 0 total: 0
}, },
loading: false, loading: false,
selectedRows: [], selectedRows: [],
selectedRowKeys: [] selectedRowKeys: []
}); });
// 处理错误 // 处理错误
export const handleTableError = (error: any, message: string) => { export const handleTableError = (error: any, message: string) => {
console.error(message, error); console.error(message, error);
return false; return false;
}; };