增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-11 21:50:18 +08:00
parent a8a9e2625e
commit edd34ae5db
7 changed files with 562 additions and 94 deletions

View File

@ -0,0 +1,159 @@
import React, { useState } from 'react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import {
Settings,
Bell,
Package,
MoreHorizontal,
Edit,
Trash2
} from 'lucide-react';
interface ActionButtonsCellProps {
environmentId: number;
environmentName?: string;
onConfig: (environmentId: number) => void;
onNotification: (config: any) => void;
onAppManage: (environmentId: number) => void;
onDelete: (config: any) => void;
config: any;
}
export const ActionButtonsCell: React.FC<ActionButtonsCellProps> = ({
environmentId,
environmentName,
onConfig,
onNotification,
onAppManage,
onDelete,
config
}) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const handleConfig = () => {
onConfig(environmentId);
};
const handleNotification = () => {
onNotification(config);
};
const handleAppManage = () => {
onAppManage(environmentId);
};
const handleDelete = () => {
onDelete(config);
};
return (
<div className="flex items-center justify-end gap-1">
{/* 主要操作按钮 */}
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleConfig}
className="h-8 px-2"
>
<Settings className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-gray-900 border-gray-700 text-white">
<div className="text-xs"></div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleNotification}
className="h-8 px-2"
>
<Bell className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-gray-900 border-gray-700 text-white">
<div className="text-xs"></div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={handleAppManage}
className="h-8 px-2"
>
<Package className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent className="bg-gray-900 border-gray-700 text-white">
<div className="text-xs"></div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* 更多操作下拉菜单 */}
<DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 px-2"
>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent className="bg-gray-900 border-gray-700 text-white">
<div className="text-xs"></div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={handleConfig} className="cursor-pointer">
<Edit className="h-4 w-4 mr-2" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleDelete}
className="cursor-pointer text-destructive focus:text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};

View File

@ -0,0 +1,63 @@
import React from 'react';
import { Badge } from '@/components/ui/badge';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import { Package, PackageOpen } from 'lucide-react';
interface ApplicationCountCellProps {
applicationCount?: number;
}
export const ApplicationCountCell: React.FC<ApplicationCountCellProps> = ({
applicationCount = 0
}) => {
const count = applicationCount || 0;
const config = count === 0
? {
icon: <Package className="h-3 w-3" />,
label: '0个应用',
variant: 'secondary' as const,
description: '该环境尚未配置任何应用',
color: 'text-muted-foreground'
}
: count <= 5
? {
icon: <PackageOpen className="h-3 w-3 text-green-500" />,
label: `${count}个应用`,
variant: 'success' as const,
description: `已配置${count}个应用`,
color: 'text-green-600'
}
: {
icon: <PackageOpen className="h-3 w-3 text-blue-500" />,
label: `${count}个应用`,
variant: 'default' as const,
description: `已配置${count}个应用,应用较多`,
color: 'text-blue-600'
};
return (
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5">
{config.icon}
<span className="text-sm font-medium">
{config.label}
</span>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-xs bg-gray-900 border-gray-700 text-white">
<div className="text-white text-xs">
{config.description}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

View File

@ -0,0 +1,101 @@
import React from 'react';
import { Badge } from '@/components/ui/badge';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import { Users, CheckCircle } from 'lucide-react';
interface User {
id: number;
realName?: string;
username?: string;
}
interface ApproversCellProps {
approvalRequired?: boolean;
approverUserIds?: number[];
users?: User[];
}
export const ApproversCell: React.FC<ApproversCellProps> = ({
approvalRequired,
approverUserIds,
users = []
}) => {
// 不需要审批
if (!approvalRequired) {
return (
<div className="flex items-center gap-1.5 text-muted-foreground text-sm">
<CheckCircle className="h-3 w-3 text-green-500" />
<span></span>
</div>
);
}
// 需要审批但没有设置审批人
if (!approverUserIds || approverUserIds.length === 0) {
return (
<div className="flex items-center gap-1.5 text-orange-600 text-sm">
<Users className="h-3 w-3" />
<span></span>
</div>
);
}
// 获取有效的审批人
const validApprovers = approverUserIds
.map(id => users.find(u => u.id === id))
.filter(Boolean) as User[];
if (validApprovers.length === 0) {
return (
<div className="flex items-center gap-1.5 text-orange-600 text-sm">
<Users className="h-3 w-3" />
<span></span>
</div>
);
}
const displayApprovers = validApprovers.slice(0, 2);
const remainingCount = validApprovers.length - 2;
return (
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1 min-w-0">
<div className="flex gap-1 flex-wrap">
{displayApprovers.map((user) => (
<Badge key={user.id} variant="secondary" className="text-xs">
{user.realName || user.username}
</Badge>
))}
{remainingCount > 0 && (
<Badge variant="outline" className="text-xs">
+{remainingCount}
</Badge>
)}
</div>
</div>
</TooltipTrigger>
{remainingCount > 0 && (
<TooltipContent className="max-w-xs bg-gray-900 border-gray-700 text-white">
<div className="space-y-1 text-white">
<div className="text-xs font-medium">:</div>
<div className="flex flex-wrap gap-1">
{validApprovers.map((user) => (
<Badge key={user.id} variant="outline" className="text-xs border-gray-600 text-gray-200">
{user.realName || user.username}
</Badge>
))}
</div>
</div>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
);
};

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Badge } from '@/components/ui/badge';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import { GitBranch, GitPullRequest, ShieldCheck } from 'lucide-react';
interface CodeReviewCellProps {
requireCodeReview?: boolean;
}
export const CodeReviewCell: React.FC<CodeReviewCellProps> = ({
requireCodeReview
}) => {
const config = requireCodeReview
? {
icon: <ShieldCheck className="h-3 w-3 text-blue-500" />,
label: '需要',
variant: 'default' as const,
description: '部署前需要进行代码审查',
color: 'text-blue-600'
}
: {
icon: <GitPullRequest className="h-3 w-3 text-gray-500" />,
label: '不需要',
variant: 'secondary' as const,
description: '部署时不需要代码审查',
color: 'text-gray-500'
};
return (
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5">
<Badge variant={config.variant} className="text-xs px-2 py-0.5">
<div className="flex items-center gap-1">
{config.icon}
<span>{config.label}</span>
</div>
</Badge>
</div>
</TooltipTrigger>
<TooltipContent className="max-w-xs bg-gray-900 border-gray-700 text-white">
<div className="text-white text-xs">
{config.description}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

View File

@ -0,0 +1,151 @@
import React from 'react';
import { Badge } from '@/components/ui/badge';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '@/components/ui/tooltip';
import {
Bell,
MessageSquare,
Mail,
AlertTriangle,
CheckCircle,
XCircle
} from 'lucide-react';
import type { NotificationConfig } from '../types';
interface NotificationChannelCellProps {
notificationConfig?: NotificationConfig | null;
}
// 通知渠道类型映射
const getChannelIcon = (channelType?: string) => {
switch (channelType) {
case 'WEWORK':
return <MessageSquare className="h-3 w-3" />;
case 'EMAIL':
return <Mail className="h-3 w-3" />;
case 'DINGTALK':
return <Bell className="h-3 w-3" />;
default:
return <Bell className="h-3 w-3" />;
}
};
// 通知类型配置
const notificationTypes = [
{
key: 'deployNotificationEnabled',
label: '部署',
variant: 'success' as const,
description: '部署状态变更时发送通知'
},
{
key: 'buildNotificationEnabled',
label: '构建',
variant: 'default' as const,
description: '构建状态变更时发送通知'
},
{
key: 'buildFailureFileEnabled',
label: '失败文件',
variant: 'destructive' as const,
description: '构建失败文件时发送通知'
}
];
export const NotificationChannelCell: React.FC<NotificationChannelCellProps> = ({
notificationConfig
}) => {
// 未配置状态
if (!notificationConfig) {
return (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<Bell className="h-3 w-3 opacity-50" />
<span></span>
</div>
);
}
const {
notificationChannelName,
deployNotificationEnabled,
buildNotificationEnabled,
buildFailureFileEnabled,
updateTime,
updateBy
} = notificationConfig;
// 计算启用的通知类型
const enabledNotifications = notificationTypes.filter(type =>
notificationConfig[type.key as keyof NotificationConfig]
);
// 渠道名称显示
const channelName = notificationChannelName || '未知渠道';
// 状态判断
const hasEnabledNotifications = enabledNotifications.length > 0;
return (
<TooltipProvider>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 min-w-0 max-w-48">
{getChannelIcon(notificationConfig.channelType)}
<span className="text-sm font-medium truncate">
{channelName}
</span>
{hasEnabledNotifications ? (
<div className="flex gap-0.5 ml-auto">
{enabledNotifications.map((notification) => (
<div
key={notification.key}
className={`w-1.5 h-1.5 rounded-full ${
notification.variant === 'success' ? 'bg-green-500' :
notification.variant === 'destructive' ? 'bg-red-500' :
'bg-blue-500'
}`}
title={notification.label}
/>
))}
</div>
) : (
<span className="text-xs text-muted-foreground ml-auto"></span>
)}
</div>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-xs bg-gray-900 border-gray-700 text-white"
>
<div className="text-white">
{/* 启用的通知类型 */}
{hasEnabledNotifications ? (
<div className="space-y-1">
<div className="text-xs font-medium text-gray-300 mb-1">:</div>
<div className="flex flex-wrap gap-1">
{enabledNotifications.map((notification) => (
<Badge
key={notification.key}
variant="outline"
className="text-xs border-gray-600 text-gray-200"
>
{notification.label}
</Badge>
))}
</div>
</div>
) : (
<div className="text-xs text-gray-300"></div>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};

View File

@ -27,6 +27,11 @@ import { getTeamEnvironmentConfigs, deleteTeamEnvironmentConfig } from '../servi
import { TeamEnvironmentConfigDialog } from './TeamEnvironmentConfigDialog'; import { TeamEnvironmentConfigDialog } from './TeamEnvironmentConfigDialog';
import { TeamApplicationManageDialog } from './TeamApplicationManageDialog'; import { TeamApplicationManageDialog } from './TeamApplicationManageDialog';
import { NotificationConfigDialog } from './NotificationConfigDialog'; import { NotificationConfigDialog } from './NotificationConfigDialog';
import { NotificationChannelCell } from './NotificationChannelCell';
import { ApproversCell } from './ApproversCell';
import { CodeReviewCell } from './CodeReviewCell';
import { ApplicationCountCell } from './ApplicationCountCell';
import { ActionButtonsCell } from './ActionButtonsCell';
interface User { interface User {
id: number; id: number;
@ -196,112 +201,42 @@ export const TeamEnvironmentManageDialog: React.FC<
{config.environmentName} {config.environmentName}
</TableCell> </TableCell>
<TableCell> <TableCell>
{config.approvalRequired && config.approverUserIds && config.approverUserIds.length > 0 ? ( <ApproversCell
<div className="flex gap-1 flex-wrap"> approvalRequired={config.approvalRequired}
{config.approverUserIds.map((userId) => { approverUserIds={config.approverUserIds}
const user = users.find(u => u.id === userId); users={users}
return user ? ( />
<Badge key={userId} variant="secondary" className="text-xs">
{user.realName || user.username}
</Badge>
) : null;
})}
</div>
) : (
<span className="text-muted-foreground text-sm">
</span>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
{config.notificationConfig ? ( <NotificationChannelCell
<div className="flex flex-col gap-1"> notificationConfig={config.notificationConfig}
{config.notificationConfig.deployNotificationEnabled && ( />
<Badge variant="outline" className="text-xs">
</Badge>
)}
{config.notificationConfig.buildNotificationEnabled && (
<Badge variant="outline" className="text-xs">
</Badge>
)}
{!config.notificationConfig.deployNotificationEnabled &&
!config.notificationConfig.buildNotificationEnabled && (
<span className="text-muted-foreground text-sm">
</span>
)}
</div>
) : (
<span className="text-muted-foreground text-sm">
</span>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-sm"> <ApplicationCountCell
{config.applicationCount || 0} applicationCount={config.applicationCount}
</span> />
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge <CodeReviewCell
variant={ requireCodeReview={config.requireCodeReview}
config.requireCodeReview />
? 'default'
: 'secondary'
}
>
{config.requireCodeReview ? '需要' : '不需要'}
</Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{config.remark || '-'} {config.remark || '-'}
</span> </span>
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell>
<div className="flex items-center justify-end gap-2"> <ActionButtonsCell
<Button environmentId={config.environmentId}
variant="ghost" environmentName={config.environmentName}
size="sm" onConfig={handleOpenConfigDialog}
onClick={() => onNotification={handleOpenNotificationDialog}
handleOpenConfigDialog( onAppManage={handleOpenAppManageDialog}
config.environmentId onDelete={handleOpenDeleteDialog}
) config={config}
} />
>
<Settings className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenNotificationDialog(config)}
>
<Bell className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() =>
handleOpenAppManageDialog(
config.environmentId
)
}
>
<AppWindow className="h-4 w-4 mr-1" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleOpenDeleteDialog(config)}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@ -55,7 +55,11 @@ export interface NotificationConfig {
notificationChannelId?: number; notificationChannelId?: number;
deployNotificationEnabled?: boolean; deployNotificationEnabled?: boolean;
buildNotificationEnabled?: boolean; buildNotificationEnabled?: boolean;
buildFailureFileEnabled?: boolean;
notificationChannelName?: string; // 关联数据 notificationChannelName?: string; // 关联数据
channelType?: string; // 渠道类型WEWORK, EMAIL, DINGTALK等
updateTime?: string; // 更新时间
updateBy?: string; // 更新人
} }
/** /**