重构前端逻辑
This commit is contained in:
parent
8712f199a1
commit
8b940229fc
@ -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>
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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" />
|
||||
|
||||
|
||||
@ -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)}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user