增加审批组件
This commit is contained in:
parent
6acc7f339f
commit
2aada4632e
@ -1,13 +1,12 @@
|
||||
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 { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudOutlined
|
||||
CloudOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { logout, setMenus } from '../store/userSlice';
|
||||
import type { MenuProps } from 'antd';
|
||||
@ -18,8 +17,18 @@ 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');
|
||||
@ -28,10 +37,12 @@ const BasicLayout: React.FC = () => {
|
||||
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 getDefaultOpenKeys = () => {
|
||||
@ -39,7 +50,7 @@ const BasicLayout: React.FC = () => {
|
||||
const openKeys: string[] = [];
|
||||
let currentPath = '';
|
||||
|
||||
pathSegments.forEach(segment => {
|
||||
pathSegments.forEach((segment) => {
|
||||
currentPath += `/${segment}`;
|
||||
openKeys.push(currentPath);
|
||||
});
|
||||
@ -53,7 +64,7 @@ const BasicLayout: React.FC = () => {
|
||||
// 当路由变化时,自动展开对应的父级菜单
|
||||
useEffect(() => {
|
||||
const newOpenKeys = getDefaultOpenKeys();
|
||||
setOpenKeys(prevKeys => {
|
||||
setOpenKeys((prevKeys) => {
|
||||
const mergedKeys = [...new Set([...prevKeys, ...newOpenKeys])];
|
||||
return mergedKeys;
|
||||
});
|
||||
@ -77,7 +88,11 @@ const BasicLayout: React.FC = () => {
|
||||
const menuData = await getCurrentUserMenus();
|
||||
dispatch(setMenus(menuData));
|
||||
} catch (error) {
|
||||
message.error('获取菜单数据失败');
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '获取菜单数据失败',
|
||||
variant: 'destructive',
|
||||
});
|
||||
console.error('获取菜单数据失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -89,7 +104,7 @@ const BasicLayout: React.FC = () => {
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [dispatch, userInfo]);
|
||||
}, [dispatch, userInfo, toast]);
|
||||
|
||||
// 处理时间和天气更新
|
||||
useEffect(() => {
|
||||
@ -109,18 +124,16 @@ const BasicLayout: React.FC = () => {
|
||||
}, [fetchWeather]);
|
||||
|
||||
const handleLogout = () => {
|
||||
confirm({
|
||||
title: '确认退出',
|
||||
icon: <ExclamationCircleOutlined/>,
|
||||
content: '确定要退出系统吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
setLogoutDialogOpen(true);
|
||||
};
|
||||
|
||||
const confirmLogout = () => {
|
||||
dispatch(logout());
|
||||
message.success('退出成功');
|
||||
navigate('/login', {replace: true});
|
||||
}
|
||||
toast({
|
||||
title: '退出成功',
|
||||
description: '已成功退出系统',
|
||||
});
|
||||
navigate('/login', { replace: true });
|
||||
};
|
||||
|
||||
const userMenuItems: MenuProps['items'] = [
|
||||
@ -133,17 +146,17 @@ const BasicLayout: React.FC = () => {
|
||||
{userInfo?.email && <div style={{ fontSize: '12px', color: '#999' }}>{userInfo.email}</div>}
|
||||
</div>
|
||||
),
|
||||
disabled: true
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: '退出登录',
|
||||
onClick: handleLogout
|
||||
}
|
||||
onClick: handleLogout,
|
||||
},
|
||||
];
|
||||
|
||||
// 处理菜单展开/收起
|
||||
@ -193,8 +206,29 @@ const BasicLayout: React.FC = () => {
|
||||
<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,27 +1,60 @@
|
||||
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 { 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 { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tenants, setTenants] = useState<TenantResponse[]>([]);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<LoginFormValues>({
|
||||
resolver: zodResolver(loginFormSchema),
|
||||
defaultValues: {
|
||||
tenantId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
const selectedTenantId = watch('tenantId');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTenants = async () => {
|
||||
try {
|
||||
@ -47,27 +80,37 @@ const Login: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleFinish = async (values: LoginForm) => {
|
||||
const onSubmit = async (values: LoginFormValues) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. 登录获取 token 和用户信息
|
||||
const loginData = await login(values);
|
||||
dispatch(setToken(loginData.token));
|
||||
dispatch(setUserInfo({
|
||||
dispatch(
|
||||
setUserInfo({
|
||||
id: loginData.id,
|
||||
username: loginData.username,
|
||||
nickname: loginData.nickname,
|
||||
email: loginData.email,
|
||||
phone: loginData.phone
|
||||
}));
|
||||
phone: loginData.phone,
|
||||
})
|
||||
);
|
||||
|
||||
// 2. 获取菜单数据
|
||||
await loadMenuData();
|
||||
|
||||
message.success('登录成功');
|
||||
toast({
|
||||
title: '登录成功',
|
||||
description: '欢迎回来!',
|
||||
});
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
toast({
|
||||
title: '登录失败',
|
||||
description: error instanceof Error ? error.message : '用户名或密码错误',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@ -94,64 +137,66 @@ const Login: React.FC = () => {
|
||||
<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: '请选择租户!'}]}
|
||||
>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
{/* 租户选择 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tenantId">租户</Label>
|
||||
<Select
|
||||
placeholder="系统管理租户"
|
||||
options={tenants.map(tenant => ({
|
||||
label: tenant.name,
|
||||
value: tenant.code
|
||||
}))}
|
||||
className={styles.input}
|
||||
dropdownStyle={{
|
||||
padding: '8px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{required: true, message: '请输入用户名!'}]}
|
||||
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={styles.input}
|
||||
className="h-10"
|
||||
{...register('username')}
|
||||
/>
|
||||
</Form.Item>
|
||||
{errors.username && (
|
||||
<p className="text-sm text-destructive">{errors.username.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{required: true, message: '请输入密码!'}]}
|
||||
>
|
||||
<Input.Password
|
||||
{/* 密码 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">密码</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="········"
|
||||
className={styles.input}
|
||||
className="h-10"
|
||||
{...register('password')}
|
||||
/>
|
||||
</Form.Item>
|
||||
{errors.password && (
|
||||
<p className="text-sm text-destructive">{errors.password.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
loading={loading}
|
||||
className={styles.loginButton}
|
||||
>
|
||||
{/* 登录按钮 */}
|
||||
<Button type="submit" className="w-full h-10" disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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