增加审批组件
This commit is contained in:
parent
6acc7f339f
commit
2aada4632e
@ -1,200 +1,234 @@
|
||||
import React, {useEffect, useState, useCallback} from 'react';
|
||||
import {Dropdown, Modal, message, Spin, Space, Tooltip} from 'antd';
|
||||
import {useNavigate, useLocation, Outlet} from 'react-router-dom';
|
||||
import {useDispatch, useSelector} from 'react-redux';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Dropdown, Spin, Space, Tooltip } from 'antd';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudOutlined
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {logout, setMenus} from '../store/userSlice';
|
||||
import type {MenuProps} from 'antd';
|
||||
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
||||
import {getWeather} from '../services/weather';
|
||||
import type {RootState} from '../store';
|
||||
import { logout, setMenus } from '../store/userSlice';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { getCurrentUserMenus } from '@/pages/System/Menu/service';
|
||||
import { getWeather } from '../services/weather';
|
||||
import type { RootState } from '../store';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { AppMenu } from './AppMenu';
|
||||
import { Layout, LayoutContent, Header, Main } from '@/components/ui/layout';
|
||||
|
||||
const {confirm} = Modal;
|
||||
import {
|
||||
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');
|
||||
|
||||
const BasicLayout: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const userInfo = useSelector((state: RootState) => state.user.userInfo);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentTime, setCurrentTime] = useState(dayjs());
|
||||
const [weather, setWeather] = useState({temp: '--', weather: '未知', city: '未知'});
|
||||
|
||||
// 根据当前路径获取需要展开的父级菜单
|
||||
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 navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const { toast } = useToast();
|
||||
const userInfo = useSelector((state: RootState) => state.user.userInfo);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentTime, setCurrentTime] = useState(dayjs());
|
||||
const [weather, setWeather] = useState({ temp: '--', weather: '未知', city: '未知' });
|
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false);
|
||||
|
||||
// 设置默认展开的菜单
|
||||
const [openKeys, setOpenKeys] = useState<string[]>(getDefaultOpenKeys());
|
||||
// 根据当前路径获取需要展开的父级菜单
|
||||
const getDefaultOpenKeys = () => {
|
||||
const pathSegments = location.pathname.split('/').filter(Boolean);
|
||||
const openKeys: string[] = [];
|
||||
let currentPath = '';
|
||||
|
||||
// 当路由变化时,自动展开对应的父级菜单
|
||||
useEffect(() => {
|
||||
const newOpenKeys = getDefaultOpenKeys();
|
||||
setOpenKeys(prevKeys => {
|
||||
const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
|
||||
return mergedKeys;
|
||||
});
|
||||
}, [location.pathname]);
|
||||
pathSegments.forEach((segment) => {
|
||||
currentPath += `/${segment}`;
|
||||
openKeys.push(currentPath);
|
||||
});
|
||||
|
||||
// 将天气获取逻辑提取为useCallback
|
||||
const fetchWeather = useCallback(async () => {
|
||||
try {
|
||||
const data = await getWeather();
|
||||
setWeather(data);
|
||||
} catch (error) {
|
||||
console.error('获取天气信息失败:', error);
|
||||
}
|
||||
}, []);
|
||||
return openKeys;
|
||||
};
|
||||
|
||||
// 初始化用户数据
|
||||
useEffect(() => {
|
||||
const initializeUserData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const menuData = await getCurrentUserMenus();
|
||||
dispatch(setMenus(menuData));
|
||||
} catch (error) {
|
||||
message.error('获取菜单数据失败');
|
||||
console.error('获取菜单数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
// 设置默认展开的菜单
|
||||
const [openKeys, setOpenKeys] = useState<string[]>(getDefaultOpenKeys());
|
||||
|
||||
if (!userInfo) {
|
||||
initializeUserData();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dispatch, userInfo]);
|
||||
// 当路由变化时,自动展开对应的父级菜单
|
||||
useEffect(() => {
|
||||
const newOpenKeys = getDefaultOpenKeys();
|
||||
setOpenKeys((prevKeys) => {
|
||||
const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
|
||||
return mergedKeys;
|
||||
});
|
||||
}, [location.pathname]);
|
||||
|
||||
// 处理时间和天气更新
|
||||
useEffect(() => {
|
||||
// 每秒更新时间
|
||||
const timer = setInterval(() => {
|
||||
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});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
// 将天气获取逻辑提取为useCallback
|
||||
const fetchWeather = useCallback(async () => {
|
||||
try {
|
||||
const data = await getWeather();
|
||||
setWeather(data);
|
||||
} catch (error) {
|
||||
console.error('获取天气信息失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化用户数据
|
||||
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 (
|
||||
<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>
|
||||
</Layout>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 {Form, Input, Button, message, Select} from 'antd';
|
||||
import {useNavigate} from 'react-router-dom';
|
||||
import {useDispatch} from 'react-redux';
|
||||
import {login} from './service';
|
||||
import {getEnabledTenants} from '@/pages/System/Tenant/service';
|
||||
import {setToken, setUserInfo, setMenus} from '../../store/userSlice';
|
||||
import {getCurrentUserMenus} from '@/pages/System/Menu/service';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { login } from './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 { 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';
|
||||
|
||||
interface LoginForm {
|
||||
tenantId: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
const loginFormSchema = z.object({
|
||||
tenantId: z.string().min(1, '请选择租户'),
|
||||
username: z.string().min(1, '请输入用户名'),
|
||||
password: z.string().min(1, '请输入密码'),
|
||||
});
|
||||
|
||||
type LoginFormValues = z.infer<typeof loginFormSchema>;
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const [form] = Form.useForm<LoginForm>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tenants, setTenants] = useState<TenantResponse[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tenants, setTenants] = useState<TenantResponse[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTenants = async () => {
|
||||
try {
|
||||
const data = await getEnabledTenants();
|
||||
setTenants(data);
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
};
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginFormSchema),
|
||||
defaultValues: {
|
||||
tenantId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
fetchTenants();
|
||||
}, []);
|
||||
const selectedTenantId = watch('tenantId');
|
||||
|
||||
// 加载菜单数据
|
||||
const loadMenuData = async () => {
|
||||
try {
|
||||
const menuData = await getCurrentUserMenus();
|
||||
if (menuData && menuData.length > 0) {
|
||||
dispatch(setMenus(menuData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单数据失败:', error);
|
||||
}
|
||||
useEffect(() => {
|
||||
const fetchTenants = async () => {
|
||||
try {
|
||||
const data = await getEnabledTenants();
|
||||
setTenants(data);
|
||||
} catch (error) {
|
||||
console.error('获取租户列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values: LoginForm) => {
|
||||
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
|
||||
}));
|
||||
fetchTenants();
|
||||
}, []);
|
||||
|
||||
// 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('登录成功');
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
const onSubmit = async (values: LoginFormValues) => {
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.loginContainer}>
|
||||
{/* 左侧区域 */}
|
||||
<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>
|
||||
// 2. 获取菜单数据
|
||||
await loadMenuData();
|
||||
|
||||
{/* 右侧登录区域 */}
|
||||
<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<LoginForm>
|
||||
form={form}
|
||||
name="login"
|
||||
onFinish={handleFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
layout="vertical"
|
||||
>
|
||||
<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>
|
||||
toast({
|
||||
title: '登录成功',
|
||||
description: '欢迎回来!',
|
||||
});
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
toast({
|
||||
title: '登录失败',
|
||||
description: error instanceof Error ? error.message : '用户名或密码错误',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{required: true, message: '请输入用户名!'}]}
|
||||
>
|
||||
<Input
|
||||
placeholder="admin"
|
||||
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>
|
||||
return (
|
||||
<div className={styles.loginContainer}>
|
||||
{/* 左侧区域 */}
|
||||
<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>
|
||||
|
||||
{/* 右侧登录区域 */}
|
||||
<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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user