feat: 重构项目页面架构并实现流水线步骤拖拽排序功能
主要更新: - 重构项目页面结构:将原有项目页面拆分为 list 和 detail 两个子模块 - 新增项目详情页面,支持多标签页展示(流水线、部署记录) - 实现流水线管理功能:支持新建、编辑、复制、删除、启用/禁用 - 实现流水线步骤管理:支持添加、编辑、删除、启用/禁用步骤 - 新增流水线步骤拖拽排序功能:集成 @dnd-kit 实现拖拽重排 - 优化左右两栏布局:左侧流水线列表,右侧步骤详情 - 新增部署记录展示功能:左右两栏布局,支持选中切换 - 提取可复用组件:DeployRecordItem、PipelineStepItem - 添加表单验证和用户交互反馈 - 更新路由配置支持项目详情页面 技术改进: - 安装 @dnd-kit 相关依赖实现拖拽功能 - 优化 TypeScript 类型定义 - 改进组件化设计,提高代码复用性 - 增强用户体验和交互反馈
This commit is contained in:
Binary file not shown.
@@ -12,9 +12,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/web-react": "^2.66.4",
|
"@arco-design/web-react": "^2.66.4",
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^7.8.0",
|
"react-router": "^7.8.0",
|
||||||
"zustand": "^5.0.8"
|
"zustand": "^5.0.8"
|
||||||
},
|
},
|
||||||
@@ -25,8 +28,8 @@
|
|||||||
"@rsbuild/plugin-react": "^1.3.4",
|
"@rsbuild/plugin-react": "^1.3.4",
|
||||||
"@rsbuild/plugin-svgr": "^1.2.2",
|
"@rsbuild/plugin-svgr": "^1.2.2",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/react": "^19.1.9",
|
"@types/react": "^18.3.24",
|
||||||
"@types/react-dom": "^19.1.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5.9.2"
|
"typescript": "^5.9.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Route, Routes, Navigate } from 'react-router';
|
import { Route, Routes, Navigate } from 'react-router';
|
||||||
import Home from '@pages/home';
|
import Home from '@pages/home';
|
||||||
import Login from '@pages/login';
|
import Login from '@pages/login';
|
||||||
import Project from '@pages/project';
|
import ProjectList from '@pages/project/list';
|
||||||
|
import ProjectDetail from '@pages/project/detail';
|
||||||
import Env from '@pages/env';
|
import Env from '@pages/env';
|
||||||
|
|
||||||
import '@styles/index.css';
|
import '@styles/index.css';
|
||||||
@@ -10,7 +11,8 @@ const App = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />}>
|
<Route path="/" element={<Home />}>
|
||||||
<Route index element={<Navigate to="project" replace />} />
|
<Route index element={<Navigate to="project" replace />} />
|
||||||
<Route path="project" element={<Project />} />
|
<Route path="project" element={<ProjectList />} />
|
||||||
|
<Route path="project/:id" element={<ProjectDetail />} />
|
||||||
<Route path="env" element={<Env />} />
|
<Route path="env" element={<Env />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { List, Tag, Space } from '@arco-design/web-react';
|
||||||
|
|
||||||
|
// 部署记录类型定义
|
||||||
|
interface DeployRecord {
|
||||||
|
id: number;
|
||||||
|
branch: string;
|
||||||
|
env: string;
|
||||||
|
commit: string;
|
||||||
|
status: 'success' | 'running' | 'failed' | 'pending';
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeployRecordItemProps {
|
||||||
|
item: DeployRecord;
|
||||||
|
isSelected: boolean;
|
||||||
|
onSelect: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeployRecordItem({
|
||||||
|
item,
|
||||||
|
isSelected,
|
||||||
|
onSelect,
|
||||||
|
}: DeployRecordItemProps) {
|
||||||
|
// 状态标签渲染函数
|
||||||
|
const getStatusTag = (status: DeployRecord['status']) => {
|
||||||
|
const statusMap: Record<
|
||||||
|
DeployRecord['status'],
|
||||||
|
{ color: string; text: string }
|
||||||
|
> = {
|
||||||
|
success: { color: 'green', text: '成功' },
|
||||||
|
running: { color: 'blue', text: '运行中' },
|
||||||
|
failed: { color: 'red', text: '失败' },
|
||||||
|
pending: { color: 'orange', text: '等待中' },
|
||||||
|
};
|
||||||
|
const config = statusMap[status];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 环境标签渲染函数
|
||||||
|
const getEnvTag = (env: string) => {
|
||||||
|
const envMap: Record<string, { color: string; text: string }> = {
|
||||||
|
production: { color: 'red', text: '生产环境' },
|
||||||
|
staging: { color: 'orange', text: '预发布环境' },
|
||||||
|
development: { color: 'blue', text: '开发环境' },
|
||||||
|
};
|
||||||
|
const config = envMap[env] || { color: 'gray', text: env };
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
key={item.id}
|
||||||
|
className={`cursor-pointer transition-all duration-200 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-blue-50 border-l-4 border-blue-500'
|
||||||
|
: 'hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
onClick={() => onSelect(item.id)}
|
||||||
|
>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
className={`font-semibold ${
|
||||||
|
isSelected ? 'text-blue-600' : 'text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
#{item.id}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-600 text-sm font-mono bg-gray-100 px-2 py-1 rounded">
|
||||||
|
{item.commit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
<div className="mt-2">
|
||||||
|
<Space size="medium" wrap>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
分支:{' '}
|
||||||
|
<span className="font-medium text-gray-700">
|
||||||
|
{item.branch}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
环境: {getEnvTag(item.env)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
状态: {getStatusTag(item.status)}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
执行时间:{' '}
|
||||||
|
<span className="font-medium text-gray-700">
|
||||||
|
{item.createdAt}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DeployRecordItem;
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { Typography, Tag, Switch, Button } from '@arco-design/web-react';
|
||||||
|
import { IconDragArrow, IconEdit, IconDelete } from '@arco-design/web-react/icon';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
|
||||||
|
// 流水线步骤类型定义
|
||||||
|
interface PipelineStep {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
script: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PipelineStepItemProps {
|
||||||
|
step: PipelineStep;
|
||||||
|
index: number;
|
||||||
|
pipelineId: string;
|
||||||
|
onToggle: (pipelineId: string, stepId: string, enabled: boolean) => void;
|
||||||
|
onEdit: (pipelineId: string, step: PipelineStep) => void;
|
||||||
|
onDelete: (pipelineId: string, stepId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PipelineStepItem({
|
||||||
|
step,
|
||||||
|
index,
|
||||||
|
pipelineId,
|
||||||
|
onToggle,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: PipelineStepItemProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: step.id });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={`bg-gray-50 rounded-lg p-4 ${isDragging ? 'shadow-lg z-10' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="cursor-grab active:cursor-grabbing"
|
||||||
|
>
|
||||||
|
<IconDragArrow className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-sm font-medium">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<Typography.Title heading={6} className="!m-0">
|
||||||
|
{step.name}
|
||||||
|
</Typography.Title>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={step.enabled}
|
||||||
|
onChange={(enabled) => onToggle(pipelineId, step.id, enabled)}
|
||||||
|
/>
|
||||||
|
{!step.enabled && (
|
||||||
|
<Tag color="gray" size="small">
|
||||||
|
已禁用
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm">
|
||||||
|
<pre className="whitespace-pre-wrap break-words">{step.script}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<IconEdit />}
|
||||||
|
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md p-1 transition-all duration-200"
|
||||||
|
onClick={() => onEdit(pipelineId, step)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<IconDelete />}
|
||||||
|
className="text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-md p-1 transition-all duration-200"
|
||||||
|
onClick={() => onDelete(pipelineId, step.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PipelineStepItem;
|
||||||
854
apps/web/src/pages/project/detail/index.tsx
Normal file
854
apps/web/src/pages/project/detail/index.tsx
Normal file
@@ -0,0 +1,854 @@
|
|||||||
|
import {
|
||||||
|
Typography,
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
List,
|
||||||
|
Tag,
|
||||||
|
Space,
|
||||||
|
Input,
|
||||||
|
Card,
|
||||||
|
Switch,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Message,
|
||||||
|
Collapse,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
|
import type { Project } from '../types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
|
import { detailService } from './service';
|
||||||
|
import {
|
||||||
|
IconPlayArrow,
|
||||||
|
IconPlus,
|
||||||
|
IconEdit,
|
||||||
|
IconDelete,
|
||||||
|
IconMore,
|
||||||
|
IconCopy,
|
||||||
|
} from '@arco-design/web-react/icon';
|
||||||
|
import DeployRecordItem from './components/DeployRecordItem';
|
||||||
|
import PipelineStepItem from './components/PipelineStepItem';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
KeyboardSensor,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
sortableKeyboardCoordinates,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
arrayMove,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
|
||||||
|
// 部署记录类型定义
|
||||||
|
interface DeployRecord {
|
||||||
|
id: number;
|
||||||
|
branch: string;
|
||||||
|
env: string;
|
||||||
|
commit: string;
|
||||||
|
status: 'success' | 'running' | 'failed' | 'pending';
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流水线步骤类型定义
|
||||||
|
interface PipelineStep {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
script: string;
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 流水线类型定义
|
||||||
|
interface Pipeline {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
enabled: boolean;
|
||||||
|
steps: PipelineStep[];
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProjectDetailPage() {
|
||||||
|
const [detail, setDetail] = useState<Project | null>();
|
||||||
|
|
||||||
|
// 拖拽传感器配置
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const [selectedRecordId, setSelectedRecordId] = useState<number>(1);
|
||||||
|
const [pipelines, setPipelines] = useState<Pipeline[]>([
|
||||||
|
{
|
||||||
|
id: 'pipeline1',
|
||||||
|
name: '前端部署流水线',
|
||||||
|
description: '用于前端项目的构建和部署',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: '2024-09-07 10:00:00',
|
||||||
|
updatedAt: '2024-09-07 14:30:00',
|
||||||
|
steps: [
|
||||||
|
{ id: 'step1', name: '安装依赖', script: 'npm install', enabled: true },
|
||||||
|
{ id: 'step2', name: '运行测试', script: 'npm test', enabled: true },
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: '构建项目',
|
||||||
|
script: 'npm run build',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pipeline2',
|
||||||
|
name: 'Docker部署流水线',
|
||||||
|
description: '用于容器化部署的流水线',
|
||||||
|
enabled: true,
|
||||||
|
createdAt: '2024-09-06 16:20:00',
|
||||||
|
updatedAt: '2024-09-07 09:15:00',
|
||||||
|
steps: [
|
||||||
|
{ id: 'step1', name: '安装依赖', script: 'npm install', enabled: true },
|
||||||
|
{
|
||||||
|
id: 'step2',
|
||||||
|
name: '构建镜像',
|
||||||
|
script: 'docker build -t $PROJECT_NAME:$BUILD_NUMBER .',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'step3',
|
||||||
|
name: 'K8s部署',
|
||||||
|
script: 'kubectl apply -f deployment.yaml',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||||
|
const [selectedPipelineId, setSelectedPipelineId] = useState<string>(
|
||||||
|
pipelines.length > 0 ? pipelines[0].id : '',
|
||||||
|
);
|
||||||
|
const [editingStep, setEditingStep] = useState<PipelineStep | null>(null);
|
||||||
|
const [editingPipelineId, setEditingPipelineId] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [pipelineModalVisible, setPipelineModalVisible] = useState(false);
|
||||||
|
const [editingPipeline, setEditingPipeline] = useState<Pipeline | null>(null);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [pipelineForm] = Form.useForm();
|
||||||
|
const [deployRecords, setDeployRecords] = useState<DeployRecord[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
branch: 'main',
|
||||||
|
env: 'development',
|
||||||
|
commit: '1d1224ae1',
|
||||||
|
status: 'success',
|
||||||
|
createdAt: '2024-09-07 14:30:25',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
branch: 'develop',
|
||||||
|
env: 'staging',
|
||||||
|
commit: '2f4b5c8e9',
|
||||||
|
status: 'running',
|
||||||
|
createdAt: '2024-09-07 13:45:12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
branch: 'feature/user-auth',
|
||||||
|
env: 'development',
|
||||||
|
commit: '3a7d9f2b1',
|
||||||
|
status: 'failed',
|
||||||
|
createdAt: '2024-09-07 12:20:45',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
branch: 'main',
|
||||||
|
env: 'production',
|
||||||
|
commit: '4e8b6a5c3',
|
||||||
|
status: 'success',
|
||||||
|
createdAt: '2024-09-07 10:15:30',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { id } = useParams();
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
if (id) {
|
||||||
|
const project = await detailService.getProject(id);
|
||||||
|
setDetail(project);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 获取模拟的构建日志
|
||||||
|
const getBuildLogs = (recordId: number): string[] => {
|
||||||
|
const logs: Record<number, string[]> = {
|
||||||
|
1: [
|
||||||
|
'[2024-09-07 14:30:25] 开始构建...',
|
||||||
|
'[2024-09-07 14:30:26] 拉取代码: git clone https://github.com/user/repo.git',
|
||||||
|
'[2024-09-07 14:30:28] 切换分支: git checkout main',
|
||||||
|
'[2024-09-07 14:30:29] 安装依赖: npm install',
|
||||||
|
'[2024-09-07 14:31:15] 运行测试: npm test',
|
||||||
|
'[2024-09-07 14:31:30] ✅ 所有测试通过',
|
||||||
|
'[2024-09-07 14:31:31] 构建项目: npm run build',
|
||||||
|
'[2024-09-07 14:32:10] 构建镜像: docker build -t app:latest .',
|
||||||
|
'[2024-09-07 14:33:25] 推送镜像: docker push registry.com/app:latest',
|
||||||
|
'[2024-09-07 14:34:10] 部署到开发环境...',
|
||||||
|
'[2024-09-07 14:34:45] ✅ 部署成功',
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
'[2024-09-07 13:45:12] 开始构建...',
|
||||||
|
'[2024-09-07 13:45:13] 拉取代码: git clone https://github.com/user/repo.git',
|
||||||
|
'[2024-09-07 13:45:15] 切换分支: git checkout develop',
|
||||||
|
'[2024-09-07 13:45:16] 安装依赖: npm install',
|
||||||
|
'[2024-09-07 13:46:02] 运行测试: npm test',
|
||||||
|
'[2024-09-07 13:46:18] ✅ 所有测试通过',
|
||||||
|
'[2024-09-07 13:46:19] 构建项目: npm run build',
|
||||||
|
'[2024-09-07 13:47:05] 构建镜像: docker build -t app:develop .',
|
||||||
|
'[2024-09-07 13:48:20] 🔄 正在推送镜像...',
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
'[2024-09-07 12:20:45] 开始构建...',
|
||||||
|
'[2024-09-07 12:20:46] 拉取代码: git clone https://github.com/user/repo.git',
|
||||||
|
'[2024-09-07 12:20:48] 切换分支: git checkout feature/user-auth',
|
||||||
|
'[2024-09-07 12:20:49] 安装依赖: npm install',
|
||||||
|
'[2024-09-07 12:21:35] 运行测试: npm test',
|
||||||
|
'[2024-09-07 12:21:50] ❌ 测试失败',
|
||||||
|
'[2024-09-07 12:21:51] Error: Authentication test failed',
|
||||||
|
'[2024-09-07 12:21:51] ❌ 构建失败',
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
'[2024-09-07 10:15:30] 开始构建...',
|
||||||
|
'[2024-09-07 10:15:31] 拉取代码: git clone https://github.com/user/repo.git',
|
||||||
|
'[2024-09-07 10:15:33] 切换分支: git checkout main',
|
||||||
|
'[2024-09-07 10:15:34] 安装依赖: npm install',
|
||||||
|
'[2024-09-07 10:16:20] 运行测试: npm test',
|
||||||
|
'[2024-09-07 10:16:35] ✅ 所有测试通过',
|
||||||
|
'[2024-09-07 10:16:36] 构建项目: npm run build',
|
||||||
|
'[2024-09-07 10:17:22] 构建镜像: docker build -t app:v1.0.0 .',
|
||||||
|
'[2024-09-07 10:18:45] 推送镜像: docker push registry.com/app:v1.0.0',
|
||||||
|
'[2024-09-07 10:19:30] 部署到生产环境...',
|
||||||
|
'[2024-09-07 10:20:15] ✅ 部署成功',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
return logs[recordId] || ['暂无日志记录'];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加新流水线
|
||||||
|
const handleAddPipeline = () => {
|
||||||
|
setEditingPipeline(null);
|
||||||
|
pipelineForm.resetFields();
|
||||||
|
setPipelineModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑流水线
|
||||||
|
const handleEditPipeline = (pipeline: Pipeline) => {
|
||||||
|
setEditingPipeline(pipeline);
|
||||||
|
pipelineForm.setFieldsValue({
|
||||||
|
name: pipeline.name,
|
||||||
|
description: pipeline.description,
|
||||||
|
});
|
||||||
|
setPipelineModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除流水线
|
||||||
|
const handleDeletePipeline = (pipelineId: string) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content:
|
||||||
|
'确定要删除这个流水线吗?此操作不可撤销,将同时删除该流水线下的所有步骤。',
|
||||||
|
onOk: () => {
|
||||||
|
setPipelines((prev) => {
|
||||||
|
const newPipelines = prev.filter((pipeline) => pipeline.id !== pipelineId);
|
||||||
|
// 如果删除的是当前选中的流水线,选中第一个或清空选择
|
||||||
|
if (selectedPipelineId === pipelineId) {
|
||||||
|
setSelectedPipelineId(newPipelines.length > 0 ? newPipelines[0].id : '');
|
||||||
|
}
|
||||||
|
return newPipelines;
|
||||||
|
});
|
||||||
|
Message.success('流水线删除成功');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 复制流水线
|
||||||
|
const handleCopyPipeline = (pipeline: Pipeline) => {
|
||||||
|
const newPipeline: Pipeline = {
|
||||||
|
...pipeline,
|
||||||
|
id: `pipeline_${Date.now()}`,
|
||||||
|
name: `${pipeline.name} - 副本`,
|
||||||
|
createdAt: new Date().toLocaleString(),
|
||||||
|
updatedAt: new Date().toLocaleString(),
|
||||||
|
steps: pipeline.steps.map((step) => ({
|
||||||
|
...step,
|
||||||
|
id: `step_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
setPipelines((prev) => [...prev, newPipeline]);
|
||||||
|
// 自动选中新复制的流水线
|
||||||
|
setSelectedPipelineId(newPipeline.id);
|
||||||
|
Message.success('流水线复制成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换流水线启用状态
|
||||||
|
const handleTogglePipeline = (pipelineId: string, enabled: boolean) => {
|
||||||
|
setPipelines((prev) =>
|
||||||
|
prev.map((pipeline) =>
|
||||||
|
pipeline.id === pipelineId ? { ...pipeline, enabled } : pipeline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存流水线
|
||||||
|
const handleSavePipeline = async () => {
|
||||||
|
try {
|
||||||
|
const values = await pipelineForm.validate();
|
||||||
|
if (editingPipeline) {
|
||||||
|
setPipelines((prev) => [
|
||||||
|
...prev.map((pipeline) =>
|
||||||
|
pipeline.id === editingPipeline.id
|
||||||
|
? {
|
||||||
|
...pipeline,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
updatedAt: new Date().toLocaleString(),
|
||||||
|
}
|
||||||
|
: pipeline,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
Message.success('流水线更新成功');
|
||||||
|
} else {
|
||||||
|
const newPipeline: Pipeline = {
|
||||||
|
id: `pipeline_${Date.now()}`,
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
enabled: true,
|
||||||
|
steps: [],
|
||||||
|
createdAt: new Date().toLocaleString(),
|
||||||
|
updatedAt: new Date().toLocaleString(),
|
||||||
|
};
|
||||||
|
setPipelines((prev) => [...prev, newPipeline]);
|
||||||
|
// 自动选中新创建的流水线
|
||||||
|
setSelectedPipelineId(newPipeline.id);
|
||||||
|
Message.success('流水线创建成功');
|
||||||
|
}
|
||||||
|
setPipelineModalVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加新步骤
|
||||||
|
const handleAddStep = (pipelineId: string) => {
|
||||||
|
setEditingStep(null);
|
||||||
|
setEditingPipelineId(pipelineId);
|
||||||
|
form.resetFields();
|
||||||
|
setEditModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑步骤
|
||||||
|
const handleEditStep = (pipelineId: string, step: PipelineStep) => {
|
||||||
|
setEditingStep(step);
|
||||||
|
setEditingPipelineId(pipelineId);
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: step.name,
|
||||||
|
script: step.script,
|
||||||
|
});
|
||||||
|
setEditModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除步骤
|
||||||
|
const handleDeleteStep = (pipelineId: string, stepId: string) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除这个流水线步骤吗?此操作不可撤销。',
|
||||||
|
onOk: () => {
|
||||||
|
setPipelines((prev) =>
|
||||||
|
prev.map((pipeline) =>
|
||||||
|
pipeline.id === pipelineId
|
||||||
|
? {
|
||||||
|
...pipeline,
|
||||||
|
steps: pipeline.steps.filter((step) => step.id !== stepId),
|
||||||
|
}
|
||||||
|
: pipeline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Message.success('步骤删除成功');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换步骤启用状态
|
||||||
|
const handleToggleStep = (
|
||||||
|
pipelineId: string,
|
||||||
|
stepId: string,
|
||||||
|
enabled: boolean,
|
||||||
|
) => {
|
||||||
|
setPipelines((prev) =>
|
||||||
|
prev.map((pipeline) =>
|
||||||
|
pipeline.id === pipelineId
|
||||||
|
? {
|
||||||
|
...pipeline,
|
||||||
|
steps: pipeline.steps.map((step) =>
|
||||||
|
step.id === stepId ? { ...step, enabled } : step,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: pipeline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 拖拽结束处理
|
||||||
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (!over || active.id === over.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedPipelineId) {
|
||||||
|
setPipelines((prev) =>
|
||||||
|
prev.map((pipeline) => {
|
||||||
|
if (pipeline.id === selectedPipelineId) {
|
||||||
|
const oldIndex = pipeline.steps.findIndex((step) => step.id === active.id);
|
||||||
|
const newIndex = pipeline.steps.findIndex((step) => step.id === over.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pipeline,
|
||||||
|
steps: arrayMove(pipeline.steps, oldIndex, newIndex),
|
||||||
|
updatedAt: new Date().toLocaleString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return pipeline;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
Message.success('步骤顺序调整成功');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存步骤
|
||||||
|
const handleSaveStep = async () => {
|
||||||
|
try {
|
||||||
|
const values = await form.validate();
|
||||||
|
if (editingStep && editingPipelineId) {
|
||||||
|
setPipelines((prev) =>
|
||||||
|
prev.map((pipeline) =>
|
||||||
|
pipeline.id === editingPipelineId
|
||||||
|
? {
|
||||||
|
...pipeline,
|
||||||
|
steps: pipeline.steps.map((step) =>
|
||||||
|
step.id === editingStep.id
|
||||||
|
? { ...step, name: values.name, script: values.script }
|
||||||
|
: step,
|
||||||
|
),
|
||||||
|
updatedAt: new Date().toLocaleString(),
|
||||||
|
}
|
||||||
|
: pipeline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Message.success('步骤更新成功');
|
||||||
|
} else if (editingPipelineId) {
|
||||||
|
const newStep: PipelineStep = {
|
||||||
|
id: `step_${Date.now()}`,
|
||||||
|
name: values.name,
|
||||||
|
script: values.script,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
setPipelines((prev) =>
|
||||||
|
prev.map((pipeline) =>
|
||||||
|
pipeline.id === editingPipelineId
|
||||||
|
? {
|
||||||
|
...pipeline,
|
||||||
|
steps: [...pipeline.steps, newStep],
|
||||||
|
updatedAt: new Date().toLocaleString(),
|
||||||
|
}
|
||||||
|
: pipeline,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Message.success('步骤添加成功');
|
||||||
|
}
|
||||||
|
setEditModalVisible(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedRecord = deployRecords.find(
|
||||||
|
(record) => record.id === selectedRecordId,
|
||||||
|
);
|
||||||
|
const buildLogs = getBuildLogs(selectedRecordId);
|
||||||
|
|
||||||
|
// 简单的状态标签渲染函数(仅用于构建日志区域)
|
||||||
|
const renderStatusTag = (status: DeployRecord['status']) => {
|
||||||
|
const statusMap: Record<
|
||||||
|
DeployRecord['status'],
|
||||||
|
{ color: string; text: string }
|
||||||
|
> = {
|
||||||
|
success: { color: 'green', text: '成功' },
|
||||||
|
running: { color: 'blue', text: '运行中' },
|
||||||
|
failed: { color: 'red', text: '失败' },
|
||||||
|
pending: { color: 'orange', text: '等待中' },
|
||||||
|
};
|
||||||
|
const config = statusMap[status];
|
||||||
|
return <Tag color={config.color}>{config.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染部署记录项
|
||||||
|
const renderDeployRecordItem = (item: DeployRecord, index: number) => {
|
||||||
|
const isSelected = item.id === selectedRecordId;
|
||||||
|
return (
|
||||||
|
<DeployRecordItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelect={setSelectedRecordId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex flex-col h-full">
|
||||||
|
<div className="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Typography.Title heading={2} className="!m-0 !text-gray-900">
|
||||||
|
{detail?.name}
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Text type="secondary">自动化地部署项目</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Button type="primary" icon={<IconPlayArrow />}>
|
||||||
|
部署
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-6 rounded-lg shadow-md flex-1">
|
||||||
|
<Tabs type="line" size="large">
|
||||||
|
<Tabs.TabPane title="部署记录" key="deployRecords">
|
||||||
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
|
{/* 左侧部署记录列表 */}
|
||||||
|
<div className="col-span-2 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
共 {deployRecords.length} 条部署记录
|
||||||
|
</Typography.Text>
|
||||||
|
<Button size="small" type="outline">
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<List
|
||||||
|
className="bg-white rounded-lg border"
|
||||||
|
dataSource={deployRecords}
|
||||||
|
render={renderDeployRecordItem}
|
||||||
|
split={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧构建日志 */}
|
||||||
|
<div className="col-span-3 bg-white rounded-lg border h-full overflow-hidden">
|
||||||
|
<div className="p-4 border-b bg-gray-50">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Typography.Title heading={5} className="!m-0">
|
||||||
|
构建日志 #{selectedRecordId}
|
||||||
|
</Typography.Title>
|
||||||
|
{selectedRecord && (
|
||||||
|
<Typography.Text type="secondary" className="text-sm">
|
||||||
|
{selectedRecord.branch} · {selectedRecord.env} ·{' '}
|
||||||
|
{selectedRecord.createdAt}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedRecord && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{renderStatusTag(selectedRecord.status)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 h-full overflow-y-auto">
|
||||||
|
<div className="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-full overflow-y-auto">
|
||||||
|
{buildLogs.map((log: string, index: number) => (
|
||||||
|
<div
|
||||||
|
key={`${selectedRecordId}-${log.slice(0, 30)}-${index}`}
|
||||||
|
className="mb-1 leading-relaxed"
|
||||||
|
>
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane title="流水线" key="pipeline">
|
||||||
|
<div className="grid grid-cols-5 gap-6 h-full">
|
||||||
|
{/* 左侧流水线列表 */}
|
||||||
|
<div className="col-span-2 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
共 {pipelines.length} 条流水线
|
||||||
|
</Typography.Text>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<IconPlus />}
|
||||||
|
size="small"
|
||||||
|
onClick={handleAddPipeline}
|
||||||
|
>
|
||||||
|
新建流水线
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="h-full overflow-y-auto">
|
||||||
|
<div className="space-y-3">
|
||||||
|
{pipelines.map((pipeline) => {
|
||||||
|
const isSelected = pipeline.id === selectedPipelineId;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={pipeline.id}
|
||||||
|
className={`cursor-pointer transition-all duration-200 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-blue-50 border-l-4 border-blue-500 border-blue-300'
|
||||||
|
: 'hover:bg-gray-50 border-gray-200'
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedPipelineId(pipeline.id)}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Typography.Title
|
||||||
|
heading={6}
|
||||||
|
className={`!m-0 ${
|
||||||
|
isSelected ? 'text-blue-600' : 'text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pipeline.name}
|
||||||
|
</Typography.Title>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={pipeline.enabled}
|
||||||
|
onChange={(enabled, e) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
e?.stopPropagation?.();
|
||||||
|
handleTogglePipeline(pipeline.id, enabled);
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
{!pipeline.enabled && (
|
||||||
|
<Tag color="gray" size="small">
|
||||||
|
已禁用
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Dropdown
|
||||||
|
droplist={
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item
|
||||||
|
key="edit"
|
||||||
|
onClick={() => handleEditPipeline(pipeline)}
|
||||||
|
>
|
||||||
|
<IconEdit className="mr-2" />
|
||||||
|
编辑流水线
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
key="copy"
|
||||||
|
onClick={() => handleCopyPipeline(pipeline)}
|
||||||
|
>
|
||||||
|
<IconCopy className="mr-2" />
|
||||||
|
复制流水线
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
key="delete"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeletePipeline(pipeline.id)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconDelete className="mr-2" />
|
||||||
|
删除流水线
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
position="bottom"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<IconMore />}
|
||||||
|
className="text-gray-400 hover:text-blue-500 hover:bg-blue-50 rounded-md p-1 transition-all duration-200"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<div>{pipeline.description}</div>
|
||||||
|
<div className="flex items-center justify-between mt-2">
|
||||||
|
<span>共 {pipeline.steps.length} 个步骤</span>
|
||||||
|
<span>{pipeline.updatedAt}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{pipelines.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
暂无流水线,点击上方"新建流水线"按钮开始创建
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右侧流水线步骤详情 */}
|
||||||
|
<div className="col-span-3 bg-white rounded-lg border h-full overflow-hidden">
|
||||||
|
{selectedPipelineId && pipelines.find(p => p.id === selectedPipelineId) ? (
|
||||||
|
(() => {
|
||||||
|
const selectedPipeline = pipelines.find(p => p.id === selectedPipelineId)!;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="p-4 border-b bg-gray-50">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<Typography.Title heading={5} className="!m-0">
|
||||||
|
{selectedPipeline.name} - 流水线步骤
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Text type="secondary" className="text-sm">
|
||||||
|
{selectedPipeline.description} · 共 {selectedPipeline.steps.length} 个步骤
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<IconPlus />}
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleAddStep(selectedPipelineId)}
|
||||||
|
>
|
||||||
|
添加步骤
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 h-full overflow-y-auto">
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={selectedPipeline.steps.map(step => step.id)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{selectedPipeline.steps.map((step, index) => (
|
||||||
|
<PipelineStepItem
|
||||||
|
key={step.id}
|
||||||
|
step={step}
|
||||||
|
index={index}
|
||||||
|
pipelineId={selectedPipelineId}
|
||||||
|
onToggle={handleToggleStep}
|
||||||
|
onEdit={handleEditStep}
|
||||||
|
onDelete={handleDeleteStep}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedPipeline.steps.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
暂无步骤,点击上方"添加步骤"按钮开始配置
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
请选择左侧的流水线查看详细步骤
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 新建/编辑流水线模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={editingPipeline ? '编辑流水线' : '新建流水线'}
|
||||||
|
visible={pipelineModalVisible}
|
||||||
|
onOk={handleSavePipeline}
|
||||||
|
onCancel={() => setPipelineModalVisible(false)}
|
||||||
|
style={{ width: 500 }}
|
||||||
|
>
|
||||||
|
<Form form={pipelineForm} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
field="name"
|
||||||
|
label="流水线名称"
|
||||||
|
rules={[{ required: true, message: '请输入流水线名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:前端部署流水线、Docker部署流水线..." />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="description"
|
||||||
|
label="流水线描述"
|
||||||
|
rules={[{ required: true, message: '请输入流水线描述' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="描述这个流水线的用途和特点..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 编辑步骤模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={editingStep ? '编辑流水线步骤' : '添加流水线步骤'}
|
||||||
|
visible={editModalVisible}
|
||||||
|
onOk={handleSaveStep}
|
||||||
|
onCancel={() => setEditModalVisible(false)}
|
||||||
|
style={{ width: 600 }}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
field="name"
|
||||||
|
label="步骤名称"
|
||||||
|
rules={[{ required: true, message: '请输入步骤名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:安装依赖、运行测试、构建项目..." />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
field="script"
|
||||||
|
label="Shell 脚本"
|
||||||
|
rules={[{ required: true, message: '请输入脚本内容' }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="例如:npm install npm test npm run build"
|
||||||
|
rows={8}
|
||||||
|
style={{ fontFamily: 'Monaco, Consolas, monospace' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<div className="bg-blue-50 p-3 rounded text-sm">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
<strong>可用环境变量:</strong>
|
||||||
|
<br />• $PROJECT_NAME - 项目名称
|
||||||
|
<br />• $BUILD_NUMBER - 构建编号
|
||||||
|
<br />• $REGISTRY - 镜像仓库地址
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectDetailPage;
|
||||||
13
apps/web/src/pages/project/detail/service.ts
Normal file
13
apps/web/src/pages/project/detail/service.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { net, type APIResponse } from '@shared';
|
||||||
|
import type { Project } from '../types';
|
||||||
|
|
||||||
|
class DetailService {
|
||||||
|
async getProject(id: string) {
|
||||||
|
const { code, data } = await net.request<APIResponse<Project>>({
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const detailService = new DetailService();
|
||||||
@@ -1,7 +1,27 @@
|
|||||||
import { Card, Tag, Avatar, Space, Typography, Button, Tooltip, Dropdown, Menu, Modal } from '@arco-design/web-react';
|
import {
|
||||||
import { IconBranch, IconCalendar, IconEye, IconCloud, IconEdit, IconMore, IconDelete } from '@arco-design/web-react/icon';
|
Card,
|
||||||
import type { Project } from '../types';
|
Tag,
|
||||||
|
Avatar,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
Modal,
|
||||||
|
} from '@arco-design/web-react';
|
||||||
|
import {
|
||||||
|
IconBranch,
|
||||||
|
IconCalendar,
|
||||||
|
IconCloud,
|
||||||
|
IconEdit,
|
||||||
|
IconMore,
|
||||||
|
IconDelete,
|
||||||
|
} from '@arco-design/web-react/icon';
|
||||||
|
import type { Project } from '../../types';
|
||||||
import IconGitea from '@assets/images/gitea.svg?react';
|
import IconGitea from '@assets/images/gitea.svg?react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
@@ -12,6 +32,7 @@ interface ProjectCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
// 处理删除操作
|
// 处理删除操作
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
@@ -30,7 +51,7 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
// 获取环境信息
|
// 获取环境信息
|
||||||
const environments = [
|
const environments = [
|
||||||
{ name: 'staging', color: 'orange', icon: '🚧' },
|
{ name: 'staging', color: 'orange', icon: '🚧' },
|
||||||
{ name: 'production', color: 'green', icon: '🚀' }
|
{ name: 'production', color: 'green', icon: '🚀' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 渲染环境标签
|
// 渲染环境标签
|
||||||
@@ -56,11 +77,16 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onProjectClick = useCallback(() => {
|
||||||
|
navigate(`/project/${project.id}`);
|
||||||
|
}, [navigate, project.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="foka-card !rounded-xl border border-gray-200 h-[320px] hover:border-blue-200 transition-all duration-300 hover:shadow-md"
|
className="foka-card !rounded-xl border border-gray-200 h-[280px] cursor-pointer"
|
||||||
hoverable
|
hoverable
|
||||||
bodyStyle={{ padding: '20px' }}
|
bodyStyle={{ padding: '20px' }}
|
||||||
|
onClick={onProjectClick}
|
||||||
>
|
>
|
||||||
{/* 项目头部 */}
|
{/* 项目头部 */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
@@ -90,10 +116,7 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
droplist={
|
droplist={
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item
|
<Menu.Item key="edit" onClick={() => onEdit?.(project)}>
|
||||||
key="edit"
|
|
||||||
onClick={() => onEdit?.(project)}
|
|
||||||
>
|
|
||||||
<IconEdit className="mr-2" />
|
<IconEdit className="mr-2" />
|
||||||
编辑
|
编辑
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
@@ -120,9 +143,7 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 项目描述 */}
|
{/* 项目描述 */}
|
||||||
<Paragraph
|
<Paragraph className="!m-0 !mb-4 !text-gray-600 !text-sm !leading-6 h-[42px] overflow-hidden line-clamp-2">
|
||||||
className="!m-0 !mb-4 !text-gray-600 !text-sm !leading-6 h-[42px] overflow-hidden line-clamp-2"
|
|
||||||
>
|
|
||||||
{project.description || '暂无描述'}
|
{project.description || '暂无描述'}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
|
|
||||||
@@ -152,25 +173,6 @@ function ProjectCard({ project, onEdit, onDelete }: ProjectCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={<IconEye />}
|
|
||||||
className="text-gray-500 hover:text-blue-500 transition-colors"
|
|
||||||
>
|
|
||||||
查看详情
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
className="text-blue-500 hover:text-blue-600 font-medium transition-colors"
|
|
||||||
>
|
|
||||||
管理项目 →
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Grid, Typography, Button, Message } from '@arco-design/web-react';
|
import { Grid, Typography, Button, Message } from '@arco-design/web-react';
|
||||||
import { IconPlus } from '@arco-design/web-react/icon';
|
import { IconPlus } from '@arco-design/web-react/icon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Project } from './types';
|
import type { Project } from '../types';
|
||||||
import { useAsyncEffect } from '../../hooks/useAsyncEffect';
|
import { useAsyncEffect } from '../../../hooks/useAsyncEffect';
|
||||||
import { projectService } from './service';
|
import { projectService } from './service';
|
||||||
import ProjectCard from './components/ProjectCard';
|
import ProjectCard from './components/ProjectCard';
|
||||||
import EditProjectModal from './components/EditProjectModal';
|
import EditProjectModal from './components/EditProjectModal';
|
||||||
@@ -1,5 +1,60 @@
|
|||||||
import { net, type APIResponse } from "@shared";
|
import { net, type APIResponse } from '@shared';
|
||||||
import type { Project } from "./types";
|
import type { Project } from '../types';
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectService {
|
||||||
|
async list(params?: ProjectQueryParams) {
|
||||||
|
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/projects',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async show(id: string) {
|
||||||
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(project: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
repository: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/api/projects',
|
||||||
|
data: project,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(
|
||||||
|
id: string,
|
||||||
|
project: Partial<{ name: string; description: string; repository: string }>,
|
||||||
|
) {
|
||||||
|
const { data } = await net.request<APIResponse<Project>>({
|
||||||
|
method: 'PUT',
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
data: project,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
await net.request({
|
||||||
|
method: 'DELETE',
|
||||||
|
url: `/api/projects/${id}`,
|
||||||
|
});
|
||||||
|
// DELETE 成功返回 204,无内容
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectService = new ProjectService();
|
||||||
|
|
||||||
interface ProjectListResponse {
|
interface ProjectListResponse {
|
||||||
data: Project[];
|
data: Project[];
|
||||||
@@ -16,55 +71,3 @@ interface ProjectQueryParams {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProjectService {
|
|
||||||
// GET /api/projects - 获取项目列表
|
|
||||||
async list(params?: ProjectQueryParams) {
|
|
||||||
const { data } = await net.request<APIResponse<ProjectListResponse>>({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/api/projects',
|
|
||||||
params,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /api/projects/:id - 获取单个项目
|
|
||||||
async show(id: string) {
|
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
|
||||||
method: 'GET',
|
|
||||||
url: `/api/projects/${id}`,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/projects - 创建项目
|
|
||||||
async create(project: { name: string; description?: string; repository: string }) {
|
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
|
||||||
method: 'POST',
|
|
||||||
url: '/api/projects',
|
|
||||||
data: project,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT /api/projects/:id - 更新项目
|
|
||||||
async update(id: string, project: Partial<{ name: string; description: string; repository: string }>) {
|
|
||||||
const { data } = await net.request<APIResponse<Project>>({
|
|
||||||
method: 'PUT',
|
|
||||||
url: `/api/projects/${id}`,
|
|
||||||
data: project,
|
|
||||||
});
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/projects/:id - 删除项目
|
|
||||||
async delete(id: string) {
|
|
||||||
await net.request({
|
|
||||||
method: 'DELETE',
|
|
||||||
url: `/api/projects/${id}`,
|
|
||||||
});
|
|
||||||
// DELETE 成功返回 204,无内容
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const projectService = new ProjectService();
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm run dev"
|
"dev": "pnpm --parallel -r run dev"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.0.6"
|
"@biomejs/biome": "2.0.6"
|
||||||
|
|||||||
118
pnpm-lock.yaml
generated
118
pnpm-lock.yaml
generated
@@ -77,22 +77,31 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@arco-design/web-react':
|
'@arco-design/web-react':
|
||||||
specifier: ^2.66.4
|
specifier: ^2.66.4
|
||||||
version: 2.66.5(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 2.66.5(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/core':
|
||||||
|
specifier: ^6.3.1
|
||||||
|
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/sortable':
|
||||||
|
specifier: ^10.0.0
|
||||||
|
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/utilities':
|
||||||
|
specifier: ^3.2.2
|
||||||
|
version: 3.2.2(react@18.3.1)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.11.0
|
specifier: ^1.11.0
|
||||||
version: 1.11.0
|
version: 1.11.0
|
||||||
react:
|
react:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.2.0
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.3.1
|
specifier: ^18.2.0
|
||||||
version: 18.3.1(react@18.3.1)
|
version: 18.3.1(react@18.3.1)
|
||||||
react-router:
|
react-router:
|
||||||
specifier: ^7.8.0
|
specifier: ^7.8.0
|
||||||
version: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^5.0.8
|
specifier: ^5.0.8
|
||||||
version: 5.0.8(@types/react@19.1.12)(react@18.3.1)
|
version: 5.0.8(@types/react@18.3.24)(react@18.3.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@arco-plugins/unplugin-react':
|
'@arco-plugins/unplugin-react':
|
||||||
specifier: 2.0.0-beta.5
|
specifier: 2.0.0-beta.5
|
||||||
@@ -113,11 +122,11 @@ importers:
|
|||||||
specifier: ^4.1.11
|
specifier: ^4.1.11
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: ^19.1.9
|
specifier: ^18.3.24
|
||||||
version: 19.1.12
|
version: 18.3.24
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.1.7
|
specifier: ^18.3.7
|
||||||
version: 19.1.9(@types/react@19.1.12)
|
version: 18.3.7(@types/react@18.3.24)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.11
|
specifier: ^4.1.11
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
@@ -273,6 +282,28 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@dnd-kit/accessibility@3.1.1':
|
||||||
|
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
|
'@dnd-kit/core@6.3.1':
|
||||||
|
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
|
'@dnd-kit/sortable@10.0.0':
|
||||||
|
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@dnd-kit/core': ^6.3.0
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
|
'@dnd-kit/utilities@3.2.2':
|
||||||
|
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
|
||||||
'@emnapi/core@1.4.5':
|
'@emnapi/core@1.4.5':
|
||||||
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
|
resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
|
||||||
|
|
||||||
@@ -846,19 +877,22 @@ packages:
|
|||||||
'@types/node@24.3.0':
|
'@types/node@24.3.0':
|
||||||
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
|
resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==}
|
||||||
|
|
||||||
|
'@types/prop-types@15.7.15':
|
||||||
|
resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
|
||||||
|
|
||||||
'@types/qs@6.14.0':
|
'@types/qs@6.14.0':
|
||||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7':
|
'@types/range-parser@1.2.7':
|
||||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||||
|
|
||||||
'@types/react-dom@19.1.9':
|
'@types/react-dom@18.3.7':
|
||||||
resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
|
resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/react': ^19.0.0
|
'@types/react': ^18.0.0
|
||||||
|
|
||||||
'@types/react@19.1.12':
|
'@types/react@18.3.24':
|
||||||
resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==}
|
resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==}
|
||||||
|
|
||||||
'@types/send@0.17.5':
|
'@types/send@0.17.5':
|
||||||
resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==}
|
resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==}
|
||||||
@@ -1949,7 +1983,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
color: 3.2.1
|
color: 3.2.1
|
||||||
|
|
||||||
'@arco-design/web-react@2.66.5(@types/react@19.1.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@arco-design/web-react@2.66.5(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@arco-design/color': 0.4.0
|
'@arco-design/color': 0.4.0
|
||||||
'@babel/runtime': 7.28.3
|
'@babel/runtime': 7.28.3
|
||||||
@@ -1961,7 +1995,7 @@ snapshots:
|
|||||||
number-precision: 1.6.0
|
number-precision: 1.6.0
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
react-focus-lock: 2.13.6(@types/react@19.1.12)(react@18.3.1)
|
react-focus-lock: 2.13.6(@types/react@18.3.24)(react@18.3.1)
|
||||||
react-is: 18.3.1
|
react-is: 18.3.1
|
||||||
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
resize-observer-polyfill: 1.5.1
|
resize-observer-polyfill: 1.5.1
|
||||||
@@ -2112,6 +2146,31 @@ snapshots:
|
|||||||
'@biomejs/cli-win32-x64@2.0.6':
|
'@biomejs/cli-win32-x64@2.0.6':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@dnd-kit/accessibility@3.1.1(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@dnd-kit/accessibility': 3.1.1(react@18.3.1)
|
||||||
|
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
'@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
|
||||||
|
react: 18.3.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.3.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@emnapi/core@1.4.5':
|
'@emnapi/core@1.4.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@emnapi/wasi-threads': 1.0.4
|
'@emnapi/wasi-threads': 1.0.4
|
||||||
@@ -2650,16 +2709,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.10.0
|
undici-types: 7.10.0
|
||||||
|
|
||||||
|
'@types/prop-types@15.7.15': {}
|
||||||
|
|
||||||
'@types/qs@6.14.0': {}
|
'@types/qs@6.14.0': {}
|
||||||
|
|
||||||
'@types/range-parser@1.2.7': {}
|
'@types/range-parser@1.2.7': {}
|
||||||
|
|
||||||
'@types/react-dom@19.1.9(@types/react@19.1.12)':
|
'@types/react-dom@18.3.7(@types/react@18.3.24)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 18.3.24
|
||||||
|
|
||||||
'@types/react@19.1.12':
|
'@types/react@18.3.24':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@types/prop-types': 15.7.15
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
'@types/send@0.17.5':
|
'@types/send@0.17.5':
|
||||||
@@ -3462,17 +3524,17 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
scheduler: 0.23.2
|
scheduler: 0.23.2
|
||||||
|
|
||||||
react-focus-lock@2.13.6(@types/react@19.1.12)(react@18.3.1):
|
react-focus-lock@2.13.6(@types/react@18.3.24)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.3
|
'@babel/runtime': 7.28.3
|
||||||
focus-lock: 1.3.6
|
focus-lock: 1.3.6
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-clientside-effect: 1.2.8(react@18.3.1)
|
react-clientside-effect: 1.2.8(react@18.3.1)
|
||||||
use-callback-ref: 1.3.3(@types/react@19.1.12)(react@18.3.1)
|
use-callback-ref: 1.3.3(@types/react@18.3.24)(react@18.3.1)
|
||||||
use-sidecar: 1.1.3(@types/react@19.1.12)(react@18.3.1)
|
use-sidecar: 1.1.3(@types/react@18.3.24)(react@18.3.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 18.3.24
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
@@ -3659,20 +3721,20 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
use-callback-ref@1.3.3(@types/react@19.1.12)(react@18.3.1):
|
use-callback-ref@1.3.3(@types/react@18.3.24)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 18.3.24
|
||||||
|
|
||||||
use-sidecar@1.1.3(@types/react@19.1.12)(react@18.3.1):
|
use-sidecar@1.1.3(@types/react@18.3.24)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-node-es: 1.1.0
|
detect-node-es: 1.1.0
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 18.3.24
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
@@ -3686,7 +3748,7 @@ snapshots:
|
|||||||
|
|
||||||
zod@4.1.5: {}
|
zod@4.1.5: {}
|
||||||
|
|
||||||
zustand@5.0.8(@types/react@19.1.12)(react@18.3.1):
|
zustand@5.0.8(@types/react@18.3.24)(react@18.3.1):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 18.3.24
|
||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
|
|||||||
Reference in New Issue
Block a user