增加团队管理页面

This commit is contained in:
dengqichen 2025-10-29 16:41:40 +08:00
parent 6d882dd6e3
commit e89f7a90e8
13 changed files with 1710 additions and 1308 deletions

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { import {
Card, Card,
CardContent,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/components/ui/card'; } from '@/components/ui/card';
@ -14,6 +13,7 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
@ -25,16 +25,19 @@ import {
} from '@/components/ui/tooltip'; } from '@/components/ui/tooltip';
import { import {
GitBranch, GitBranch,
FolderGit2, FolderGit2,
RefreshCw, RefreshCw,
Globe, Globe,
Lock, Lock,
Users, Users,
Search, Search,
Loader2, Loader2,
ChevronRight, ChevronRight,
ChevronDown, ChevronDown,
ExternalLink, ExternalLink,
Shield, Shield,
Code2, Code2,
Calendar, Calendar,
@ -45,19 +48,20 @@ import type {
RepositoryGroupResponse, RepositoryGroupResponse,
RepositoryProjectResponse, RepositoryProjectResponse,
RepositoryBranchResponse, RepositoryBranchResponse,
GitStatistics,
} from './types'; } from './types';
import { import {
getGitInstances, getGitInstances,
getRepositoryGroups, getRepositoryGroups,
getRepositoryProjects, getRepositoryProjects,
getRepositoryBranches, getRepositoryBranches,
syncAllGitData, syncAllGitData,
syncRepositoryGroups, syncRepositoryGroups,
syncRepositoryProjects, syncRepositoryProjects,
syncRepositoryBranches, syncRepositoryBranches,
getGitStatistics,
} from './service'; } from './service';
import type { ExternalSystemResponse } from '@/pages/Deploy/External/types'; import type { ExternalSystemResponse } from '@/pages/Deploy/External/types';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
@ -66,9 +70,11 @@ import 'dayjs/locale/zh-cn';
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
dayjs.locale('zh-cn'); dayjs.locale('zh-cn');
// 可见性配置 // 可见性配置
const VISIBILITY_CONFIG = { const VISIBILITY_CONFIG = {
public: { label: '公开', variant: 'default' as const, icon: Globe, color: 'text-green-600' }, public: { label: '公开', variant: 'default' as const, icon: Globe, color: 'text-green-600' },
private: { label: '私有', variant: 'secondary' as const, icon: Lock, color: 'text-red-600' }, private: { label: '私有', variant: 'secondary' as const, icon: Lock, color: 'text-red-600' },
internal: { label: '内部', variant: 'outline' as const, icon: Users, color: 'text-yellow-600' }, internal: { label: '内部', variant: 'outline' as const, icon: Users, color: 'text-yellow-600' },
}; };
@ -76,6 +82,7 @@ const VISIBILITY_CONFIG = {
const GitManager: React.FC = () => { const GitManager: React.FC = () => {
const { toast } = useToast(); const { toast } = useToast();
// Git 实例选择 // Git 实例选择
const [instances, setInstances] = useState<ExternalSystemResponse[]>([]); const [instances, setInstances] = useState<ExternalSystemResponse[]>([]);
const [selectedInstanceId, setSelectedInstanceId] = useState<number>(); const [selectedInstanceId, setSelectedInstanceId] = useState<number>();
@ -84,7 +91,6 @@ const GitManager: React.FC = () => {
const [groupTree, setGroupTree] = useState<RepositoryGroupResponse[]>([]); const [groupTree, setGroupTree] = useState<RepositoryGroupResponse[]>([]);
const [projects, setProjects] = useState<RepositoryProjectResponse[]>([]); const [projects, setProjects] = useState<RepositoryProjectResponse[]>([]);
const [branches, setBranches] = useState<RepositoryBranchResponse[]>([]); const [branches, setBranches] = useState<RepositoryBranchResponse[]>([]);
const [statistics, setStatistics] = useState<GitStatistics>();
// 选中状态 // 选中状态
const [selectedGroup, setSelectedGroup] = useState<RepositoryGroupResponse>(); const [selectedGroup, setSelectedGroup] = useState<RepositoryGroupResponse>();
@ -96,7 +102,6 @@ const GitManager: React.FC = () => {
groups: false, groups: false,
projects: false, projects: false,
branches: false, branches: false,
statistics: false,
}); });
// 同步状态 // 同步状态
@ -107,14 +112,18 @@ const GitManager: React.FC = () => {
branches: false, branches: false,
}); });
// 搜索过滤 // 搜索过滤
const [groupSearch, setGroupSearch] = useState(''); const [groupSearch, setGroupSearch] = useState('');
const [projectSearch, setProjectSearch] = useState(''); const [projectSearch, setProjectSearch] = useState('');
const [branchSearch, setBranchSearch] = useState(''); const [branchSearch, setBranchSearch] = useState('');
// 加载 Git 实例列表 // 加载 Git 实例列表
const loadInstances = async () => { const loadInstances = async () => {
try { try {
const data = await getGitInstances(); const data = await getGitInstances();
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
setInstances(data); setInstances(data);
@ -124,6 +133,7 @@ const GitManager: React.FC = () => {
} }
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: '加载失败', title: '加载失败',
description: '加载 Git 实例列表失败', description: '加载 Git 实例列表失败',
@ -131,6 +141,7 @@ const GitManager: React.FC = () => {
} }
}; };
// 构建树形结构 // 构建树形结构
const buildTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => { const buildTree = (groups: RepositoryGroupResponse[]): RepositoryGroupResponse[] => {
// 使用 repoGroupId 作为 key因为 parentId 对应的是 Git 系统中的 repoGroupId // 使用 repoGroupId 作为 key因为 parentId 对应的是 Git 系统中的 repoGroupId
@ -226,6 +237,7 @@ const GitManager: React.FC = () => {
setBranches(data || []); setBranches(data || []);
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: '加载失败', title: '加载失败',
description: '加载分支列表失败', description: '加载分支列表失败',
@ -236,38 +248,18 @@ const GitManager: React.FC = () => {
} }
}; };
// 加载统计信息 // 同步所有数据(异步任务)
const loadStatistics = async () => {
if (!selectedInstanceId) return;
setLoading((prev) => ({ ...prev, statistics: true }));
try {
const data = await getGitStatistics(selectedInstanceId);
setStatistics(data);
} catch (error) {
// 静默失败
} finally {
setLoading((prev) => ({ ...prev, statistics: false }));
}
};
// 同步所有数据
const handleSyncAll = async () => { const handleSyncAll = async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, all: true })); setSyncing((prev) => ({ ...prev, all: true }));
try { try {
await syncAllGitData(selectedInstanceId); await syncAllGitData(selectedInstanceId);
toast({ toast({
title: '同步成功', title: '同步任务已启动',
description: '已开始同步所有数据', description: '正在后台同步所有数据,请稍后手动刷新查看结果',
}); });
// 重新加载数据
await Promise.all([
loadGroupTree(),
loadProjects(selectedGroup?.repoGroupId || selectedGroup?.id),
loadStatistics(),
]);
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
@ -279,31 +271,32 @@ const GitManager: React.FC = () => {
} }
}; };
// 同步仓库组 // 同步仓库组(异步任务)
const handleSyncGroups = async () => { const handleSyncGroups = async () => {
if (!selectedInstanceId) return; if (!selectedInstanceId) return;
setSyncing((prev) => ({ ...prev, groups: true })); setSyncing((prev) => ({ ...prev, groups: true }));
try { try {
await syncRepositoryGroups(selectedInstanceId); await syncRepositoryGroups(selectedInstanceId);
toast({ toast({
title: '同步成功', title: '同步任务已启动',
description: '已开始同步仓库组', description: '正在后台同步仓库组,请稍后手动刷新查看结果',
}); });
await loadGroupTree();
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: '同步失败', title: '同步失败',
description: '仓库组同步失败', description: '仓库组同步任务启动失败',
}); });
} finally { } finally {
setSyncing((prev) => ({ ...prev, groups: false })); setSyncing((prev) => ({ ...prev, groups: false }));
} }
}; };
// 同步项目 // 同步项目(异步任务)
const handleSyncProjects = async () => { const handleSyncProjects = async () => {
if (!selectedInstanceId || !selectedGroup) return; if (!selectedInstanceId || !selectedGroup) return;
setSyncing((prev) => ({ ...prev, projects: true })); setSyncing((prev) => ({ ...prev, projects: true }));
@ -314,43 +307,47 @@ const GitManager: React.FC = () => {
selectedGroup.repoGroupId selectedGroup.repoGroupId
); );
toast({ toast({
title: '同步成功', title: '同步任务已启动',
description: `已开始同步仓库组"${selectedGroup.name}"的项目`, description: `正在后台同步仓库组"${selectedGroup.name}"的项目,请稍后手动刷新查看结果`,
}); });
await loadProjects(selectedGroup.repoGroupId || selectedGroup.id);
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: '同步失败', title: '同步失败',
description: '项目同步失败', description: '项目同步任务启动失败',
}); });
} finally { } finally {
setSyncing((prev) => ({ ...prev, projects: false })); setSyncing((prev) => ({ ...prev, projects: false }));
} }
}; };
// 同步分支 // 同步分支(异步任务)
const handleSyncBranches = async () => { const handleSyncBranches = async () => {
if (!selectedInstanceId || !selectedProject || !selectedGroup) return;
if (!selectedInstanceId || !selectedGroup) return;
setSyncing((prev) => ({ ...prev, branches: true })); setSyncing((prev) => ({ ...prev, branches: true }));
try { try {
// 传递 repoProjectId, repoGroupId (Git系统中的ID) 和 externalSystemId // 如果选择了项目,只同步该项目的分支;否则同步该组下所有项目的分支
await syncRepositoryBranches( await syncRepositoryBranches(
selectedInstanceId, selectedInstanceId,
selectedProject.repoProjectId, selectedProject?.repoProjectId,
selectedGroup.repoGroupId selectedGroup.repoGroupId
); );
const description = selectedProject
? `正在后台同步项目"${selectedProject.name}"的分支,请稍后手动刷新查看结果`
: `正在后台同步组"${selectedGroup.name}"下所有项目的分支,请稍后手动刷新查看结果`;
toast({ toast({
title: '同步成功', title: '同步任务已启动',
description: `已开始同步项目"${selectedProject.name}"的分支`, description,
}); });
await loadBranches(selectedProject.repoProjectId || selectedProject.id);
} catch (error) { } catch (error) {
toast({ toast({
variant: 'destructive', variant: 'destructive',
title: '同步失败', title: '同步失败',
description: '分支同步失败', description: '分支同步任务启动失败',
}); });
} finally { } finally {
setSyncing((prev) => ({ ...prev, branches: false })); setSyncing((prev) => ({ ...prev, branches: false }));
@ -370,6 +367,7 @@ const GitManager: React.FC = () => {
}); });
}; };
// 选中仓库组 // 选中仓库组
const handleSelectGroup = (group: RepositoryGroupResponse) => { const handleSelectGroup = (group: RepositoryGroupResponse) => {
setSelectedGroup(group); setSelectedGroup(group);
@ -386,6 +384,7 @@ const GitManager: React.FC = () => {
// 格式化时间 // 格式化时间
const formatTime = (time?: string) => { const formatTime = (time?: string) => {
if (!time) return '-'; if (!time) return '-';
const diff = dayjs().diff(dayjs(time), 'day'); const diff = dayjs().diff(dayjs(time), 'day');
if (diff > 7) { if (diff > 7) {
@ -394,6 +393,7 @@ const GitManager: React.FC = () => {
return dayjs(time).fromNow(); return dayjs(time).fromNow();
}; };
// 渲染可见性图标(带 Tooltip // 渲染可见性图标(带 Tooltip
const renderVisibilityIcon = (visibility: string) => { const renderVisibilityIcon = (visibility: string) => {
const config = const config =
@ -421,6 +421,7 @@ const GitManager: React.FC = () => {
return groups.filter((group) => { return groups.filter((group) => {
const matchesSearch = const matchesSearch =
group.name.toLowerCase().includes(groupSearch.toLowerCase()) || group.name.toLowerCase().includes(groupSearch.toLowerCase()) ||
(group.path && group.path.toLowerCase().includes(groupSearch.toLowerCase())); (group.path && group.path.toLowerCase().includes(groupSearch.toLowerCase()));
const filteredChildren = group.children const filteredChildren = group.children
@ -511,10 +512,12 @@ const GitManager: React.FC = () => {
// 过滤项目列表 // 过滤项目列表
const filteredProjects = useMemo(() => { const filteredProjects = useMemo(() => {
if (!projectSearch) return projects; if (!projectSearch) return projects;
return projects.filter( return projects.filter(
(project) => (project) =>
project.name.toLowerCase().includes(projectSearch.toLowerCase()) || project.name.toLowerCase().includes(projectSearch.toLowerCase()) ||
(project.path && project.path.toLowerCase().includes(projectSearch.toLowerCase())) (project.path && project.path.toLowerCase().includes(projectSearch.toLowerCase()))
); );
}, [projects, projectSearch]); }, [projects, projectSearch]);
@ -533,9 +536,9 @@ const GitManager: React.FC = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (selectedInstanceId) { if (selectedInstanceId) {
loadGroupTree(); loadGroupTree();
loadStatistics();
setSelectedGroup(undefined); setSelectedGroup(undefined);
setSelectedProject(undefined); setSelectedProject(undefined);
setProjects([]); setProjects([]);
@ -546,25 +549,31 @@ const GitManager: React.FC = () => {
const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, groupSearch]); const filteredGroupTree = useMemo(() => filterGroupTree(groupTree), [groupTree, groupSearch]);
return ( return (
<TooltipProvider> <TooltipProvider>
<div className="flex flex-col h-full p-6 space-y-4"> <div className="flex flex-col h-full p-6 gap-4">
{/* 顶部标题栏 */} {/* 顶部标题栏 */}
<div className="flex items-center justify-between flex-shrink-0"> <div className="flex items-center justify-between flex-shrink-0">
<div> <div>
<h1 className="text-3xl font-bold">Git </h1> <h1 className="text-3xl font-bold">Git </h1>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
Git Git
</p> </p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Select <Select
value={selectedInstanceId?.toString()} value={selectedInstanceId?.toString()}
onValueChange={(value) => setSelectedInstanceId(Number(value))} onValueChange={(value) => setSelectedInstanceId(Number(value))}
> >
<SelectTrigger className="w-[200px]"> <SelectTrigger className="w-[200px]">
<SelectValue placeholder="选择GIT实例" /> <SelectValue placeholder="选择GIT实例" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{instances.map((instance) => ( {instances.map((instance) => (
<SelectItem key={instance.id} value={instance.id.toString()}> <SelectItem key={instance.id} value={instance.id.toString()}>
{instance.name} {instance.name}
@ -572,6 +581,7 @@ const GitManager: React.FC = () => {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
onClick={handleSyncAll} onClick={handleSyncAll}
disabled={syncing.all || !selectedInstanceId} disabled={syncing.all || !selectedInstanceId}
@ -587,39 +597,6 @@ const GitManager: React.FC = () => {
</div> </div>
</div> </div>
{/* 统计卡片 */}
{statistics && (
<div className="grid grid-cols-3 gap-4 flex-shrink-0">
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<FolderGit2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statistics.totalGroups}</div>
</CardContent>
</Card>
<Card className="border-l-4 border-l-purple-500">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<Code2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statistics.totalProjects}</div>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium"></CardTitle>
<GitBranch className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{statistics.totalBranches}</div>
</CardContent>
</Card>
</div>
)}
{/* 三栏布局 */} {/* 三栏布局 */}
<div className="flex-1 grid grid-cols-12 gap-4 min-h-0 overflow-hidden"> <div className="flex-1 grid grid-cols-12 gap-4 min-h-0 overflow-hidden">
{/* 左栏:仓库组树 */} {/* 左栏:仓库组树 */}
@ -630,6 +607,7 @@ const GitManager: React.FC = () => {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncGroups} onClick={handleSyncGroups}
disabled={syncing.groups || !selectedInstanceId} disabled={syncing.groups || !selectedInstanceId}
@ -639,15 +617,19 @@ const GitManager: React.FC = () => {
/> />
</Button> </Button>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
placeholder="搜索仓库组..." placeholder="搜索仓库组..."
value={groupSearch} value={groupSearch}
onChange={(e) => setGroupSearch(e.target.value)} onChange={(e) => setGroupSearch(e.target.value)}
className="pl-8 h-8" className="pl-8 h-8"
/> />
</div> </div>
</CardHeader> </CardHeader>
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="p-2 space-y-1"> <div className="p-2 space-y-1">
@ -664,9 +646,11 @@ const GitManager: React.FC = () => {
filteredGroupTree.map((group) => renderGroupTreeNode(group)) filteredGroupTree.map((group) => renderGroupTreeNode(group))
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
</Card> </Card>
{/* 中栏:项目列表 */} {/* 中栏:项目列表 */}
<Card className="col-span-5 flex flex-col min-h-0"> <Card className="col-span-5 flex flex-col min-h-0">
<CardHeader className="border-b flex-shrink-0"> <CardHeader className="border-b flex-shrink-0">
@ -676,6 +660,7 @@ const GitManager: React.FC = () => {
</CardTitle> </CardTitle>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncProjects} onClick={handleSyncProjects}
@ -686,34 +671,42 @@ const GitManager: React.FC = () => {
/> />
</Button> </Button>
</div> </div>
<div className="relative mt-2"> <div className="relative mt-2">
<Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" /> <Search className="absolute left-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input <Input
placeholder="搜索项目..." placeholder="搜索项目..."
value={projectSearch} value={projectSearch}
onChange={(e) => setProjectSearch(e.target.value)} onChange={(e) => setProjectSearch(e.target.value)}
className="pl-8 h-8" className="pl-8 h-8"
/> />
</div> </div>
</CardHeader> </CardHeader>
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{loading.projects ? ( {loading.projects ? (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" /> <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div> </div>
) : !selectedGroup ? ( ) : !selectedGroup ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Code2 className="h-12 w-12 mb-2 opacity-20" /> <Code2 className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm"></p>
</div> </div>
) : filteredProjects.length === 0 ? ( ) : filteredProjects.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground"> <div className="flex flex-col items-center justify-center h-full text-muted-foreground">
<Code2 className="h-12 w-12 mb-2 opacity-20" /> <Code2 className="h-12 w-12 mb-2 opacity-20" />
<p className="text-sm"></p> <p className="text-sm"></p>
</div> </div>
) : ( ) : (
<div className="p-2 space-y-2"> <div className="p-2 space-y-2">
{filteredProjects.map((project) => ( {filteredProjects.map((project) => (
<div <div
key={project.id} key={project.id}
className={`p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${ className={`p-3 border rounded-lg cursor-pointer hover:bg-accent transition-colors ${
@ -732,6 +725,7 @@ const GitManager: React.FC = () => {
{project.name} {project.name}
</span> </span>
</div> </div>
{project.description && ( {project.description && (
<p className="text-xs text-muted-foreground line-clamp-2"> <p className="text-xs text-muted-foreground line-clamp-2">
{project.description} {project.description}
@ -742,23 +736,27 @@ const GitManager: React.FC = () => {
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GitBranch className="h-3 w-3" /> <GitBranch className="h-3 w-3" />
{project.isDefaultBranch} {project.isDefaultBranch}
</span> </span>
)} )}
{project.lastActivityAt && ( {project.lastActivityAt && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{formatTime(project.lastActivityAt)} {formatTime(project.lastActivityAt)}
</span> </span>
)} )}
</div> </div>
</div> </div>
{project.webUrl && ( {project.webUrl && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<a <a
href={project.webUrl} href={project.webUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex-shrink-0" className="flex-shrink-0"
> >
@ -792,7 +790,7 @@ const GitManager: React.FC = () => {
size="icon" size="icon"
className="h-7 w-7" className="h-7 w-7"
onClick={handleSyncBranches} onClick={handleSyncBranches}
disabled={syncing.branches || !selectedProject} disabled={syncing.branches || !selectedGroup}
> >
<RefreshCw <RefreshCw
className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`} className={`h-4 w-4 ${syncing.branches ? 'animate-spin' : ''}`}
@ -897,6 +895,7 @@ const GitManager: React.FC = () => {
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />
</Button> </Button>
</a> </a>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> GitLab </p> <p> GitLab </p>
@ -912,8 +911,10 @@ const GitManager: React.FC = () => {
</Card> </Card>
</div> </div>
</div> </div>
</TooltipProvider> </TooltipProvider>
); );
}; };
export default GitManager; export default GitManager;

View File

@ -6,7 +6,6 @@ import type {
RepositoryProjectQuery, RepositoryProjectQuery,
RepositoryBranchResponse, RepositoryBranchResponse,
RepositoryBranchQuery, RepositoryBranchQuery,
GitStatistics,
} from './types'; } from './types';
import { getExternalSystems } from '@/pages/Deploy/External/service'; import { getExternalSystems } from '@/pages/Deploy/External/service';
import { SystemType } from '@/pages/Deploy/External/types'; import { SystemType } from '@/pages/Deploy/External/types';
@ -61,6 +60,8 @@ export const getProjectsByGroupId = (repoGroupId: number) =>
/** /**
* *
* @param externalSystemId ID
* @param repoGroupId ID==
*/ */
export const syncRepositoryProjects = (externalSystemId: number, repoGroupId?: number) => export const syncRepositoryProjects = (externalSystemId: number, repoGroupId?: number) =>
request.post<void>(`${PROJECT_URL}/sync`, null, { request.post<void>(`${PROJECT_URL}/sync`, null, {
@ -85,6 +86,9 @@ export const getBranchesByProjectId = (repoProjectId: number) =>
/** /**
* *
* @param externalSystemId ID
* @param repoProjectId ID=repoGroupId规则=repoGroupId必传
* @param repoGroupId ID==
*/ */
export const syncRepositoryBranches = ( export const syncRepositoryBranches = (
externalSystemId: number, externalSystemId: number,
@ -95,16 +99,6 @@ export const syncRepositoryBranches = (
params: { externalSystemId, repoProjectId, repoGroupId }, params: { externalSystemId, repoProjectId, repoGroupId },
}); });
// ==================== 统计信息 ====================
/**
* Git仓库统计信息
*/
export const getGitStatistics = (externalSystemId: number) =>
request.get<GitStatistics>(`${GROUP_URL}/statistics`, {
params: { externalSystemId },
});
// ==================== 批量操作 ==================== // ==================== 批量操作 ====================
/** /**

View File

@ -136,16 +136,3 @@ export interface RepositoryBranchQuery {
isDefaultBranch?: boolean; isDefaultBranch?: boolean;
} }
/**
*
*/
export interface GitStatistics {
/** 总仓库组数 */
totalGroups: number;
/** 总项目数 */
totalProjects: number;
/** 总分支数 */
totalBranches: number;
/** 最后同步时间 */
lastSyncTime?: string;
}

View File

@ -0,0 +1,561 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/components/ui/use-toast';
import { useForm } from 'react-hook-form';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import {
Plus,
Edit,
Trash2,
Search,
Loader2,
FolderKanban,
CheckCircle2,
XCircle,
} from 'lucide-react';
import DynamicIcon from '@/components/DynamicIcon';
import LucideIconSelect from '@/components/LucideIconSelect';
import {
getJobCategories,
createJobCategory,
updateJobCategory,
deleteJobCategory
} from '../service';
import type {
JobCategoryResponse,
JobCategoryRequest,
JobCategoryQuery
} from '../types';
import type { Page } from '@/types/base';
interface CategoryManageDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
const CategoryManageDialog: React.FC<CategoryManageDialogProps> = ({
open,
onOpenChange,
onSuccess
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<JobCategoryResponse> | null>(null);
const [searchText, setSearchText] = useState('');
const [editMode, setEditMode] = useState(false);
const [editRecord, setEditRecord] = useState<JobCategoryResponse | null>(null);
const [iconSelectOpen, setIconSelectOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [deleteRecord, setDeleteRecord] = useState<JobCategoryResponse | null>(null);
// 分页状态
const [pageNum, setPageNum] = useState(DEFAULT_CURRENT - 1);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const form = useForm<JobCategoryRequest>({
defaultValues: {
name: '',
code: '',
description: '',
icon: '',
color: '',
sort: 0,
enabled: true,
}
});
// 加载分类列表
const loadCategories = async () => {
setLoading(true);
try {
const query: JobCategoryQuery = {
pageNum,
pageSize,
};
if (searchText) {
query.name = searchText;
}
const result = await getJobCategories(query);
setData(result);
} catch (error) {
console.error('加载分类失败:', error);
toast({
variant: 'destructive',
title: '加载失败',
description: '加载分类列表失败',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open) {
loadCategories();
}
}, [open, searchText, pageNum, pageSize]);
// 搜索时重置页码
useEffect(() => {
if (searchText !== '') {
setPageNum(0);
}
}, [searchText]);
// 新建
const handleCreate = () => {
setEditRecord(null);
form.reset({
name: '',
code: '',
description: '',
icon: '',
color: '',
sort: 0,
enabled: true,
});
setEditMode(true);
};
// 编辑
const handleEdit = (record: JobCategoryResponse) => {
setEditRecord(record);
form.reset({
name: record.name,
code: record.code,
description: record.description || '',
icon: record.icon || '',
color: record.color || '',
sort: record.sort,
enabled: record.enabled,
});
setEditMode(true);
};
// 打开删除确认对话框
const handleDeleteClick = (record: JobCategoryResponse) => {
setDeleteRecord(record);
setDeleteDialogOpen(true);
};
// 确认删除
const confirmDelete = async () => {
if (!deleteRecord) return;
try {
await deleteJobCategory(deleteRecord.id);
toast({
title: '删除成功',
description: `分类 "${deleteRecord.name}" 已删除`,
});
loadCategories();
onSuccess?.();
setDeleteDialogOpen(false);
setDeleteRecord(null);
} catch (error) {
console.error('删除失败:', error);
toast({
variant: 'destructive',
title: '删除失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 保存
const handleSave = async (values: JobCategoryRequest) => {
try {
if (editRecord) {
await updateJobCategory(editRecord.id, values);
toast({
title: '更新成功',
description: `分类 "${values.name}" 已更新`,
});
} else {
await createJobCategory(values);
toast({
title: '创建成功',
description: `分类 "${values.name}" 已创建`,
});
}
setEditMode(false);
loadCategories();
onSuccess?.();
} catch (error) {
console.error('保存失败:', error);
toast({
variant: 'destructive',
title: '保存失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 取消编辑
const handleCancel = () => {
setEditMode(false);
setEditRecord(null);
form.reset();
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-6xl max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FolderKanban className="h-5 w-5" />
</DialogTitle>
</DialogHeader>
{!editMode ? (
<div className="space-y-4">
{/* 搜索和新建 */}
<div className="flex items-center gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="搜索分类名称..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="pl-10"
/>
</div>
<Button onClick={handleCreate}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 分类列表 */}
<div className="rounded-md border">
<Table minWidth="770px">
<TableHeader>
<TableRow>
<TableHead width="160px"></TableHead>
<TableHead width="140px"></TableHead>
<TableHead width="60px"></TableHead>
<TableHead width="80px"></TableHead>
<TableHead width="80px"></TableHead>
<TableHead width="80px"></TableHead>
<TableHead width="150px"></TableHead>
<TableHead width="100px" sticky></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={8} className="text-center h-32">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : data && data.content.length > 0 ? (
data.content.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium">{record.name}</TableCell>
<TableCell>
<code className="text-xs bg-muted px-1 py-0.5 rounded">{record.code}</code>
</TableCell>
<TableCell>
{record.icon ? (
<DynamicIcon name={record.icon} className="h-5 w-5" />
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{record.color ? (
<div className="flex items-center gap-2">
<div
className="w-5 h-5 rounded border"
style={{ backgroundColor: record.color }}
/>
<span className="text-xs text-muted-foreground">{record.color}</span>
</div>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>{record.sort}</TableCell>
<TableCell>
{record.enabled ? (
<Badge variant="default" className="gap-1">
<CheckCircle2 className="h-3 w-3" />
</Badge>
) : (
<Badge variant="secondary" className="gap-1">
<XCircle className="h-3 w-3" />
</Badge>
)}
</TableCell>
<TableCell className="max-w-[150px] truncate" title={record.description}>
{record.description || '-'}
</TableCell>
<TableCell sticky width="100px">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleEdit(record)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => handleDeleteClick(record)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={8} className="text-center h-32 text-muted-foreground">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.content.length > 0 && (
<DataTablePagination
pageIndex={pageNum}
pageSize={pageSize}
totalPages={data.totalPages}
totalElements={data.totalElements}
onPageChange={setPageNum}
/>
)}
</div>
) : (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSave)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="name"
rules={{ required: '请输入分类名称' }}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} placeholder="请输入分类名称" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
rules={{ required: '请输入分类代码' }}
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input {...field} placeholder="请输入分类代码" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="icon"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<div className="flex gap-2">
<Input {...field} placeholder="选择图标" readOnly />
<Button
type="button"
variant="outline"
onClick={() => setIconSelectOpen(true)}
>
{field.value ? (
<DynamicIcon name={field.value} className="h-4 w-4" />
) : (
'选择'
)}
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="color"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<div className="flex gap-2">
<Input {...field} placeholder="#000000" />
<input
type="color"
value={field.value || '#000000'}
onChange={(e) => field.onChange(e.target.value)}
className="w-10 h-10 rounded border cursor-pointer"
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="sort"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Input
{...field}
type="number"
placeholder="请输入排序号"
onChange={e => field.onChange(Number(e.target.value))}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel></FormLabel>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel></FormLabel>
<FormControl>
<Textarea {...field} placeholder="请输入描述" rows={3} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={handleCancel}>
</Button>
<Button type="submit">
</Button>
</div>
</form>
</Form>
)}
</DialogContent>
</Dialog>
{/* 图标选择器 */}
<LucideIconSelect
open={iconSelectOpen}
onOpenChange={setIconSelectOpen}
value={form.watch('icon')}
onValueChange={(value) => form.setValue('icon', value)}
/>
{/* 删除确认对话框 */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
<span className="font-semibold">{deleteRecord?.name}</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete}></AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
export default CategoryManageDialog;

View File

@ -0,0 +1,327 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DataTablePagination } from '@/components/ui/pagination';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import {
FileText,
Loader2,
Search,
CheckCircle2,
XCircle,
Clock,
Server,
Calendar,
AlertCircle,
} from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { getJobLogs } from '../service';
import type { ScheduleJobResponse, ScheduleJobLogResponse, ScheduleJobLogQuery, LogStatus } from '../types';
import type { Page } from '@/types/base';
import dayjs from 'dayjs';
import { Textarea } from '@/components/ui/textarea';
interface JobLogDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
job: ScheduleJobResponse | null;
}
const JobLogDialog: React.FC<JobLogDialogProps> = ({
open,
onOpenChange,
job
}) => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<ScheduleJobLogResponse> | null>(null);
const [selectedLog, setSelectedLog] = useState<ScheduleJobLogResponse | null>(null);
const [query, setQuery] = useState<ScheduleJobLogQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE,
jobId: undefined,
status: undefined,
});
// 加载日志列表
const loadLogs = async () => {
if (!job) return;
setLoading(true);
try {
const result = await getJobLogs({
...query,
jobId: job.id,
});
setData(result);
} catch (error) {
console.error('加载日志失败:', error);
toast({
variant: 'destructive',
title: '加载失败',
description: '加载执行日志失败',
});
} finally {
setLoading(false);
}
};
useEffect(() => {
if (open && job) {
loadLogs();
}
}, [open, job, query]);
// 重置查询条件
useEffect(() => {
if (open && job) {
setQuery({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
jobId: job.id,
status: undefined,
});
setSelectedLog(null);
}
}, [open, job]);
// 状态徽章
const getStatusBadge = (status: LogStatus) => {
const statusMap: Record<LogStatus, {
variant: 'default' | 'secondary' | 'destructive';
text: string;
icon: React.ElementType
}> = {
SUCCESS: { variant: 'default', text: '成功', icon: CheckCircle2 },
FAIL: { variant: 'destructive', text: '失败', icon: XCircle },
TIMEOUT: { variant: 'secondary', text: '超时', icon: Clock },
};
const statusInfo = statusMap[status];
const Icon = statusInfo.icon;
return (
<Badge variant={statusInfo.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3" />
{statusInfo.text}
</Badge>
);
};
// 格式化耗时
const formatDuration = (duration?: number) => {
if (!duration) return '-';
if (duration < 1000) return `${duration}ms`;
if (duration < 60000) return `${(duration / 1000).toFixed(2)}s`;
return `${(duration / 60000).toFixed(2)}min`;
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-7xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
- {job?.jobName}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-hidden flex flex-col space-y-4">
{/* 筛选栏 */}
<div className="flex items-center gap-4">
<Select
value={query.status || ''}
onValueChange={(value) => setQuery({ ...query, status: value as LogStatus || undefined, pageNum: 0 })}
>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""></SelectItem>
<SelectItem value="SUCCESS"></SelectItem>
<SelectItem value="FAIL"></SelectItem>
<SelectItem value="TIMEOUT"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm" onClick={loadLogs}>
<Search className="h-4 w-4 mr-2" />
</Button>
</div>
{/* 日志列表 */}
<div className="flex-1 overflow-auto rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[180px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[100px]"></TableHead>
<TableHead className="w-[120px]">IP</TableHead>
<TableHead></TableHead>
<TableHead className="w-[80px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="h-32 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : data && data.content.length > 0 ? (
data.content.map((log) => (
<TableRow
key={log.id}
className={selectedLog?.id === log.id ? 'bg-accent' : ''}
>
<TableCell className="text-xs">
<div className="flex items-center gap-1">
<Calendar className="h-3 w-3 text-muted-foreground" />
{dayjs(log.executeTime).format('YYYY-MM-DD HH:mm:ss')}
</div>
</TableCell>
<TableCell className="text-xs">
{log.finishTime ? dayjs(log.finishTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</TableCell>
<TableCell className="text-xs font-mono">
{formatDuration(log.duration)}
</TableCell>
<TableCell>{getStatusBadge(log.status)}</TableCell>
<TableCell className="text-xs">
{log.serverIp ? (
<div className="flex items-center gap-1">
<Server className="h-3 w-3 text-muted-foreground" />
{log.serverIp}
</div>
) : (
'-'
)}
</TableCell>
<TableCell className="max-w-[300px] truncate text-xs" title={log.resultMessage}>
{log.resultMessage || '-'}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={() => setSelectedLog(log)}
>
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="h-32 text-center text-muted-foreground">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.content.length > 0 && (
<DataTablePagination
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={data.totalPages}
onPageChange={(pageIndex) => setQuery({ ...query, pageNum: pageIndex })}
/>
)}
{/* 日志详情 */}
{selectedLog && (
<div className="border-t pt-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
</h3>
<Button variant="ghost" size="sm" onClick={() => setSelectedLog(null)}>
</Button>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">Bean名称</span>
<code className="ml-2 text-xs bg-muted px-1 py-0.5 rounded">{selectedLog.beanName}</code>
</div>
<div>
<span className="text-muted-foreground"></span>
<code className="ml-2 text-xs bg-muted px-1 py-0.5 rounded">{selectedLog.methodName}</code>
</div>
<div>
<span className="text-muted-foreground"></span>
<span className="ml-2">{selectedLog.serverHost || '-'}</span>
</div>
<div>
<span className="text-muted-foreground">IP</span>
<span className="ml-2">{selectedLog.serverIp || '-'}</span>
</div>
</div>
{selectedLog.methodParams && (
<div>
<label className="text-sm text-muted-foreground mb-1 block"></label>
<Textarea
value={selectedLog.methodParams}
readOnly
className="font-mono text-xs h-20"
/>
</div>
)}
{selectedLog.resultMessage && (
<div>
<label className="text-sm text-muted-foreground mb-1 block"></label>
<Textarea
value={selectedLog.resultMessage}
readOnly
className="text-xs h-20"
/>
</div>
)}
{selectedLog.exceptionInfo && (
<div>
<label className="text-sm text-destructive mb-1 block flex items-center gap-1">
<XCircle className="h-4 w-4" />
</label>
<Textarea
value={selectedLog.exceptionInfo}
readOnly
className="font-mono text-xs h-40 text-destructive"
/>
</div>
)}
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};
export default JobLogDialog;

View File

@ -0,0 +1,458 @@
import React, { useState, useEffect, useMemo } from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DataTablePagination } from '@/components/ui/pagination';
import {
Loader2, Plus, Search, Edit, Trash2, Play, Pause,
Clock, Activity, CheckCircle2, XCircle, FolderKanban, PlayCircle, FileText
} from 'lucide-react';
import { useToast } from '@/components/ui/use-toast';
import { getScheduleJobs, getJobCategoryList, executeJobNow, pauseJob, resumeJob } from './service';
import type { ScheduleJobResponse, ScheduleJobQuery, JobCategoryResponse, JobStatus } from './types';
import type { Page } from '@/types/base';
import { DEFAULT_PAGE_SIZE, DEFAULT_CURRENT } from '@/utils/page';
import CategoryManageDialog from './components/CategoryManageDialog';
import JobLogDialog from './components/JobLogDialog';
import dayjs from 'dayjs';
/**
*
*/
const ScheduleJobList: React.FC = () => {
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Page<ScheduleJobResponse> | null>(null);
const [categories, setCategories] = useState<JobCategoryResponse[]>([]);
const [categoryDialogOpen, setCategoryDialogOpen] = useState(false);
const [logDialogOpen, setLogDialogOpen] = useState(false);
const [selectedJob, setSelectedJob] = useState<ScheduleJobResponse | null>(null);
const [query, setQuery] = useState<ScheduleJobQuery>({
pageNum: DEFAULT_CURRENT - 1,
pageSize: DEFAULT_PAGE_SIZE,
jobName: '',
categoryId: undefined,
status: undefined
});
// 加载数据
const loadData = async () => {
setLoading(true);
try {
const result = await getScheduleJobs(query);
setData(result);
} catch (error) {
console.error('加载定时任务失败:', error);
} finally {
setLoading(false);
}
};
// 加载分类
const loadCategories = async () => {
try {
const result = await getJobCategoryList();
setCategories(result || []);
} catch (error) {
console.error('加载分类失败:', error);
}
};
useEffect(() => {
loadCategories();
}, []);
useEffect(() => {
loadData();
}, [query]);
// 搜索
const handleSearch = () => {
setQuery(prev => ({ ...prev, pageNum: 0 }));
};
// 重置
const handleReset = () => {
setQuery({
pageNum: 0,
pageSize: DEFAULT_PAGE_SIZE,
jobName: '',
categoryId: undefined,
status: undefined
});
};
// 立即执行
const handleExecute = async (record: ScheduleJobResponse) => {
try {
await executeJobNow(record.id);
toast({
title: '执行成功',
description: `任务 "${record.jobName}" 已开始执行`,
});
loadData();
} catch (error) {
console.error('执行失败:', error);
toast({
variant: 'destructive',
title: '执行失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 暂停任务
const handlePause = async (record: ScheduleJobResponse) => {
try {
await pauseJob(record.id);
toast({
title: '暂停成功',
description: `任务 "${record.jobName}" 已暂停`,
});
loadData();
} catch (error) {
console.error('暂停失败:', error);
toast({
variant: 'destructive',
title: '暂停失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 恢复任务
const handleResume = async (record: ScheduleJobResponse) => {
try {
await resumeJob(record.id);
toast({
title: '恢复成功',
description: `任务 "${record.jobName}" 已恢复`,
});
loadData();
} catch (error) {
console.error('恢复失败:', error);
toast({
variant: 'destructive',
title: '恢复失败',
description: error instanceof Error ? error.message : '未知错误',
});
}
};
// 查看日志
const handleViewLog = (record: ScheduleJobResponse) => {
setSelectedJob(record);
setLogDialogOpen(true);
};
// 状态徽章
const getStatusBadge = (status: JobStatus) => {
const statusMap: Record<JobStatus, {
variant: 'default' | 'secondary' | 'destructive' | 'outline';
text: string;
icon: React.ElementType
}> = {
ENABLED: { variant: 'default', text: '启用', icon: CheckCircle2 },
DISABLED: { variant: 'secondary', text: '禁用', icon: XCircle },
PAUSED: { variant: 'outline', text: '暂停', icon: Pause },
};
const statusInfo = statusMap[status] || { variant: 'outline', text: status, icon: Clock };
const Icon = statusInfo.icon;
return (
<Badge variant={statusInfo.variant} className="inline-flex items-center gap-1">
<Icon className="h-3 w-3" />
{statusInfo.text}
</Badge>
);
};
// 统计数据
const stats = useMemo(() => {
const total = data?.totalElements || 0;
const enabledCount = data?.content?.filter(d => d.status === 'ENABLED').length || 0;
const pausedCount = data?.content?.filter(d => d.status === 'PAUSED').length || 0;
return { total, enabledCount, pausedCount };
}, [data]);
return (
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground"></h1>
<p className="text-muted-foreground mt-2">
</p>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
<Card className="bg-gradient-to-br from-blue-500/10 to-blue-500/5 border-blue-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-blue-700"></CardTitle>
<Activity className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.total}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-green-500/10 to-green-500/5 border-green-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-green-700"></CardTitle>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.enabledCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-yellow-500/10 to-yellow-500/5 border-yellow-500/20">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-yellow-700"></CardTitle>
<Pause className="h-4 w-4 text-yellow-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.pausedCount}</div>
<p className="text-xs text-muted-foreground mt-1"></p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCategoryDialogOpen(true)}>
<FolderKanban className="h-4 w-4 mr-2" />
</Button>
<Button onClick={() => toast({ title: '功能开发中' })}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
{/* 搜索栏 */}
<div className="flex flex-wrap items-center gap-4 mb-4">
<div className="flex-1 max-w-md">
<Input
placeholder="搜索任务名称..."
value={query.jobName}
onChange={(e) => setQuery({ ...query, jobName: e.target.value })}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Select
value={query.categoryId?.toString()}
onValueChange={(value) => setQuery({ ...query, categoryId: value ? Number(value) : undefined, pageNum: 0 })}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="全部分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""></SelectItem>
{categories.map((cat) => (
<SelectItem key={cat.id} value={cat.id.toString()}>
{cat.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={query.status || ''}
onValueChange={(value) => setQuery({ ...query, status: value as JobStatus || undefined, pageNum: 0 })}
>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="全部状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value=""></SelectItem>
<SelectItem value="ENABLED"></SelectItem>
<SelectItem value="DISABLED"></SelectItem>
<SelectItem value="PAUSED"></SelectItem>
</SelectContent>
</Select>
<Button onClick={handleSearch}>
<Search className="h-4 w-4 mr-2" />
</Button>
<Button variant="outline" onClick={handleReset}>
</Button>
</div>
{/* 任务列表 */}
<div className="rounded-md border">
<Table minWidth="1600px">
<TableHeader>
<TableRow>
<TableHead width="180px"></TableHead>
<TableHead width="120px"></TableHead>
<TableHead width="150px">Cron表达式</TableHead>
<TableHead width="120px">Bean名称</TableHead>
<TableHead width="120px"></TableHead>
<TableHead width="100px"></TableHead>
<TableHead width="160px"></TableHead>
<TableHead width="160px"></TableHead>
<TableHead width="100px"></TableHead>
<TableHead width="90px"></TableHead>
<TableHead width="240px" sticky></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={11} className="h-24 text-center">
<div className="flex items-center justify-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-muted-foreground">...</span>
</div>
</TableCell>
</TableRow>
) : data && data.content.length > 0 ? (
data.content.map((record) => (
<TableRow key={record.id}>
<TableCell className="font-medium">{record.jobName}</TableCell>
<TableCell>{record.category?.name || '-'}</TableCell>
<TableCell>
<code className="text-xs bg-muted px-1 py-0.5 rounded">{record.cronExpression}</code>
</TableCell>
<TableCell>
<code className="text-xs">{record.beanName}</code>
</TableCell>
<TableCell>
<code className="text-xs">{record.methodName}</code>
</TableCell>
<TableCell>{getStatusBadge(record.status)}</TableCell>
<TableCell className="text-xs text-muted-foreground">
{record.lastExecuteTime ? dayjs(record.lastExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</TableCell>
<TableCell className="text-xs text-muted-foreground">
{record.nextExecuteTime ? dayjs(record.nextExecuteTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</TableCell>
<TableCell className="text-center">
<Badge variant="outline">{record.executeCount || 0}</Badge>
</TableCell>
<TableCell>
{record.executeCount && record.executeCount > 0 ? (
<Badge variant={
(record.successCount! / record.executeCount) >= 0.9 ? 'default' :
(record.successCount! / record.executeCount) >= 0.7 ? 'outline' : 'destructive'
}>
{((record.successCount! / record.executeCount) * 100).toFixed(1)}%
</Badge>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell sticky width="240px">
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleExecute(record)}
title="立即执行"
>
<PlayCircle className="h-4 w-4" />
</Button>
{record.status === 'ENABLED' && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handlePause(record)}
title="暂停"
>
<Pause className="h-4 w-4" />
</Button>
)}
{record.status === 'PAUSED' && (
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleResume(record)}
title="恢复"
>
<Play className="h-4 w-4" />
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => handleViewLog(record)}
title="查看日志"
>
<FileText className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => toast({ title: '功能开发中' })}
title="编辑"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive"
onClick={() => toast({ title: '功能开发中' })}
title="删除"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={11} className="h-24 text-center text-muted-foreground">
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{data && data.content.length > 0 && (
<div className="mt-4">
<DataTablePagination
pageIndex={query.pageNum || 0}
pageSize={query.pageSize || DEFAULT_PAGE_SIZE}
pageCount={data.totalPages}
onPageChange={(pageIndex) => setQuery({ ...query, pageNum: pageIndex })}
/>
</div>
)}
</CardContent>
</Card>
{/* 分类管理对话框 */}
<CategoryManageDialog
open={categoryDialogOpen}
onOpenChange={setCategoryDialogOpen}
onSuccess={loadCategories}
/>
{/* 任务日志对话框 */}
<JobLogDialog
open={logDialogOpen}
onOpenChange={setLogDialogOpen}
job={selectedJob}
/>
</div>
);
};
export default ScheduleJobList;

View File

@ -0,0 +1,119 @@
import request from '@/utils/request';
import type { Page } from '@/types/base';
import type {
JobCategoryResponse,
JobCategoryRequest,
JobCategoryQuery,
ScheduleJobResponse,
ScheduleJobRequest,
ScheduleJobQuery,
ScheduleJobLogResponse,
ScheduleJobLogQuery,
} from './types';
const JOB_CATEGORY_URL = '/api/v1/schedule/job-categories';
const SCHEDULE_JOB_URL = '/api/v1/schedule/jobs';
const JOB_LOG_URL = '/api/v1/schedule/job-logs';
// ==================== 任务分类 ====================
/**
*
*/
export const getJobCategoryList = (params?: JobCategoryQuery) =>
request.get<JobCategoryResponse[]>(`${JOB_CATEGORY_URL}/list`, { params });
/**
*
*/
export const getJobCategories = (params?: JobCategoryQuery) =>
request.get<Page<JobCategoryResponse>>(`${JOB_CATEGORY_URL}/page`, { params });
/**
*
*/
export const createJobCategory = (data: JobCategoryRequest) =>
request.post<JobCategoryResponse>(JOB_CATEGORY_URL, data);
/**
*
*/
export const updateJobCategory = (id: number, data: JobCategoryRequest) =>
request.put<JobCategoryResponse>(`${JOB_CATEGORY_URL}/${id}`, data);
/**
*
*/
export const deleteJobCategory = (id: number) =>
request.delete(`${JOB_CATEGORY_URL}/${id}`);
/**
*
*/
export const batchDeleteJobCategories = (ids: number[]) =>
request.post(`${JOB_CATEGORY_URL}/batch-delete`, { ids });
// ==================== 定时任务 ====================
/**
*
*/
export const getScheduleJobs = (params?: ScheduleJobQuery) =>
request.get<Page<ScheduleJobResponse>>(`${SCHEDULE_JOB_URL}/page`, { params });
/**
*
*/
export const getScheduleJob = (id: number) =>
request.get<ScheduleJobResponse>(`${SCHEDULE_JOB_URL}/${id}`);
/**
*
*/
export const createScheduleJob = (data: ScheduleJobRequest) =>
request.post<ScheduleJobResponse>(SCHEDULE_JOB_URL, data);
/**
*
*/
export const updateScheduleJob = (id: number, data: ScheduleJobRequest) =>
request.put<ScheduleJobResponse>(`${SCHEDULE_JOB_URL}/${id}`, data);
/**
*
*/
export const deleteScheduleJob = (id: number) =>
request.delete(`${SCHEDULE_JOB_URL}/${id}`);
/**
*
*/
export const batchDeleteScheduleJobs = (ids: number[]) =>
request.post(`${SCHEDULE_JOB_URL}/batch-delete`, { ids });
/**
*
*/
export const executeJobNow = (id: number) =>
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/execute`);
/**
*
*/
export const pauseJob = (id: number) =>
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/pause`);
/**
*
*/
export const resumeJob = (id: number) =>
request.post<void>(`${SCHEDULE_JOB_URL}/${id}/resume`);
// ==================== 任务日志 ====================
/**
*
*/
export const getJobLogs = (params?: ScheduleJobLogQuery) =>
request.get<Page<ScheduleJobLogResponse>>(`${JOB_LOG_URL}/page`, { params });

View File

@ -0,0 +1,150 @@
import type { BaseResponse, BaseRequest, BaseQuery } from '@/types/base';
/**
*
*/
export interface JobCategoryResponse extends BaseResponse {
code: string;
name: string;
description?: string;
icon?: string;
color?: string;
sort?: number;
}
/**
*
*/
export interface JobCategoryRequest extends BaseRequest {
code: string;
name: string;
description?: string;
icon?: string;
color?: string;
enabled?: boolean;
sort?: number;
}
/**
*
*/
export interface JobCategoryQuery extends BaseQuery {
name?: string;
enabled?: boolean;
}
/**
*
*/
export type JobStatus = 'ENABLED' | 'DISABLED' | 'PAUSED';
/**
*
*/
export interface ScheduleJobResponse extends BaseResponse {
jobName: string;
jobDescription?: string;
categoryId: number;
category?: JobCategoryResponse;
// 执行配置
beanName: string;
methodName: string;
formDefinitionId?: number;
methodParams?: string;
// 调度配置
cronExpression: string;
status: JobStatus;
concurrent?: boolean;
// 统计信息
lastExecuteTime?: string;
nextExecuteTime?: string;
executeCount?: number;
successCount?: number;
failCount?: number;
// 高级配置
timeoutSeconds?: number;
retryCount?: number;
alertEmail?: string;
}
/**
*
*/
export interface ScheduleJobRequest extends BaseRequest {
jobName: string;
jobDescription?: string;
categoryId: number;
// 执行配置
beanName: string;
methodName: string;
formDefinitionId?: number;
methodParams?: string;
// 调度配置
cronExpression: string;
status: JobStatus;
concurrent?: boolean;
// 高级配置
timeoutSeconds?: number;
retryCount?: number;
alertEmail?: string;
}
/**
*
*/
export interface ScheduleJobQuery extends BaseQuery {
jobName?: string;
categoryId?: number;
status?: JobStatus;
}
/**
*
*/
export type LogStatus = 'SUCCESS' | 'FAIL' | 'TIMEOUT';
/**
*
*/
export interface ScheduleJobLogResponse extends BaseResponse {
jobId: number;
jobName: string;
// 执行信息
beanName: string;
methodName: string;
methodParams?: string;
// 时间统计
executeTime: string;
finishTime?: string;
duration?: number;
// 状态信息
status: LogStatus;
resultMessage?: string;
exceptionInfo?: string;
// 服务器信息
serverIp?: string;
serverHost?: string;
}
/**
*
*/
export interface ScheduleJobLogQuery extends BaseQuery {
jobId?: number;
jobName?: string;
status?: LogStatus;
executeTimeStart?: string;
executeTimeEnd?: string;
}

View File

@ -1,653 +0,0 @@
import React, {useState, useEffect} from 'react';
import {PageContainer} from '@ant-design/pro-components';
import {Button, Card, Form, Input, InputNumber, Select, Switch, Tabs, Row, Col, message, ColorPicker} from 'antd';
import type {NodeDesignDataResponse} from './types';
import * as service from './service';
import {useParams} from 'react-router-dom';
// Tab 配置
const TAB_CONFIG = [
{key: 'panel', label: '属性(预览)', schemaKey: 'panelVariablesSchema', readonly: true},
{key: 'local', label: '环境变量(预览)', schemaKey: 'localVariablesSchema', readonly: true},
{key: 'form', label: '表单(预览)', schemaKey: 'formVariablesSchema', readonly: true},
{key: 'ui', label: 'UI配置', schemaKey: 'uiVariables', readonly: false}
];
// 渲染具体的表单控件
const renderField = (schema: any, fieldPath: string) => {
const commonProps = {
placeholder: `请输入${schema.title}`,
};
// 颜色字段路径列表
const colorFields = [
'ports.groups.in.attrs.circle.fill',
'ports.groups.in.attrs.circle.stroke',
'ports.groups.out.attrs.circle.fill',
'ports.groups.out.attrs.circle.stroke',
'style.fill',
'style.stroke',
'style.iconColor'
];
// 检查是否是颜色字段
if (colorFields.some(path => fieldPath.endsWith(path))) {
return <ColorPicker showText/>;
}
if (schema.enum) {
return (
<Select
{...commonProps}
options={schema.enum.map((value: string, index: number) => ({
label: schema.enumNames?.[index] || value,
value
}))}
/>
);
}
switch (schema.type) {
case 'string':
return <Input {...commonProps} />;
case 'integer':
case 'number':
return <InputNumber {...commonProps} style={{width: '100%'}}/>;
case 'boolean':
return <Switch/>;
default:
return <Input {...commonProps} />;
}
};
// 表单渲染器组件
const FormRenderer: React.FC<{
schema: any;
path?: string;
readOnly?: boolean;
}> = ({schema, path = '', readOnly = false}) => {
if (!schema || !schema.properties) return null;
const renderPortConfig = (portSchema: any, portPath: string) => {
if (!portSchema || !portSchema.properties) return null;
const {position, attrs} = portSchema.properties;
return (
<>
{/* 先渲染端口位置 */}
<Form.Item
name={`${portPath}.position`}
label={
<span style={{fontSize: '14px'}}>
{position.title}
</span>
}
tooltip={position.description}
required={portSchema.required?.includes('position')}
initialValue={'default' in position ? position.default : undefined}
style={{marginBottom: 16}}
>
{readOnly ? (
<span style={{color: '#666'}}>{position.default || '-'}</span>
) : (
renderField(position, `${portPath}.position`)
)}
</Form.Item>
{/* 再渲染端口属性 */}
{attrs && (
<Card
title={attrs.title}
size="small"
style={{
marginBottom: 16,
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
borderRadius: '8px',
border: '1px solid #f0f0f0'
}}
styles={{
body: {padding: '16px'}
}}
>
<FormRenderer
schema={attrs}
path={`${portPath}.attrs`}
readOnly={readOnly}
/>
</Card>
)}
</>
);
};
return (
<div style={{padding: '8px 0'}}>
{Object.entries(schema.properties).map(([key, value]: [string, any]) => {
const fieldPath = path ? `${path}.${key}` : key;
if (value.type === 'object') {
// 特殊处理端口组配置
if (key === 'groups' && path.endsWith('ports')) {
const inPort = value.properties?.in;
const outPort = value.properties?.out;
return (
<Row key={fieldPath} gutter={16}>
<Col span={12}>
<Card
title={inPort.title}
size="small"
style={{
marginBottom: 16,
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
borderRadius: '8px',
border: '1px solid #f0f0f0'
}}
styles={{
body: {padding: '16px'}
}}
>
{renderPortConfig(inPort, `${fieldPath}.in`)}
</Card>
</Col>
<Col span={12}>
<Card
title={outPort.title}
size="small"
style={{
marginBottom: 16,
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
borderRadius: '8px',
border: '1px solid #f0f0f0'
}}
styles={{
body: {padding: '16px'}
}}
>
{renderPortConfig(outPort, `${fieldPath}.out`)}
</Card>
</Col>
</Row>
);
}
return (
<Card
key={fieldPath}
title={
<span style={{fontSize: '14px', fontWeight: 500}}>
{value.title}
</span>
}
size="small"
style={{
marginBottom: 16,
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
borderRadius: '8px',
border: '1px solid #f0f0f0'
}}
styles={{
body: {padding: '16px'}
}}
>
<FormRenderer
schema={value}
path={fieldPath}
readOnly={readOnly}
/>
</Card>
);
}
return (
<Form.Item
key={fieldPath}
name={fieldPath}
label={
<span style={{fontSize: '14px'}}>
{value.title}
</span>
}
tooltip={value.description}
required={schema.required?.includes(key)}
initialValue={'default' in value ? value.default : undefined}
style={{marginBottom: 16}}
>
{readOnly ? (
<span style={{color: '#666'}}>{value.default || '-'}</span>
) : (
renderField(value, fieldPath)
)}
</Form.Item>
);
})}
</div>
);
};
const NodeDesignForm: React.FC = () => {
const [nodeDefinitionsDefined, setNodeDefinitionsDefined] = useState<NodeDesignDataResponse[]>([]);
const [loading, setLoading] = useState(false);
const [selectedNode, setSelectedNode] = useState<NodeDesignDataResponse | null>(null);
const [activeTab, setActiveTab] = useState<string>('panel');
const [form] = Form.useForm();
const [isEdit, setIsEdit] = useState(false);
const [editData, setEditData] = useState<NodeDesignDataResponse | null>(null);
// 从路由参数获取 id
const { id } = useParams();
// 加载节点定义数据
useEffect(() => {
const loadNodeDefinitions = async () => {
try {
setLoading(true);
const data = await service.getNodeDefinitionsDefined();
setNodeDefinitionsDefined(data);
// 自动选中第一个节点
if (data && data.length > 0) {
handleNodeSelect(data[0]);
}
} catch (error) {
console.error('加载节点定义失败:', error);
message.error('加载节点定义失败');
} finally {
setLoading(false);
}
};
loadNodeDefinitions();
}, []);
// 如果有 id加载节点详情
useEffect(() => {
const loadNodeDetail = async () => {
if (!id) return;
try {
setLoading(true);
const data = await service.getNodeDefinition(id);
if (data) {
setIsEdit(true);
setEditData(data);
// 设置基本信息表单值
form.setFieldsValue({
'base.nodeType': data.nodeType,
'base.nodeCode': data.nodeCode,
'base.nodeName': data.nodeName,
'base.category': data.category,
'base.description': data.description
});
}
} catch (error) {
console.error('加载节点详情失败:', error);
message.error('加载节点详情失败');
} finally {
setLoading(false);
}
};
loadNodeDetail();
}, [id, form]);
// 获取当前节点可用的 Tab 列表
const getAvailableTabs = (node: NodeDesignDataResponse | null) => {
if (!node) return [];
return TAB_CONFIG.filter(tab => {
const value = node[tab.schemaKey as keyof NodeDesignDataResponse];
return value !== null && value !== undefined;
});
};
// 处理节点选择
const handleNodeSelect = (node: NodeDesignDataResponse) => {
console.log('选择节点:', node);
console.log('当前编辑状态:', isEdit);
console.log('当前编辑数据:', editData);
setSelectedNode(node);
// 更新表单数据
form.setFieldsValue({
'base.nodeType': node.nodeType,
'base.nodeCode': node.nodeCode,
'base.nodeName': node.nodeName,
'base.category': node.category,
'base.description': node.description
});
};
// 处理 Tab 切换
const handleTabChange = (key: string) => {
setActiveTab(key);
// 不需要重置表单
};
// 处理保存
const handleSave = async () => {
try {
const values = await form.validateFields();
console.log('Form values:', values);
// 提取基本信息字段
const baseFields = {
nodeType: values['base.nodeType'],
nodeCode: values['base.nodeCode'],
nodeName: values['base.nodeName'],
category: values['base.category'],
description: values['base.description']
};
// 移除基本信息字段,处理 UI 配置
const uiValues = {...values};
delete uiValues['base.nodeType'];
delete uiValues['base.nodeCode'];
delete uiValues['base.nodeName'];
delete uiValues['base.category'];
delete uiValues['base.description'];
// 处理颜色值转换为十六进制
const processColorValue = (value: any): any => {
if (!value || typeof value !== 'object') return value;
// 如果是 ColorPicker 的值,转换为十六进制
if (value.metaColor) {
const { r, g, b } = value.metaColor;
const toHex = (n: number): string => {
const hex = n.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// 如果是普通对象,递归处理
if (typeof value === 'object') {
const result: any = {};
Object.keys(value).forEach(key => {
result[key] = processColorValue(value[key]);
});
return result;
}
return value;
};
// 将扁平的键值对转换为嵌套对象
const convertToNestedObject = (flatObj: any) => {
const result: any = {};
Object.keys(flatObj).forEach(key => {
const parts = key.split('.');
let current = result;
for (let i = 0; i < parts.length - 1; i++) {
current[parts[i]] = current[parts[i]] || {};
current = current[parts[i]];
}
// 处理颜色值
const value = processColorValue(flatObj[key]);
current[parts[parts.length - 1]] = value;
});
return result;
};
const saveData = {
...selectedNode,
...baseFields,
uiVariables: convertToNestedObject(uiValues)
};
console.log('Save data:', saveData);
// 根据是否是编辑模式调用不同的接口
if (isEdit && editData?.id) {
await service.updateNodeDefinition(editData.id, saveData);
message.success('更新成功');
} else {
await service.saveNodeDefinition(saveData);
message.success('保存成功');
}
} catch (error) {
console.error('保存失败:', error);
message.error('保存失败');
}
};
// 获取当前 schema
const getCurrentSchema = () => {
if (!selectedNode) return null;
const currentTab = TAB_CONFIG.find(tab => tab.key === activeTab);
if (!currentTab) return null;
const schema = selectedNode[currentTab.schemaKey as keyof NodeDesignDataResponse];
console.log('当前 schema:', schema);
console.log('是否编辑模式:', isEdit);
console.log('编辑数据:', editData);
// 如果是编辑模式且有保存的数据,合并到 schema
if (isEdit && editData?.uiVariables) {
console.log('开始合并 UI 配置数据');
// 处理颜色值
const processColorValue = (value: any): string => {
if (!value || typeof value !== 'object') return value;
// 如果是颜色对象结构
if (value.metaColor) {
const { r, g, b } = value.metaColor;
// 转换为十六进制
const toHex = (n: number): string => {
const hex = n.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
return value;
};
const mergeUiVariables = (schemaObj: any, uiData: any, parentPath = ''): any => {
if (!schemaObj || typeof schemaObj !== 'object') return schemaObj;
console.log('正在处理 schema:', schemaObj);
console.log('UI 数据:', uiData);
console.log('当前路径:', parentPath);
const result = {...schemaObj};
// 颜色字段路径列表
const colorFields = [
'ports.groups.in.attrs.circle.fill',
'ports.groups.in.attrs.circle.stroke',
'ports.groups.out.attrs.circle.fill',
'ports.groups.out.attrs.circle.stroke',
'style.fill',
'style.stroke',
'style.iconColor'
];
if (result.properties) {
Object.keys(result.properties).forEach(key => {
const currentPath = parentPath ? `${parentPath}.${key}` : key;
console.log('处理属性:', currentPath);
// 处理嵌套对象
if (result.properties[key].type === 'object') {
const nestedValue = currentPath.split('.').reduce((obj, key) => obj?.[key], uiData);
console.log('嵌套对象的值:', nestedValue);
if (nestedValue !== undefined) {
result.properties[key].default = nestedValue;
}
}
// 处理基本类型
else {
const value = currentPath.split('.').reduce((obj, key) => obj?.[key], uiData);
console.log('属性值:', value);
if (value !== undefined) {
// 如果是颜色字段,处理颜色值
if (colorFields.some(field => currentPath.endsWith(field))) {
result.properties[key].default = processColorValue(value);
} else {
result.properties[key].default = value;
}
}
}
// 递归处理嵌套属性
result.properties[key] = mergeUiVariables(
result.properties[key],
uiData,
currentPath
);
});
}
if (result.items) {
result.items = mergeUiVariables(result.items, uiData, parentPath);
}
return result;
};
const mergedSchema = mergeUiVariables(schema, editData.uiVariables);
console.log('合并后的 schema:', mergedSchema);
return mergedSchema;
}
console.log('使用原始 schema');
return schema;
};
return (
<PageContainer
header={{
title: '节点设计',
extra: [
<Button
key="save"
type="primary"
onClick={handleSave}
>
</Button>
],
}}
>
<div style={{display: 'flex', gap: '24px', padding: '24px'}}>
<Tabs
tabPosition="left"
type="card"
activeKey={isEdit && editData?.nodeType ?
nodeDefinitionsDefined.find(n => n.nodeType === editData.nodeType)?.nodeCode || selectedNode?.nodeCode :
selectedNode?.nodeCode || ''
}
onChange={(key) => {
const node = nodeDefinitionsDefined.find(n => n.nodeCode === key);
if (node) {
handleNodeSelect(node);
}
}}
items={nodeDefinitionsDefined.map(node => ({
key: node.nodeCode,
label: (
<div style={{padding: '4px 0'}}>
<div style={{fontSize: '14px', fontWeight: 500}}>
{node.nodeName}
</div>
<div style={{
fontSize: '12px',
color: '#666',
marginTop: '4px'
}}>
{node.nodeCode}
</div>
</div>
),
disabled: isEdit && editData?.nodeType && node.nodeType !== editData.nodeType
}))}
/>
<Card
style={{
flex: 1,
}}
>
<Form
form={form}
layout="vertical"
key={`${selectedNode?.nodeCode}-${activeTab}`}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="base.nodeType"
label="节点类型"
rules={[{required: true}]}
>
<Input disabled/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="base.nodeCode"
label="节点编码"
rules={[{required: true}]}
>
<Input disabled/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="base.nodeName"
label="节点名称"
rules={[{required: true}]}
>
<Input disabled/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="base.category"
label="节点类别"
>
<Input disabled/>
</Form.Item>
</Col>
</Row>
<Form.Item
name="base.description"
label="节点描述"
>
<Input.TextArea rows={4}/>
</Form.Item>
<Tabs
activeKey={activeTab}
onChange={handleTabChange}
items={getAvailableTabs(selectedNode).map(tab => ({
key: tab.key,
label: (
<span style={{fontSize: '14px'}}>
{tab.label}
</span>
),
children: tab.key === activeTab ? (
<div style={{
padding: '16px 0',
minHeight: '400px'
}}>
<FormRenderer
schema={getCurrentSchema()}
readOnly={tab.readonly}
/>
</div>
) : null
}))}
/>
</Form>
</Card>
</div>
</PageContainer>
);
};
export default NodeDesignForm;

View File

@ -1,372 +0,0 @@
import React, {useState, useEffect} from 'react';
import {useNavigate} from 'react-router-dom';
import {PageContainer} from '@/components/ui/page-container';
import {PlusOutlined, DeleteOutlined} from '@ant-design/icons';
import {NodeTypeEnum, type NodeDesignDataResponse} from './types';
import * as service from './service';
import {
Table,
TableHeader,
TableBody,
TableHead,
TableRow,
TableCell,
} from "@/components/ui/table";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {Button} from "@/components/ui/button";
import {Badge} from "@/components/ui/badge";
import {useToast} from "@/components/ui/use-toast";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {DataTablePagination} from "@/components/ui/pagination";
// 节点类型标签样式映射
const nodeTypeStyles = {
[NodeTypeEnum.START_EVENT]: {
variant: 'success' as const,
label: '开始事件'
},
[NodeTypeEnum.END_EVENT]: {
variant: 'destructive' as const,
label: '结束事件'
},
[NodeTypeEnum.SCRIPT_TASK]: {
variant: 'default' as const,
label: '脚本任务'
},
};
interface Column {
accessorKey?: keyof NodeDesignDataResponse;
id?: string;
header: string;
size: number;
cell?: (props: { row: { original: NodeDesignDataResponse } }) => React.ReactNode;
}
const NodeDesignList: React.FC = () => {
const navigate = useNavigate();
const [detailVisible, setDetailVisible] = useState(false);
const [currentNode, setCurrentNode] = useState<NodeDesignDataResponse>();
const [list, setList] = useState<NodeDesignDataResponse[]>([]);
const [setLoading] = useState(false);
const [pagination, setPagination] = useState({
pageNum: 1,
pageSize: 10,
totalElements: 0,
});
const {toast} = useToast();
const loadData = async () => {
setLoading(true);
try {
const response = await service.getNodeDefinitions({
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
});
setList(response.content || []);
setPagination({
...pagination,
totalElements: response.totalElements,
});
} catch (error) {
toast({
variant: "destructive",
title: "获取节点列表失败",
duration: 3000,
});
} finally {
setLoading(false);
}
};
const handlePageChange = (page: number) => {
setPagination({
...pagination,
pageNum: page,
});
};
const handleDelete = async (id: number) => {
try {
await service.deleteNodeDefinition(id);
toast({
title: "删除成功",
duration: 3000,
});
loadData();
} catch (error) {
toast({
variant: "destructive",
title: "删除失败",
duration: 3000,
});
}
};
useEffect(() => {
loadData();
}, [pagination.pageNum, pagination.pageSize]);
const columns: Column[] = [
{
accessorKey: 'nodeCode',
header: '节点编码',
size: 180,
cell: ({row}) => (
<div className="font-medium">{row.original.nodeCode}</div>
),
},
{
accessorKey: 'nodeName',
header: '节点名称',
size: 150,
},
{
accessorKey: 'nodeType',
header: '节点类型',
size: 120,
cell: ({row}) => {
const style = nodeTypeStyles[row.original.nodeType as NodeTypeEnum] || {
variant: 'secondary' as const,
label: row.original.nodeType || '未知类型'
};
return (
<Badge variant={style.variant}>
{style.label}
</Badge>
);
},
},
{
accessorKey: 'panelVariablesSchema',
header: '面板变量',
size: 100,
cell: ({row}) => (
<Badge variant={row.original.panelVariablesSchema ? "outline" : "secondary"}>
{row.original.panelVariablesSchema ? '已配置' : '未配置'}
</Badge>
),
},
{
accessorKey: 'localVariablesSchema',
header: '本地变量',
size: 100,
cell: ({row}) => (
<Badge variant={row.original.localVariablesSchema ? "outline" : "secondary"}>
{row.original.localVariablesSchema ? '已配置' : '未配置'}
</Badge>
),
},
{
accessorKey: 'formVariablesSchema',
header: '表单变量',
size: 100,
cell: ({row}) => (
<Badge variant={row.original.formVariablesSchema ? "outline" : "secondary"}>
{row.original.formVariablesSchema ? '已配置' : '未配置'}
</Badge>
),
},
{
id: 'actions',
header: '操作',
size: 180,
cell: ({row}) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => {
setCurrentNode(row.original);
setDetailVisible(true);
}}
>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => navigate(`/workflow/node-design/design/${row.original.id}`)}
>
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="sm"
className="text-destructive"
>
<DeleteOutlined className="mr-1"/>
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={() => handleDelete(row.original.id)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
),
},
];
return (
<PageContainer>
<div className="flex items-center justify-between">
<h2 className="text-3xl font-bold tracking-tight"></h2>
<Button onClick={() => navigate('/workflow/node-design/create')}>
<PlusOutlined className="mr-1"/>
</Button>
</div>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="rounded-md border">
<Table minWidth="930px">
<TableHeader>
<TableRow>
{columns.map((column) => (
<TableHead
key={column.accessorKey || column.id}
width={`${column.size}px`}
sticky={column.id === 'actions'}
>
{column.header}
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{list.map((item) => (
<TableRow key={item.id}>
{columns.map((column) => (
<TableCell
key={column.accessorKey || column.id}
width={`${column.size}px`}
sticky={column.id === 'actions'}
>
{column.cell
? column.cell({row: {original: item}})
: item[column.accessorKey!]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
<div className="flex justify-end border-t border-border bg-muted/40">
<DataTablePagination
pageIndex={pagination.pageNum}
pageSize={pagination.pageSize}
pageCount={Math.ceil(pagination.totalElements / pagination.pageSize)}
onPageChange={handlePageChange}
/>
</div>
</div>
</CardContent>
</Card>
{/* 节点详情弹窗 */}
<AlertDialog open={detailVisible} onOpenChange={setDetailVisible}>
<AlertDialogContent className="max-w-[800px]">
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="mt-4 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="mt-1">{currentNode?.nodeCode}</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="mt-1">{currentNode?.nodeName}</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="mt-1">
{currentNode?.nodeType && (
<Badge variant={(nodeTypeStyles[currentNode.nodeType as NodeTypeEnum] || {}).variant || 'secondary'}>
{(nodeTypeStyles[currentNode.nodeType as NodeTypeEnum] || {}).label || currentNode.nodeType}
</Badge>
)}
</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="mt-1">{currentNode?.category}</div>
</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="mt-1">{currentNode?.description || '-'}</div>
</div>
<div>
<div className="text-sm font-medium text-muted-foreground"></div>
<div className="mt-2 space-y-2">
<div className="flex items-center gap-2">
<span></span>
<Badge variant={currentNode?.panelVariablesSchema ? "outline" : "secondary"}>
{currentNode?.panelVariablesSchema ? '已配置' : '未配置'}
</Badge>
</div>
<div className="flex items-center gap-2">
<span></span>
<Badge variant={currentNode?.localVariablesSchema ? "outline" : "secondary"}>
{currentNode?.localVariablesSchema ? '已配置' : '未配置'}
</Badge>
</div>
<div className="flex items-center gap-2">
<span></span>
<Badge variant={currentNode?.formVariablesSchema ? "outline" : "secondary"}>
{currentNode?.formVariablesSchema ? '已配置' : '未配置'}
</Badge>
</div>
</div>
</div>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setDetailVisible(false)}></AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</PageContainer>
);
};
export default NodeDesignList;

View File

@ -1,33 +0,0 @@
// 节点设计相关服务
import request from '@/utils/request';
import type {NodeDesignQuery, NodeDefinitionResponse, NodeDesignDataResponse} from './types';
const BASE_URL = '/api/v1/workflow/node-definition';
// 获取节点设计列表
export const getNodeDefinitions = (params: NodeDesignQuery) =>
request.get<NodeDefinitionResponse>(`${BASE_URL}/page`, {params});
// 获取节点设计详情
export const getNodeDefinition = (id: number) =>
request.get<NodeDefinitionResponse>(`${BASE_URL}/${id}`);
// 获取已定义的节点类型配置
export const getNodeDefinitionsDefined = () =>
request.get<NodeDesignDataResponse[]>(`${BASE_URL}/defined`);
// 保存节点定义
export const saveNodeDefinition = (data: NodeDesignDataResponse) =>
request.post<void>(`${BASE_URL}`, data);
export const updateNodeDefinition = (id: number, data: NodeDesignDataResponse) =>
request.put<void>(`${BASE_URL}/${id}`, data);
/**
*
* @param id ID
*/
export const deleteNodeDefinition = (id: number) =>
request.delete<void>(`${BASE_URL}/${id}`);

View File

@ -1,111 +0,0 @@
// 节点设计相关类型定义
// 基础Schema接口
import {BaseQuery, BaseResponse} from "@/types/base";
export interface BaseSchema {
type: string;
properties: Record<string, any>;
required?: string[];
}
// 节点变量Schema
export interface NodeVariablesSchema extends BaseSchema {
title?: string;
description?: string;
}
// UI配置中的大小接口
export interface NodeSize {
width: number;
height: number;
}
// 端口样式配置
export interface PortStyle {
r: number;
fill: string;
stroke: string;
}
// 端口属性配置
export interface PortAttributes {
circle: PortStyle;
}
// 端口位置类型
export type PortPosition = 'left' | 'right' | 'top' | 'bottom';
// 端口配置
export interface PortConfig {
attrs: PortAttributes;
position: PortPosition;
}
// 端口组配置
export interface PortGroups {
in: PortConfig;
out: PortConfig;
}
// 节点形状类型
export type NodeShape = 'rect' | 'circle' | 'diamond';
// 节点样式配置
export interface NodeStyle {
fill: string;
icon: string;
stroke: string;
iconColor: string;
strokeWidth: number;
}
// UI变量配置
export interface UIVariables extends BaseSchema {
size: NodeSize;
ports: {
groups: PortGroups;
};
shape: NodeShape;
style: NodeStyle;
}
// 节点设计数据
export interface NodeDesignDataResponse extends BaseResponse {
nodeCode: string;
nodeName: string;
nodeType: string;
category: string;
description: string;
panelVariablesSchema: NodeVariablesSchema | null;
localVariablesSchema: NodeVariablesSchema | null;
formVariablesSchema: NodeVariablesSchema | null;
uiVariables: UIVariables;
}
// 节点类型枚举
export enum NodeTypeEnum {
START_EVENT = 'START_EVENT',
END_EVENT = 'END_EVENT',
SCRIPT_TASK = 'SCRIPT_TASK',
}
// 查询参数接口
export interface NodeDesignQuery extends BaseQuery {
nodeCode?: string;
nodeName?: string;
}
// 分页响应接口
export interface NodeDefinitionResponse extends BaseResponse {
nodeCode: string;
nodeName: string;
nodeType: NodeTypeEnum;
category: string;
description: string;
panelVariablesSchema: NodeVariablesSchema | null;
uiVariables: UIVariables | null;
localVariablesSchema: NodeVariablesSchema | null;
panelVariables?: Record<string, any>;
localVariables?: Record<string, any>;
}

View File

@ -35,8 +35,6 @@ const WorkflowDesign = lazy(() => import('../pages/Workflow/Design'));
const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance')); const WorkflowInstance = lazy(() => import('../pages/Workflow/Instance'));
const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor')); const WorkflowMonitor = lazy(() => import('../pages/Workflow/Monitor'));
const LogStreamPage = lazy(() => import('../pages/LogStream')); const LogStreamPage = lazy(() => import('../pages/LogStream'));
const NodeDesign = lazy(() => import('../pages/Workflow/NodeDesign'));
const NodeDesignForm = lazy(() => import('../pages/Workflow/NodeDesign/Design'));
const ApplicationList = lazy(() => import('../pages/Deploy/Application/List')); const ApplicationList = lazy(() => import('../pages/Deploy/Application/List'));
const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List')); const EnvironmentList = lazy(() => import('../pages/Deploy/Environment/List'));
const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List')); const DeploymentConfigList = lazy(() => import('../pages/Deploy/Deployment/List'));
@ -44,6 +42,7 @@ const JenkinsManagerList = lazy(() => import('../pages/Deploy/JenkinsManager/Lis
const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List')); const GitManagerList = lazy(() => import('../pages/Deploy/GitManager/List'));
const External = lazy(() => import('../pages/Deploy/External')); const External = lazy(() => import('../pages/Deploy/External'));
const TeamList = lazy(() => import('../pages/Deploy/Team/List')); const TeamList = lazy(() => import('../pages/Deploy/Team/List'));
const ScheduleJobList = lazy(() => import('../pages/Deploy/ScheduleJob/List'));
const FormDesigner = lazy(() => import('../pages/FormDesigner')); const FormDesigner = lazy(() => import('../pages/FormDesigner'));
const FormDefinitionList = lazy(() => import('../pages/Form/Definition')); const FormDefinitionList = lazy(() => import('../pages/Form/Definition'));
const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer')); const FormDefinitionDesigner = lazy(() => import('../pages/Form/Definition/Designer'));
@ -92,6 +91,10 @@ const router = createBrowserRouter([
{ {
path: 'deployment', path: 'deployment',
element: <Suspense fallback={<LoadingComponent/>}><DeploymentConfigList/></Suspense> element: <Suspense fallback={<LoadingComponent/>}><DeploymentConfigList/></Suspense>
},
{
path: 'schedule-jobs',
element: <Suspense fallback={<LoadingComponent/>}><ScheduleJobList/></Suspense>
} }
] ]
}, },
@ -234,35 +237,6 @@ const router = createBrowserRouter([
} }
] ]
}, },
{
path: 'node-design',
children: [
{
index: true,
element: (
<Suspense fallback={<LoadingComponent/>}>
<NodeDesign/>
</Suspense>
)
},
{
path: 'create',
element: (
<Suspense fallback={<LoadingComponent/>}>
<NodeDesignForm/>
</Suspense>
)
},
{
path: 'design/:id',
element: (
<Suspense fallback={<LoadingComponent/>}>
<NodeDesignForm/>
</Suspense>
)
}
]
},
{ {
path: 'monitor', path: 'monitor',
element: ( element: (