增加审批组件

This commit is contained in:
dengqichen 2025-10-24 20:30:02 +08:00
parent 6acc7f339f
commit 2aada4632e
3 changed files with 401 additions and 322 deletions

View File

@ -1,25 +1,34 @@
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
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');
@ -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 [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,41 +124,39 @@ 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'] = [
{
key: 'userInfo',
icon: <UserOutlined/>,
icon: <UserOutlined />,
label: (
<div style={{padding: '4px 0'}}>
<div style={{ padding: '4px 0' }}>
<div>{userInfo?.nickname || userInfo?.username}</div>
{userInfo?.email && <div style={{fontSize: '12px', color: '#999'}}>{userInfo.email}</div>}
{userInfo?.email && <div style={{ fontSize: '12px', color: '#999' }}>{userInfo.email}</div>}
</div>
),
disabled: true
disabled: true,
},
{
type: 'divider'
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined/>,
icon: <LogoutOutlined />,
label: '退出登录',
onClick: handleLogout
}
onClick: handleLogout,
},
];
// 处理菜单展开/收起
@ -154,7 +167,7 @@ const BasicLayout: React.FC = () => {
if (loading) {
return (
<div className="flex h-screen w-screen flex-col items-center justify-center bg-slate-50">
<Spin size="large"/>
<Spin size="large" />
<div className="mt-6 text-center text-slate-500">
<p className="text-base"></p>
<p className="text-sm">...</p>
@ -181,20 +194,41 @@ const BasicLayout: React.FC = () => {
</span>
</Tooltip>
</Space>
<Dropdown menu={{items: userMenuItems}} trigger={['hover']}>
<Dropdown menu={{ items: userMenuItems }} trigger={['hover']}>
<Space className="cursor-pointer">
<UserOutlined/>
<UserOutlined />
<span>{userInfo?.nickname || userInfo?.username}</span>
</Space>
</Dropdown>
</Space>
</Header>
<Main className="bg-white">
<Outlet/>
<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;

View File

@ -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 {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 { 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>

View File

@ -1,4 +1,4 @@
import type { BaseResponse } from '@/types/base/response';
import type { BaseResponse } from '@/types/base';
// 租户响应类型
export interface TenantResponse extends BaseResponse {