重构前端逻辑

This commit is contained in:
dengqichen 2025-11-11 10:57:42 +08:00
parent 8712f199a1
commit 8b940229fc
9 changed files with 113 additions and 106 deletions

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Deploy Ease Platform</title> <title>链宇Deploy Ease平台</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -16,7 +16,7 @@ export const isProd = MODE === 'production';
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
// 应用标题 // 应用标题
export const APP_TITLE = import.meta.env.VITE_APP_TITLE || 'Deploy Ease Platform'; export const APP_TITLE = import.meta.env.VITE_APP_TITLE || '链宇Deploy Ease平台';
// 是否启用 Mock // 是否启用 Mock
export const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'; export const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';

View File

@ -35,7 +35,7 @@ export const EnvironmentTabs: React.FC<EnvironmentTabsProps> = React.memo(({
<div className="h-8 w-1 bg-primary rounded-full" /> <div className="h-8 w-1 bg-primary rounded-full" />
<h3 className="text-lg font-semibold"></h3> <h3 className="text-lg font-semibold"></h3>
</div> </div>
<div className="flex items-center gap-2 text-sm text-muted-foreground min-h-[20px]"> <div className="flex items-center gap-2 text-sm text-muted-foreground h-[32px]">
{currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 && ( {currentEnv && currentEnv.requiresApproval && currentEnv.approvers.length > 0 && (
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800"> <div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-800">
<Shield className="h-3.5 w-3.5 text-amber-600" /> <Shield className="h-3.5 w-3.5 text-amber-600" />
@ -90,7 +90,7 @@ export const EnvironmentTabs: React.FC<EnvironmentTabsProps> = React.memo(({
{/* 内容区域 */} {/* 内容区域 */}
{team.environments.map((env) => ( {team.environments.map((env) => (
<TabsContent key={env.environmentId} value={env.environmentId.toString()} className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0"> <TabsContent key={env.environmentId} value={env.environmentId.toString()} className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0">
<div className="p-6"> <div className="p-6 min-h-[400px]">
{env.applications.length === 0 ? ( {env.applications.length === 0 ? (
<Card> <Card>
<CardContent className="flex flex-col items-center justify-center py-16"> <CardContent className="flex flex-col items-center justify-center py-16">

View File

@ -8,6 +8,7 @@ interface TeamSelectorProps {
teams: DeployTeam[]; teams: DeployTeam[];
currentTeamId: number | null; currentTeamId: number | null;
pendingApprovalCount: number; pendingApprovalCount: number;
showApprovalButton: boolean; // 是否显示待审批按钮(基于当前环境)
onTeamChange: (teamId: string) => void; onTeamChange: (teamId: string) => void;
onApprovalClick: () => void; onApprovalClick: () => void;
} }
@ -20,12 +21,14 @@ export const TeamSelector: React.FC<TeamSelectorProps> = React.memo(({
teams, teams,
currentTeamId, currentTeamId,
pendingApprovalCount, pendingApprovalCount,
showApprovalButton,
onTeamChange, onTeamChange,
onApprovalClick onApprovalClick
}) => { }) => {
return ( return (
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{/* 待审批按钮 */} {/* 待审批按钮 - 只在当前环境需要审批且用户有权限时显示 */}
{showApprovalButton && (
<Button <Button
variant="outline" variant="outline"
className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600" className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600"
@ -39,6 +42,7 @@ export const TeamSelector: React.FC<TeamSelectorProps> = React.memo(({
</span> </span>
)} )}
</Button> </Button>
)}
<div className="h-6 w-px bg-border" /> <div className="h-6 w-px bg-border" />

View File

@ -60,6 +60,18 @@ const Dashboard: React.FC = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [deploymentData.teams.length]); }, [deploymentData.teams.length]);
// 计算当前环境是否应该显示待审批按钮
// 逻辑:当前环境需要审批 且 用户是该环境的审批人
const shouldShowApprovalButton = React.useMemo(() => {
if (!deploymentData.currentTeam || !deploymentData.currentEnvId) {
return false;
}
const currentEnv = deploymentData.currentTeam.environments.find(
env => env.environmentId === deploymentData.currentEnvId
);
return currentEnv?.requiresApproval === true && currentEnv?.canApprove === true;
}, [deploymentData.currentTeam, deploymentData.currentEnvId]);
// 处理部署成功 - 使用 useCallback 避免重复创建 // 处理部署成功 - 使用 useCallback 避免重复创建
const handleDeploy = useCallback(async (app: ApplicationConfig, remark: string) => { const handleDeploy = useCallback(async (app: ApplicationConfig, remark: string) => {
await deploymentData.handleDeploySuccess(); await deploymentData.handleDeploySuccess();
@ -101,6 +113,7 @@ const Dashboard: React.FC = () => {
teams={deploymentData.teams} teams={deploymentData.teams}
currentTeamId={deploymentData.currentTeamId} currentTeamId={deploymentData.currentTeamId}
pendingApprovalCount={approvalData.pendingApprovalCount} pendingApprovalCount={approvalData.pendingApprovalCount}
showApprovalButton={shouldShowApprovalButton}
onTeamChange={deploymentData.handleTeamChange} onTeamChange={deploymentData.handleTeamChange}
onApprovalClick={() => approvalData.setApprovalModalOpen(true)} onApprovalClick={() => approvalData.setApprovalModalOpen(true)}
/> />

View File

@ -261,8 +261,10 @@ export const TeamEnvironmentConfigDialog: React.FC<
teamId, teamId,
environmentId: data.environmentId, environmentId: data.environmentId,
approvalRequired: data.approvalRequired, approvalRequired: data.approvalRequired,
approverUserIds: data.approverUserIds, // 如果不需要审批,清空审批人列表
notificationChannelId: data.notificationChannelId, approverUserIds: data.approvalRequired ? data.approverUserIds : [],
// 如果未启用通知,清空通知渠道
notificationChannelId: data.notificationEnabled ? data.notificationChannelId : undefined,
notificationEnabled: data.notificationEnabled, notificationEnabled: data.notificationEnabled,
requireCodeReview: data.requireCodeReview, requireCodeReview: data.requireCodeReview,
remark: data.remark, remark: data.remark,
@ -376,7 +378,13 @@ export const TeamEnvironmentConfigDialog: React.FC<
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={(checked) => {
field.onChange(checked);
// 取消审批时自动清空审批人列表
if (!checked) {
form.setValue('approverUserIds', []);
}
}}
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>
@ -500,7 +508,13 @@ export const TeamEnvironmentConfigDialog: React.FC<
<FormControl> <FormControl>
<Switch <Switch
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={(checked) => {
field.onChange(checked);
// 取消通知时自动清空通知渠道
if (!checked) {
form.setValue('notificationChannelId', undefined);
}
}}
/> />
</FormControl> </FormControl>
</FormItem> </FormItem>

View File

@ -47,7 +47,7 @@ const Login: React.FC = () => {
} = useForm<LoginFormValues>({ } = useForm<LoginFormValues>({
resolver: zodResolver(loginFormSchema), resolver: zodResolver(loginFormSchema),
defaultValues: { defaultValues: {
tenantId: '', tenantId: 'admin', // 默认租户
username: '', username: '',
password: '', password: '',
}, },
@ -69,14 +69,21 @@ const Login: React.FC = () => {
}, []); }, []);
// 加载菜单数据 // 加载菜单数据
const loadMenuData = async () => { const loadMenuData = async (): Promise<boolean> => {
try { try {
const menuData = await getCurrentUserMenus(); const menuData = await getCurrentUserMenus();
if (menuData && menuData.length > 0) { if (menuData && menuData.length > 0) {
dispatch(setMenus(menuData)); dispatch(setMenus(menuData));
return true;
} else {
// 没有任何菜单权限
dispatch(setMenus([]));
return false;
} }
} catch (error) { } catch (error) {
console.error('获取菜单数据失败:', error); console.error('获取菜单数据失败:', error);
dispatch(setMenus([]));
return false;
} }
}; };
@ -97,19 +104,23 @@ const Login: React.FC = () => {
); );
// 2. 获取菜单数据 // 2. 获取菜单数据
await loadMenuData(); const hasMenus = await loadMenuData();
// 3. 显示成功提示 // 3. 显示成功提示
toast({ toast({
title: '登录成功', title: '登录成功',
description: '正在进入系统...', description: hasMenus ? '正在进入系统...' : '正在加载...',
}); });
// 4. 短暂延迟后跳转(让用户看到成功提示) // 4. 短暂延迟后跳转(让用户看到成功提示)
setTimeout(() => { setTimeout(() => {
// ✅ 最稳定方案:刷新页面重新初始化 if (hasMenus) {
// 优点100% 可靠,使用缓存菜单启动快 // 有菜单权限,跳转到首页
window.location.href = '/'; window.location.href = '/';
} else {
// 没有任何菜单权限,跳转到无权限欢迎页
window.location.href = '/no-permission';
}
}, 300); }, 300);
} catch (error) { } catch (error) {
console.error('登录失败:', error); console.error('登录失败:', error);
@ -141,33 +152,12 @@ const Login: React.FC = () => {
<div className={styles.rightSection}> <div className={styles.rightSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>
<div className={styles.logo}> <div className={styles.logo}>
<h1>Deploy Ease Platform</h1> <h1>Deploy Ease平台</h1>
<p className="text-gray-500 mt-2"></p> <p className="text-gray-500 mt-2"></p>
</div> </div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* 租户选择 */} {/* 租户选择 - 隐藏,默认使用 admin */}
<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"> <div className="space-y-2">

View File

@ -12,7 +12,7 @@ import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { Loader2, Tag as TagIcon, Search, CheckCircle2 } from 'lucide-react'; import { Loader2, Tag as TagIcon, Search } from 'lucide-react';
import type { RoleTagResponse } from '../types'; import type { RoleTagResponse } from '../types';
import { getAllTags, assignTags } from '../service'; import { getAllTags, assignTags } from '../service';
@ -106,26 +106,19 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[580px]"> <DialogContent className="sm:max-w-[550px]">
<DialogHeader className="space-y-3"> <DialogHeader>
<DialogTitle className="flex items-center gap-2 text-lg"> <DialogTitle className="flex items-center gap-2">
<div className="flex items-center justify-center w-10 h-10 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500"> <TagIcon className="h-5 w-5" />
<TagIcon className="h-5 w-5 text-white" />
</div>
<div className="flex-1">
<div className="font-semibold"></div>
<div className="text-sm font-normal text-muted-foreground mt-0.5">
</div>
</div>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<DialogBody> <DialogBody>
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" /> <Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="ml-3 text-sm text-muted-foreground">...</span> <span className="text-sm text-muted-foreground">...</span>
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
@ -143,25 +136,20 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
)} )}
{/* 统计信息 */} {/* 统计信息 */}
<div className="flex items-center justify-between px-1 py-2 rounded-lg bg-muted/50"> {allTags.length > 0 && (
<span className="text-sm text-muted-foreground"> <div className="flex items-center justify-between text-sm text-muted-foreground">
{filteredTags.length} <span> {filteredTags.length} </span>
</span> <span> {selectedTagIds.length} </span>
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-green-600" />
<span className="text-sm font-medium">
{selectedTagIds.length}
</span>
</div>
</div> </div>
)}
{/* 标签列表 */} {/* 标签列表 */}
<div className="space-y-2 max-h-[360px] overflow-y-auto pr-2 -mr-2"> <div className="space-y-2 max-h-[320px] overflow-y-auto">
{filteredTags.length > 0 ? ( {filteredTags.length > 0 ? (
filteredTags.map(tag => ( filteredTags.map(tag => (
<div <div
key={tag.id} key={tag.id}
className="flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/50 transition-colors cursor-pointer group" className="flex items-center gap-3 p-2.5 rounded-md hover:bg-muted/50 transition-colors cursor-pointer"
onClick={() => handleToggle(tag.id)} onClick={() => handleToggle(tag.id)}
> >
<Checkbox <Checkbox
@ -170,39 +158,28 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
onCheckedChange={() => handleToggle(tag.id)} onCheckedChange={() => handleToggle(tag.id)}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
/> />
<div className="flex-1 min-w-0"> <div className="flex-1 flex items-center gap-2">
<div className="flex items-center gap-2 mb-1">
<Badge <Badge
variant="secondary" variant="outline"
className="font-medium" style={{ backgroundColor: tag.color, color: '#fff' }}
style={{
backgroundColor: tag.color + '20',
color: tag.color,
borderColor: tag.color + '40'
}}
> >
{tag.name} {tag.name}
</Badge> </Badge>
</div>
{tag.description && ( {tag.description && (
<p className="text-xs text-muted-foreground line-clamp-1"> <span className="text-xs text-muted-foreground">
{tag.description} {tag.description}
</p> </span>
)} )}
</div> </div>
</div> </div>
)) ))
) : searchText ? ( ) : searchText ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
<Search className="w-12 h-12 mb-3 opacity-20" />
<p className="text-sm"></p> <p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground"> <div className="text-center py-8 text-muted-foreground">
<TagIcon className="w-12 h-12 mb-3 opacity-20" />
<p className="text-sm"></p> <p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div> </div>
)} )}
</div> </div>
@ -221,9 +198,7 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
<Button <Button
onClick={handleSubmit} onClick={handleSubmit}
disabled={submitting || loading} disabled={submitting || loading}
className="min-w-[100px]"
> >
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{submitting ? '保存中...' : '确定'} {submitting ? '保存中...' : '确定'}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -12,6 +12,7 @@ import ErrorFallback from '@/components/ErrorFallback';
// 错误页面 // 错误页面
const Forbidden = lazy(() => import('@/pages/Error/403')); const Forbidden = lazy(() => import('@/pages/Error/403'));
const NotFound = lazy(() => import('@/pages/Error/404')); const NotFound = lazy(() => import('@/pages/Error/404'));
const NoPermission = lazy(() => import('@/pages/Error/NoPermission'));
// 表单设计器测试页面(写死的路由) // 表单设计器测试页面(写死的路由)
const FormDesignerTest = lazy(() => import('../pages/FormDesigner')); const FormDesignerTest = lazy(() => import('../pages/FormDesigner'));
@ -67,6 +68,7 @@ const generateRoutes = (menus: MenuResponse[]): RouteObject[] => {
export const createDynamicRouter = () => { export const createDynamicRouter = () => {
const state = store.getState(); const state = store.getState();
const menus = state.user.menus || []; const menus = state.user.menus || [];
const hasMenus = menus.length > 0;
// 动态生成路由 // 动态生成路由
const dynamicRoutes = generateRoutes(menus); const dynamicRoutes = generateRoutes(menus);
@ -83,7 +85,7 @@ export const createDynamicRouter = () => {
children: [ children: [
{ {
path: '', path: '',
element: <Navigate to="/dashboard" replace />, element: hasMenus ? <Navigate to="/dashboard" replace /> : <Navigate to="/no-permission" replace />,
}, },
// 写死的测试路由:表单设计器测试页面 // 写死的测试路由:表单设计器测试页面
{ {
@ -98,6 +100,15 @@ export const createDynamicRouter = () => {
}, },
// 动态生成的路由 // 动态生成的路由
...dynamicRoutes, ...dynamicRoutes,
// 无权限欢迎页(用户没有任何菜单权限时显示)
{
path: 'no-permission',
element: (
<Suspense fallback={<RouteLoading />}>
<NoPermission />
</Suspense>
),
},
// 403 无权限页面 // 403 无权限页面
{ {
path: '403', path: '403',