重构前端逻辑

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" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Deploy Ease Platform</title>
<title>链宇Deploy Ease平台</title>
</head>
<body>
<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 APP_TITLE = import.meta.env.VITE_APP_TITLE || 'Deploy Ease Platform';
export const APP_TITLE = import.meta.env.VITE_APP_TITLE || '链宇Deploy Ease平台';
// 是否启用 Mock
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" />
<h3 className="text-lg font-semibold"></h3>
</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 && (
<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" />
@ -90,7 +90,7 @@ export const EnvironmentTabs: React.FC<EnvironmentTabsProps> = React.memo(({
{/* 内容区域 */}
{team.environments.map((env) => (
<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 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-16">

View File

@ -8,6 +8,7 @@ interface TeamSelectorProps {
teams: DeployTeam[];
currentTeamId: number | null;
pendingApprovalCount: number;
showApprovalButton: boolean; // 是否显示待审批按钮(基于当前环境)
onTeamChange: (teamId: string) => void;
onApprovalClick: () => void;
}
@ -20,25 +21,28 @@ export const TeamSelector: React.FC<TeamSelectorProps> = React.memo(({
teams,
currentTeamId,
pendingApprovalCount,
showApprovalButton,
onTeamChange,
onApprovalClick
}) => {
return (
<div className="flex items-center gap-4">
{/* 待审批按钮 */}
<Button
variant="outline"
className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600"
onClick={onApprovalClick}
>
<ClipboardCheck className="h-4 w-4 mr-2" />
{pendingApprovalCount > 0 && (
<span className="ml-2 px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-semibold">
{pendingApprovalCount}
</span>
)}
</Button>
{/* 待审批按钮 - 只在当前环境需要审批且用户有权限时显示 */}
{showApprovalButton && (
<Button
variant="outline"
className="border-orange-500 text-orange-600 hover:bg-orange-50 hover:text-orange-700 hover:border-orange-600"
onClick={onApprovalClick}
>
<ClipboardCheck className="h-4 w-4 mr-2" />
{pendingApprovalCount > 0 && (
<span className="ml-2 px-2 py-0.5 rounded-full bg-orange-100 text-orange-700 text-xs font-semibold">
{pendingApprovalCount}
</span>
)}
</Button>
)}
<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
}, [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 避免重复创建
const handleDeploy = useCallback(async (app: ApplicationConfig, remark: string) => {
await deploymentData.handleDeploySuccess();
@ -101,6 +113,7 @@ const Dashboard: React.FC = () => {
teams={deploymentData.teams}
currentTeamId={deploymentData.currentTeamId}
pendingApprovalCount={approvalData.pendingApprovalCount}
showApprovalButton={shouldShowApprovalButton}
onTeamChange={deploymentData.handleTeamChange}
onApprovalClick={() => approvalData.setApprovalModalOpen(true)}
/>

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
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 { getAllTags, assignTags } from '../service';
@ -106,26 +106,19 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[580px]">
<DialogHeader className="space-y-3">
<DialogTitle className="flex items-center gap-2 text-lg">
<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 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>
<DialogContent className="sm:max-w-[550px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<TagIcon className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
<DialogBody>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<span className="ml-3 text-sm text-muted-foreground">...</span>
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin mr-2" />
<span className="text-sm text-muted-foreground">...</span>
</div>
) : (
<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">
<span className="text-sm text-muted-foreground">
{filteredTags.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>
{allTags.length > 0 && (
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span> {filteredTags.length} </span>
<span> {selectedTagIds.length} </span>
</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.map(tag => (
<div
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)}
>
<Checkbox
@ -170,39 +158,28 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
onCheckedChange={() => handleToggle(tag.id)}
onClick={(e) => e.stopPropagation()}
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Badge
variant="secondary"
className="font-medium"
style={{
backgroundColor: tag.color + '20',
color: tag.color,
borderColor: tag.color + '40'
}}
>
{tag.name}
</Badge>
</div>
<div className="flex-1 flex items-center gap-2">
<Badge
variant="outline"
style={{ backgroundColor: tag.color, color: '#fff' }}
>
{tag.name}
</Badge>
{tag.description && (
<p className="text-xs text-muted-foreground line-clamp-1">
<span className="text-xs text-muted-foreground">
{tag.description}
</p>
</span>
)}
</div>
</div>
))
) : searchText ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<Search className="w-12 h-12 mb-3 opacity-20" />
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
<TagIcon className="w-12 h-12 mb-3 opacity-20" />
<div className="text-center py-8 text-muted-foreground">
<p className="text-sm"></p>
<p className="text-xs mt-1"></p>
</div>
)}
</div>
@ -221,9 +198,7 @@ const AssignTagDialog: React.FC<AssignTagDialogProps> = ({
<Button
onClick={handleSubmit}
disabled={submitting || loading}
className="min-w-[100px]"
>
{submitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{submitting ? '保存中...' : '确定'}
</Button>
</DialogFooter>

View File

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