可用版本
This commit is contained in:
parent
b01d9a630a
commit
7efbb63d31
@ -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 userData = await getCurrentUser();
|
const menuData = await getCurrentUserMenus();
|
||||||
dispatch(setUserInfo(userData));
|
dispatch(setMenus(menuData));
|
||||||
|
} catch (error) {
|
||||||
|
message.error('初始化用户数据失败');
|
||||||
|
dispatch(logout());
|
||||||
|
navigate('/login', {replace: true});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const menuData = await getCurrentUserMenus();
|
if (!userInfo) {
|
||||||
dispatch(setMenus(menuData));
|
initializeUserData();
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
setIsInitialized(true);
|
// 处理时间和天气更新
|
||||||
} catch (error) {
|
useEffect(() => {
|
||||||
message.error('初始化用户数据失败');
|
// 每秒更新时间
|
||||||
} finally {
|
const timer = setInterval(() => {
|
||||||
setLoading(false);
|
setCurrentTime(dayjs());
|
||||||
}
|
}, 1000);
|
||||||
|
|
||||||
|
// 初始化天气并设置定时更新
|
||||||
|
fetchWeather();
|
||||||
|
const weatherTimer = setInterval(fetchWeather, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
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;
|
||||||
@ -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}>
|
||||||
|
|||||||
@ -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="昵称"
|
||||||
|
|||||||
@ -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: '未知'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -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>;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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
|
||||||
});
|
});
|
||||||
@ -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;
|
||||||
@ -27,11 +27,11 @@ request.interceptors.request.use(
|
|||||||
(config) => {
|
(config) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
const tenantId = localStorage.getItem('tenantId');
|
const tenantId = localStorage.getItem('tenantId');
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
config.headers['X-Tenant-Id'] = tenantId;
|
config.headers['X-Tenant-Id'] = tenantId;
|
||||||
}
|
}
|
||||||
@ -61,7 +61,7 @@ request.interceptors.response.use(
|
|||||||
(error) => {
|
(error) => {
|
||||||
const config = error.config as RequestOptions;
|
const config = error.config as RequestOptions;
|
||||||
const response = error.response;
|
const response = error.response;
|
||||||
|
|
||||||
if (!config.hideErrorMessage) {
|
if (!config.hideErrorMessage) {
|
||||||
if (response?.data?.message && response?.status !== 500) {
|
if (response?.data?.message && response?.status !== 500) {
|
||||||
message.error(response.data.message);
|
message.error(response.data.message);
|
||||||
@ -75,18 +75,18 @@ request.interceptors.response.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const extendedRequest = {
|
const extendedRequest = {
|
||||||
get: <T = any>(url: string, config?: RequestOptions) =>
|
get: <T = any>(url: string, config?: RequestOptions) =>
|
||||||
request.get<any, T>(url, config),
|
request.get<any, T>(url, config),
|
||||||
|
|
||||||
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
post: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||||
request.post<any, T>(url, data, config),
|
request.post<any, T>(url, data, config),
|
||||||
|
|
||||||
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
put: <T = any>(url: string, data?: any, config?: RequestOptions) =>
|
||||||
request.put<any, T>(url, data, config),
|
request.put<any, T>(url, data, config),
|
||||||
|
|
||||||
delete: <T = any>(url: string, config?: RequestOptions) =>
|
delete: <T = any>(url: string, config?: RequestOptions) =>
|
||||||
request.delete<any, T>(url, config),
|
request.delete<any, T>(url, config),
|
||||||
|
|
||||||
request: request
|
request: request
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user