增加代码编辑器表单组件

This commit is contained in:
dengqichen 2025-11-02 22:59:19 +08:00
parent c23c93b753
commit efc92ed8e2
4 changed files with 285 additions and 32 deletions

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { import {
Package, Package,
@ -11,7 +12,11 @@ import {
GitBranch, GitBranch,
Users, Users,
Server, Server,
CheckCircle2 CheckCircle2,
XCircle,
Clock,
TrendingUp,
History
} from "lucide-react"; } from "lucide-react";
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { getDeployEnvironments, startDeployment } from './service'; import { getDeployEnvironments, startDeployment } from './service';
@ -26,6 +31,63 @@ const LoadingState = () => (
</div> </div>
); );
// 格式化持续时间
const formatDuration = (ms?: number) => {
if (!ms) return '-';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes > 0) {
return `${minutes}${seconds % 60}`;
}
return `${seconds}`;
};
// 格式化时间
const formatTime = (timeStr?: string) => {
if (!timeStr) return '-';
try {
const date = new Date(timeStr);
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return timeStr;
}
};
// 获取状态图标和颜色
const getStatusIcon = (status?: string) => {
switch (status) {
case 'SUCCESS':
return { icon: CheckCircle2, color: 'text-green-600' };
case 'FAILED':
return { icon: XCircle, color: 'text-red-600' };
case 'RUNNING':
return { icon: Loader2, color: 'text-blue-600' };
default:
return { icon: Clock, color: 'text-gray-400' };
}
};
// 获取状态文本
const getStatusText = (status?: string) => {
switch (status) {
case 'SUCCESS':
return '成功';
case 'FAILED':
return '失败';
case 'RUNNING':
return '运行中';
case 'CANCELLED':
return '已取消';
default:
return '未知';
}
};
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const { toast } = useToast(); const { toast } = useToast();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -257,45 +319,214 @@ const Dashboard: React.FC = () => {
<div className="flex items-start gap-2 mb-3"> <div className="flex items-start gap-2 mb-3">
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" /> <Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4> {app.applicationName ? (
<code className="text-xs text-muted-foreground truncate block"> <h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
{app.applicationCode} ) : (
</code> <Skeleton className="h-4 w-24 mb-1" />
)}
{app.applicationCode ? (
<code className="text-xs text-muted-foreground truncate block">
{app.applicationCode}
</code>
) : (
<Skeleton className="h-3 w-20" />
)}
</div> </div>
</div> </div>
<div className="space-y-1.5 mb-3 text-xs text-muted-foreground"> <div className="space-y-1.5 mb-3 text-xs text-muted-foreground">
{/* 分支 */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<GitBranch className="h-3 w-3 shrink-0" /> <GitBranch className="h-3 w-3 shrink-0" />
<code className="truncate font-mono">{app.branch}</code> {app.branch ? (
<code className="truncate font-mono">{app.branch}</code>
) : (
<Skeleton className="h-3 w-16" />
)}
</div> </div>
{/* 工作流 */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Rocket className="h-3 w-3 shrink-0" /> <Rocket className="h-3 w-3 shrink-0" />
<span className="truncate">{app.workflowDefinitionName || '未配置'}</span> {app.workflowDefinitionName ? (
<span className="truncate">{app.workflowDefinitionName}</span>
) : (
<Skeleton className="h-3 w-20" />
)}
</div> </div>
{app.deploySystemName && ( {/* Jenkins */}
{app.deploySystemName ? (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Server className="h-3 w-3 shrink-0" /> <Server className="h-3 w-3 shrink-0" />
<span className="truncate">{app.deploySystemName}</span> <span className="truncate">{app.deploySystemName}</span>
</div> </div>
) : (
<div className="flex items-center gap-1.5">
<Server className="h-3 w-3 shrink-0" />
<Skeleton className="h-3 w-24" />
</div>
)}
</div>
{/* 部署统计信息 */}
<div className="mb-3 pt-2 border-t border-dashed">
<div className="grid grid-cols-3 gap-1.5 mb-2 text-xs">
{/* 总次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-1">
<TrendingUp className="h-3 w-3 text-muted-foreground" />
<span className="font-semibold">{app.deployStatistics.totalCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 成功次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-1">
<CheckCircle2 className="h-3 w-3 text-green-600" />
<span className="font-semibold text-green-600">{app.deployStatistics.successCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
{/* 失败次数 */}
<div className="text-center">
{app.deployStatistics ? (
<>
<div className="flex items-center justify-center gap-1">
<XCircle className="h-3 w-3 text-red-600" />
<span className="font-semibold text-red-600">{app.deployStatistics.failedCount ?? 0}</span>
</div>
<div className="text-[10px] text-muted-foreground"></div>
</>
) : (
<>
<Skeleton className="h-4 w-8 mx-auto mb-1" />
<Skeleton className="h-3 w-12 mx-auto" />
</>
)}
</div>
</div>
{/* 最近部署信息 */}
{app.deployStatistics ? (
<div className="flex items-center gap-1 text-[10px] text-muted-foreground flex-wrap">
<Clock className="h-3 w-3 shrink-0" />
{app.deployStatistics.lastDeployTime ? (
<>
<span>: {formatTime(app.deployStatistics.lastDeployTime)}</span>
{app.deployStatistics.lastDeployBy ? (
<span>by {app.deployStatistics.lastDeployBy}</span>
) : (
<Skeleton className="h-3 w-16" />
)}
{app.deployStatistics.latestStatus ? (() => {
const { icon: StatusIcon, color } = getStatusIcon(app.deployStatistics.latestStatus);
return (
<span className={cn("flex items-center gap-0.5", color)}>
<StatusIcon className={cn("h-3 w-3", app.deployStatistics.latestStatus === 'RUNNING' && "animate-spin")} />
{getStatusText(app.deployStatistics.latestStatus)}
</span>
);
})() : (
<Skeleton className="h-3 w-12" />
)}
</>
) : (
<Skeleton className="h-3 w-32" />
)}
</div>
) : (
<div className="flex items-center gap-1 text-[10px]">
<Clock className="h-3 w-3 shrink-0" />
<Skeleton className="h-3 w-32" />
</div>
)}
{/* 最近部署记录 */}
{app.recentDeployRecords && app.recentDeployRecords.length > 0 ? (
<div className="mt-2 space-y-1">
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground">
<History className="h-3 w-3" />
<span></span>
</div>
{app.recentDeployRecords.slice(0, 2).map((record) => {
const { icon: StatusIcon, color } = getStatusIcon(record.status);
return (
<div key={record.id} className="flex items-center justify-between text-[10px]">
<div className="flex items-center gap-1 flex-1 min-w-0">
<StatusIcon className={cn("h-3 w-3 shrink-0", color, record.status === 'RUNNING' && "animate-spin")} />
{record.startTime ? (
<>
<span className="truncate">{formatTime(record.startTime)}</span>
{record.deployRemark && (
<span className="text-muted-foreground truncate">- {record.deployRemark}</span>
)}
</>
) : (
<Skeleton className="h-3 w-20" />
)}
</div>
{record.duration ? (
<span className="text-muted-foreground shrink-0 ml-1">
{formatDuration(record.duration)}
</span>
) : (
<Skeleton className="h-3 w-12 shrink-0" />
)}
</div>
);
})}
</div>
) : (
<div className="mt-2 space-y-1">
<div className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground">
<History className="h-3 w-3" />
<span></span>
</div>
{/* 显示2条骨架记录 */}
{[1, 2].map((i) => (
<div key={i} className="flex items-center justify-between text-[10px]">
<Skeleton className="h-3 w-24" />
<Skeleton className="h-3 w-12 shrink-0" />
</div>
))}
</div>
)} )}
</div> </div>
<Button <Button
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)} onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
disabled={!currentEnv?.enabled || isDeploying} disabled={!currentEnv?.enabled || isDeploying || app.isDeploying}
size="sm" size="sm"
className={cn( className={cn(
'w-full text-xs h-8', 'w-full text-xs h-8',
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700' currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
)} )}
> >
{isDeploying ? ( {(isDeploying || app.isDeploying) ? (
<> <>
<Loader2 className="h-3 w-3 mr-1 animate-spin" /> <Loader2 className="h-3 w-3 mr-1 animate-spin" />
</> </>
) : ( ) : (
<> <>

View File

@ -1,11 +1,29 @@
import type { BaseResponse } from '@/types/base';
export interface Approver { export interface Approver {
userId: number; userId: number;
username: string; username: string;
realName: string; realName: string;
} }
export interface DeployStatistics {
totalCount: number;
successCount: number;
failedCount: number;
runningCount: number;
lastDeployTime?: string;
lastDeployBy?: string;
latestStatus?: 'SUCCESS' | 'FAILED' | 'RUNNING' | 'CANCELLED';
}
export interface DeployRecord {
id: number;
status: 'SUCCESS' | 'FAILED' | 'RUNNING' | 'CANCELLED';
deployBy: string;
startTime: string;
endTime?: string;
deployRemark?: string;
duration?: number; // 持续时间(毫秒)
}
export interface ApplicationConfig { export interface ApplicationConfig {
teamApplicationId: number; teamApplicationId: number;
applicationId: number; applicationId: number;
@ -19,10 +37,13 @@ export interface ApplicationConfig {
workflowDefinitionId?: number; workflowDefinitionId?: number;
workflowDefinitionName?: string; workflowDefinitionName?: string;
workflowDefinitionKey?: string; workflowDefinitionKey?: string;
deployStatistics?: DeployStatistics;
isDeploying?: boolean;
recentDeployRecords?: DeployRecord[];
} }
export interface DeployEnvironment { export interface DeployEnvironment {
environmentId: number; environmentId: number;
environmentCode: string; environmentCode: string;
environmentName: string; environmentName: string;
environmentDesc?: string; environmentDesc?: string;

View File

@ -42,7 +42,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
if (open) { if (open) {
if (record) { if (record) {
setFormData({ setFormData({
tenantCode: record.tenantCode,
envCode: record.envCode, envCode: record.envCode,
envName: record.envName, envName: record.envName,
envDesc: record.envDesc, envDesc: record.envDesc,
@ -58,10 +57,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
// 验证 // 验证
if (!formData.tenantCode?.trim()) {
toast({ title: '提示', description: '请输入租户代码', variant: 'destructive' });
return;
}
if (!formData.envCode?.trim()) { if (!formData.envCode?.trim()) {
toast({ title: '提示', description: '请输入环境编码', variant: 'destructive' }); toast({ title: '提示', description: '请输入环境编码', variant: 'destructive' });
return; return;
@ -99,17 +94,6 @@ const EditDialog: React.FC<EditDialogProps> = ({
</DialogHeader> </DialogHeader>
<DialogBody> <DialogBody>
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid gap-2">
<Label htmlFor="tenantCode"> *</Label>
<Input
id="tenantCode"
value={formData.tenantCode || ''}
onChange={(e) => setFormData({ ...formData, tenantCode: e.target.value })}
placeholder="请输入租户代码"
disabled={!!record}
/>
</div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label htmlFor="envCode"> *</Label> <Label htmlFor="envCode"> *</Label>
<Input <Input

View File

@ -228,11 +228,28 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// 设置输入映射默认值(转换 UUID 为显示名称) // 设置输入映射默认值(转换 UUID 为显示名称)
if (isConfigurableNode(nodeDefinition)) { if (isConfigurableNode(nodeDefinition)) {
const savedInputMapping = nodeData.inputMapping || {};
const displayInputMapping = convertObjectToDisplayName( const displayInputMapping = convertObjectToDisplayName(
nodeData.inputMapping || {}, savedInputMapping,
allNodes allNodes
); );
inputForm.reset(displayInputMapping);
// ✅ 从 schema 中提取 default 值并合并
const schemaDefaults: Record<string, any> = {};
if (nodeDefinition.inputMappingSchema?.properties) {
Object.keys(nodeDefinition.inputMappingSchema.properties).forEach(key => {
const prop = nodeDefinition.inputMappingSchema!.properties![key];
// 如果该字段没有保存的值,使用 schema 中的 default
if (!(key in displayInputMapping) && 'default' in prop) {
schemaDefaults[key] = prop.default;
}
});
}
inputForm.reset({
...schemaDefaults,
...displayInputMapping,
});
} }
} }
// 只在 visible 或 node.id 改变时重置表单 // 只在 visible 或 node.id 改变时重置表单