增加代码编辑器表单组件

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 { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import {
Package,
@ -11,7 +12,11 @@ import {
GitBranch,
Users,
Server,
CheckCircle2
CheckCircle2,
XCircle,
Clock,
TrendingUp,
History
} from "lucide-react";
import { useToast } from '@/components/ui/use-toast';
import { getDeployEnvironments, startDeployment } from './service';
@ -26,6 +31,63 @@ const LoadingState = () => (
</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 { toast } = useToast();
const [loading, setLoading] = useState(true);
@ -257,45 +319,214 @@ const Dashboard: React.FC = () => {
<div className="flex items-start gap-2 mb-3">
<Package className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
{app.applicationName ? (
<h4 className="font-semibold text-sm mb-1 truncate">{app.applicationName}</h4>
) : (
<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 className="space-y-1.5 mb-3 text-xs text-muted-foreground">
{/* 分支 */}
<div className="flex items-center gap-1.5">
<GitBranch className="h-3 w-3 shrink-0" />
{app.branch ? (
<code className="truncate font-mono">{app.branch}</code>
) : (
<Skeleton className="h-3 w-16" />
)}
</div>
{/* 工作流 */}
<div className="flex items-center gap-1.5">
<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>
{app.deploySystemName && (
{/* Jenkins */}
{app.deploySystemName ? (
<div className="flex items-center gap-1.5">
<Server className="h-3 w-3 shrink-0" />
<span className="truncate">{app.deploySystemName}</span>
</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>
<Button
onClick={() => handleDeploy(app, currentEnv?.requiresApproval || false)}
disabled={!currentEnv?.enabled || isDeploying}
disabled={!currentEnv?.enabled || isDeploying || app.isDeploying}
size="sm"
className={cn(
'w-full text-xs h-8',
currentEnv?.requiresApproval && 'bg-amber-600 hover:bg-amber-700'
)}
>
{isDeploying ? (
{(isDeploying || app.isDeploying) ? (
<>
<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 {
userId: number;
username: 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 {
teamApplicationId: number;
applicationId: number;
@ -19,6 +37,9 @@ export interface ApplicationConfig {
workflowDefinitionId?: number;
workflowDefinitionName?: string;
workflowDefinitionKey?: string;
deployStatistics?: DeployStatistics;
isDeploying?: boolean;
recentDeployRecords?: DeployRecord[];
}
export interface DeployEnvironment {

View File

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

View File

@ -228,11 +228,28 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
// 设置输入映射默认值(转换 UUID 为显示名称)
if (isConfigurableNode(nodeDefinition)) {
const savedInputMapping = nodeData.inputMapping || {};
const displayInputMapping = convertObjectToDisplayName(
nodeData.inputMapping || {},
savedInputMapping,
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 改变时重置表单