增加审批组件
This commit is contained in:
parent
6acc7f339f
commit
2aada4632e
@ -1,200 +1,234 @@
|
|||||||
import React, {useEffect, useState, useCallback} from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import {Dropdown, Modal, message, Spin, Space, Tooltip} from 'antd';
|
import { Dropdown, 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,
|
||||||
ExclamationCircleOutlined,
|
ClockCircleOutlined,
|
||||||
ClockCircleOutlined,
|
CloudOutlined,
|
||||||
CloudOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {logout, setMenus} from '../store/userSlice';
|
import { logout, setMenus } from '../store/userSlice';
|
||||||
import type {MenuProps} from 'antd';
|
import type { MenuProps } from 'antd';
|
||||||
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 {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';
|
||||||
import { AppMenu } from './AppMenu';
|
import { AppMenu } from './AppMenu';
|
||||||
import { Layout, LayoutContent, Header, Main } from '@/components/ui/layout';
|
import { Layout, LayoutContent, Header, Main } from '@/components/ui/layout';
|
||||||
|
import {
|
||||||
const {confirm} = Modal;
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from '@/components/ui/alert-dialog';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
// 设置中文语言
|
// 设置中文语言
|
||||||
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 { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(true);
|
const userInfo = useSelector((state: RootState) => state.user.userInfo);
|
||||||
const [currentTime, setCurrentTime] = useState(dayjs());
|
const [loading, setLoading] = useState(true);
|
||||||
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
const [currentTime, setCurrentTime] = useState(dayjs());
|
||||||
|
const [weather, setWeather] = useState({ temp: '--', weather: '未知', city: '未知' });
|
||||||
// 根据当前路径获取需要展开的父级菜单
|
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
||||||
const getDefaultOpenKeys = () => {
|
|
||||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
|
||||||
const openKeys: string[] = [];
|
|
||||||
let currentPath = '';
|
|
||||||
|
|
||||||
pathSegments.forEach(segment => {
|
|
||||||
currentPath += `/${segment}`;
|
|
||||||
openKeys.push(currentPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
return openKeys;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 设置默认展开的菜单
|
// 根据当前路径获取需要展开的父级菜单
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>(getDefaultOpenKeys());
|
const getDefaultOpenKeys = () => {
|
||||||
|
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||||
|
const openKeys: string[] = [];
|
||||||
|
let currentPath = '';
|
||||||
|
|
||||||
// 当路由变化时,自动展开对应的父级菜单
|
pathSegments.forEach((segment) => {
|
||||||
useEffect(() => {
|
currentPath += `/${segment}`;
|
||||||
const newOpenKeys = getDefaultOpenKeys();
|
openKeys.push(currentPath);
|
||||||
setOpenKeys(prevKeys => {
|
});
|
||||||
const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
|
|
||||||
return mergedKeys;
|
|
||||||
});
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
// 将天气获取逻辑提取为useCallback
|
return openKeys;
|
||||||
const fetchWeather = useCallback(async () => {
|
};
|
||||||
try {
|
|
||||||
const data = await getWeather();
|
|
||||||
setWeather(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取天气信息失败:', error);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 初始化用户数据
|
// 设置默认展开的菜单
|
||||||
useEffect(() => {
|
const [openKeys, setOpenKeys] = useState<string[]>(getDefaultOpenKeys());
|
||||||
const initializeUserData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const menuData = await getCurrentUserMenus();
|
|
||||||
dispatch(setMenus(menuData));
|
|
||||||
} catch (error) {
|
|
||||||
message.error('获取菜单数据失败');
|
|
||||||
console.error('获取菜单数据失败:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!userInfo) {
|
// 当路由变化时,自动展开对应的父级菜单
|
||||||
initializeUserData();
|
useEffect(() => {
|
||||||
} else {
|
const newOpenKeys = getDefaultOpenKeys();
|
||||||
setLoading(false);
|
setOpenKeys((prevKeys) => {
|
||||||
}
|
const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
|
||||||
}, [dispatch, userInfo]);
|
return mergedKeys;
|
||||||
|
});
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
// 处理时间和天气更新
|
// 将天气获取逻辑提取为useCallback
|
||||||
useEffect(() => {
|
const fetchWeather = useCallback(async () => {
|
||||||
// 每秒更新时间
|
try {
|
||||||
const timer = setInterval(() => {
|
const data = await getWeather();
|
||||||
setCurrentTime(dayjs());
|
setWeather(data);
|
||||||
}, 1000);
|
} catch (error) {
|
||||||
|
console.error('获取天气信息失败:', error);
|
||||||
// 初始化天气并设置定时更新
|
|
||||||
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});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const userMenuItems: MenuProps['items'] = [
|
|
||||||
{
|
|
||||||
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 handleOpenChange = (keys: string[]) => {
|
|
||||||
setOpenKeys(keys);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-screen flex-col items-center justify-center bg-slate-50">
|
|
||||||
<Spin size="large"/>
|
|
||||||
<div className="mt-6 text-center text-slate-500">
|
|
||||||
<p className="text-base">正在为您准备系统资源</p>
|
|
||||||
<p className="text-sm">请稍候,马上就好...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 初始化用户数据
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeUserData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const menuData = await getCurrentUserMenus();
|
||||||
|
dispatch(setMenus(menuData));
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: '加载失败',
|
||||||
|
description: '获取菜单数据失败',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
console.error('获取菜单数据失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!userInfo) {
|
||||||
|
initializeUserData();
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [dispatch, userInfo, toast]);
|
||||||
|
|
||||||
|
// 处理时间和天气更新
|
||||||
|
useEffect(() => {
|
||||||
|
// 每秒更新时间
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCurrentTime(dayjs());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// 初始化天气并设置定时更新
|
||||||
|
fetchWeather();
|
||||||
|
const weatherTimer = setInterval(fetchWeather, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
clearInterval(weatherTimer);
|
||||||
|
};
|
||||||
|
}, [fetchWeather]);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
setLogoutDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmLogout = () => {
|
||||||
|
dispatch(logout());
|
||||||
|
toast({
|
||||||
|
title: '退出成功',
|
||||||
|
description: '已成功退出系统',
|
||||||
|
});
|
||||||
|
navigate('/login', { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMenuItems: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
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 handleOpenChange = (keys: string[]) => {
|
||||||
|
setOpenKeys(keys);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<div className="flex h-screen w-screen flex-col items-center justify-center bg-slate-50">
|
||||||
<AppMenu openKeys={openKeys} onOpenChange={handleOpenChange} />
|
<Spin size="large" />
|
||||||
<LayoutContent>
|
<div className="mt-6 text-center text-slate-500">
|
||||||
<Header className="justify-end">
|
<p className="text-base">正在为您准备系统资源</p>
|
||||||
<Space size={24}>
|
<p className="text-sm">请稍候,马上就好...</p>
|
||||||
<Space size={16}>
|
</div>
|
||||||
<span className="inline-flex items-center text-sm text-slate-600">
|
</div>
|
||||||
<ClockCircleOutlined className="mr-2" />
|
|
||||||
{currentTime.format('YYYY年MM月DD日 HH:mm:ss')} 星期{currentTime.format('dd')}
|
|
||||||
</span>
|
|
||||||
<Tooltip title={`${weather.city}`}>
|
|
||||||
<span className="inline-flex items-center text-sm text-slate-600">
|
|
||||||
<CloudOutlined className="mr-2" />
|
|
||||||
{weather.weather} {weather.temp}℃
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
|
|
||||||
<Space className="cursor-pointer">
|
|
||||||
<UserOutlined/>
|
|
||||||
<span>{userInfo?.nickname || userInfo?.username}</span>
|
|
||||||
</Space>
|
|
||||||
</Dropdown>
|
|
||||||
</Space>
|
|
||||||
</Header>
|
|
||||||
<Main className="bg-white">
|
|
||||||
<Outlet/>
|
|
||||||
</Main>
|
|
||||||
</LayoutContent>
|
|
||||||
</Layout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BasicLayout;
|
return (
|
||||||
|
<Layout>
|
||||||
|
<AppMenu openKeys={openKeys} onOpenChange={handleOpenChange} />
|
||||||
|
<LayoutContent>
|
||||||
|
<Header className="justify-end">
|
||||||
|
<Space size={24}>
|
||||||
|
<Space size={16}>
|
||||||
|
<span className="inline-flex items-center text-sm text-slate-600">
|
||||||
|
<ClockCircleOutlined className="mr-2" />
|
||||||
|
{currentTime.format('YYYY年MM月DD日 HH:mm:ss')} 星期{currentTime.format('dd')}
|
||||||
|
</span>
|
||||||
|
<Tooltip title={`${weather.city}`}>
|
||||||
|
<span className="inline-flex items-center text-sm text-slate-600">
|
||||||
|
<CloudOutlined className="mr-2" />
|
||||||
|
{weather.weather} {weather.temp}℃
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
<Dropdown menu={{ items: userMenuItems }} trigger={['hover']}>
|
||||||
|
<Space className="cursor-pointer">
|
||||||
|
<UserOutlined />
|
||||||
|
<span>{userInfo?.nickname || userInfo?.username}</span>
|
||||||
|
</Space>
|
||||||
|
</Dropdown>
|
||||||
|
</Space>
|
||||||
|
</Header>
|
||||||
|
<Main className="bg-white">
|
||||||
|
<Outlet />
|
||||||
|
</Main>
|
||||||
|
</LayoutContent>
|
||||||
|
|
||||||
|
{/* 退出登录确认对话框 */}
|
||||||
|
<AlertDialog open={logoutDialogOpen} onOpenChange={setLogoutDialogOpen}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle className="flex items-center gap-2">
|
||||||
|
<AlertCircle className="h-5 w-5 text-amber-500" />
|
||||||
|
确认退出
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
确定要退出系统吗?
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={() => setLogoutDialogOpen(false)}>
|
||||||
|
取消
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={confirmLogout}>确定</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BasicLayout;
|
||||||
|
|||||||
@ -1,161 +1,206 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {Form, Input, Button, message, Select} from 'antd';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {useNavigate} from 'react-router-dom';
|
import { useDispatch } from 'react-redux';
|
||||||
import {useDispatch} from 'react-redux';
|
import { useForm } from 'react-hook-form';
|
||||||
import {login} from './service';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import {getEnabledTenants} from '@/pages/System/Tenant/service';
|
import * as z from 'zod';
|
||||||
import {setToken, setUserInfo, setMenus} from '../../store/userSlice';
|
import { login } from './service';
|
||||||
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
import { getEnabledTenants } from '@/pages/System/Tenant/service';
|
||||||
|
import { setToken, setUserInfo, setMenus } from '../../store/userSlice';
|
||||||
|
import { getCurrentUserMenus } from '@/pages/System/Menu/service';
|
||||||
import type { TenantResponse } from '@/pages/System/Tenant/types';
|
import type { TenantResponse } from '@/pages/System/Tenant/types';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { useToast } from '@/components/ui/use-toast';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
|
||||||
interface LoginForm {
|
const loginFormSchema = z.object({
|
||||||
tenantId: string;
|
tenantId: z.string().min(1, '请选择租户'),
|
||||||
username: string;
|
username: z.string().min(1, '请输入用户名'),
|
||||||
password: string;
|
password: z.string().min(1, '请输入密码'),
|
||||||
}
|
});
|
||||||
|
|
||||||
|
type LoginFormValues = z.infer<typeof loginFormSchema>;
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [form] = Form.useForm<LoginForm>();
|
const { toast } = useToast();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [tenants, setTenants] = useState<TenantResponse[]>([]);
|
const [tenants, setTenants] = useState<TenantResponse[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
const {
|
||||||
const fetchTenants = async () => {
|
register,
|
||||||
try {
|
handleSubmit,
|
||||||
const data = await getEnabledTenants();
|
setValue,
|
||||||
setTenants(data);
|
watch,
|
||||||
} catch (error) {
|
formState: { errors },
|
||||||
console.error('获取租户列表失败:', error);
|
} = useForm<LoginFormValues>({
|
||||||
}
|
resolver: zodResolver(loginFormSchema),
|
||||||
};
|
defaultValues: {
|
||||||
|
tenantId: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
fetchTenants();
|
const selectedTenantId = watch('tenantId');
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 加载菜单数据
|
useEffect(() => {
|
||||||
const loadMenuData = async () => {
|
const fetchTenants = async () => {
|
||||||
try {
|
try {
|
||||||
const menuData = await getCurrentUserMenus();
|
const data = await getEnabledTenants();
|
||||||
if (menuData && menuData.length > 0) {
|
setTenants(data);
|
||||||
dispatch(setMenus(menuData));
|
} catch (error) {
|
||||||
}
|
console.error('获取租户列表失败:', error);
|
||||||
} catch (error) {
|
}
|
||||||
console.error('获取菜单数据失败:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = async (values: LoginForm) => {
|
fetchTenants();
|
||||||
setLoading(true);
|
}, []);
|
||||||
try {
|
|
||||||
// 1. 登录获取 token 和用户信息
|
|
||||||
const loginData = await login(values);
|
|
||||||
dispatch(setToken(loginData.token));
|
|
||||||
dispatch(setUserInfo({
|
|
||||||
id: loginData.id,
|
|
||||||
username: loginData.username,
|
|
||||||
nickname: loginData.nickname,
|
|
||||||
email: loginData.email,
|
|
||||||
phone: loginData.phone
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 2. 获取菜单数据
|
// 加载菜单数据
|
||||||
await loadMenuData();
|
const loadMenuData = async () => {
|
||||||
|
try {
|
||||||
|
const menuData = await getCurrentUserMenus();
|
||||||
|
if (menuData && menuData.length > 0) {
|
||||||
|
dispatch(setMenus(menuData));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取菜单数据失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
message.success('登录成功');
|
const onSubmit = async (values: LoginFormValues) => {
|
||||||
navigate('/');
|
setLoading(true);
|
||||||
} catch (error) {
|
try {
|
||||||
console.error('登录失败:', error);
|
// 1. 登录获取 token 和用户信息
|
||||||
} finally {
|
const loginData = await login(values);
|
||||||
setLoading(false);
|
dispatch(setToken(loginData.token));
|
||||||
}
|
dispatch(
|
||||||
};
|
setUserInfo({
|
||||||
|
id: loginData.id,
|
||||||
|
username: loginData.username,
|
||||||
|
nickname: loginData.nickname,
|
||||||
|
email: loginData.email,
|
||||||
|
phone: loginData.phone,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
// 2. 获取菜单数据
|
||||||
<div className={styles.loginContainer}>
|
await loadMenuData();
|
||||||
{/* 左侧区域 */}
|
|
||||||
<div className={styles.leftSection}>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-bold">链宇DevOps门户管理系统</h1>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<blockquote className="text-2xl font-medium">
|
|
||||||
"平台帮助我们显著提升了部署效率,让团队可以更专注于业务开发。"
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 右侧登录区域 */}
|
toast({
|
||||||
<div className={styles.rightSection}>
|
title: '登录成功',
|
||||||
<div className={styles.loginBox}>
|
description: '欢迎回来!',
|
||||||
<div className={styles.logo}>
|
});
|
||||||
<h1>Deploy Ease Platform</h1>
|
navigate('/');
|
||||||
<p className="text-gray-500 mt-2">输入您的账号密码登录系统</p>
|
} catch (error) {
|
||||||
</div>
|
console.error('登录失败:', error);
|
||||||
<Form<LoginForm>
|
toast({
|
||||||
form={form}
|
title: '登录失败',
|
||||||
name="login"
|
description: error instanceof Error ? error.message : '用户名或密码错误',
|
||||||
onFinish={handleFinish}
|
variant: 'destructive',
|
||||||
autoComplete="off"
|
});
|
||||||
size="large"
|
} finally {
|
||||||
layout="vertical"
|
setLoading(false);
|
||||||
>
|
}
|
||||||
<Form.Item
|
};
|
||||||
name="tenantId"
|
|
||||||
rules={[{required: true, message: '请选择租户!'}]}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
placeholder="系统管理租户"
|
|
||||||
options={tenants.map(tenant => ({
|
|
||||||
label: tenant.name,
|
|
||||||
value: tenant.code
|
|
||||||
}))}
|
|
||||||
className={styles.input}
|
|
||||||
dropdownStyle={{
|
|
||||||
padding: '8px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
return (
|
||||||
name="username"
|
<div className={styles.loginContainer}>
|
||||||
rules={[{required: true, message: '请输入用户名!'}]}
|
{/* 左侧区域 */}
|
||||||
>
|
<div className={styles.leftSection}>
|
||||||
<Input
|
<div>
|
||||||
placeholder="admin"
|
<h1 className="text-xl font-bold">链宇DevOps门户管理系统</h1>
|
||||||
className={styles.input}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item
|
|
||||||
name="password"
|
|
||||||
rules={[{required: true, message: '请输入密码!'}]}
|
|
||||||
>
|
|
||||||
<Input.Password
|
|
||||||
placeholder="········"
|
|
||||||
className={styles.input}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
block
|
|
||||||
loading={loading}
|
|
||||||
className={styles.loginButton}
|
|
||||||
>
|
|
||||||
登录
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="space-y-4">
|
||||||
|
<blockquote className="text-2xl font-medium">
|
||||||
|
"平台帮助我们显著提升了部署效率,让团队可以更专注于业务开发。"
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧登录区域 */}
|
||||||
|
<div className={styles.rightSection}>
|
||||||
|
<div className={styles.loginBox}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<h1>Deploy Ease Platform</h1>
|
||||||
|
<p className="text-gray-500 mt-2">输入您的账号密码登录系统</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
{/* 租户选择 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="tenantId">租户</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedTenantId}
|
||||||
|
onValueChange={(value) => setValue('tenantId', value, { shouldValidate: true })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="tenantId" className="h-10">
|
||||||
|
<SelectValue placeholder="系统管理租户" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tenants.map((tenant) => (
|
||||||
|
<SelectItem key={tenant.code} value={tenant.code}>
|
||||||
|
{tenant.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{errors.tenantId && (
|
||||||
|
<p className="text-sm text-destructive">{errors.tenantId.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 用户名 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="username">用户名</Label>
|
||||||
|
<Input
|
||||||
|
id="username"
|
||||||
|
placeholder="admin"
|
||||||
|
className="h-10"
|
||||||
|
{...register('username')}
|
||||||
|
/>
|
||||||
|
{errors.username && (
|
||||||
|
<p className="text-sm text-destructive">{errors.username.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 密码 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="password">密码</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="········"
|
||||||
|
className="h-10"
|
||||||
|
{...register('password')}
|
||||||
|
/>
|
||||||
|
{errors.password && (
|
||||||
|
<p className="text-sm text-destructive">{errors.password.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登录按钮 */}
|
||||||
|
<Button type="submit" className="w-full h-10" disabled={loading}>
|
||||||
|
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { BaseResponse } from '@/types/base/response';
|
import type { BaseResponse } from '@/types/base';
|
||||||
|
|
||||||
// 租户响应类型
|
// 租户响应类型
|
||||||
export interface TenantResponse extends BaseResponse {
|
export interface TenantResponse extends BaseResponse {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user