重构前端逻辑

This commit is contained in:
dengqichen 2025-11-10 13:40:01 +08:00
parent 341062cef5
commit a80eea3a1e
3 changed files with 543 additions and 304 deletions

View File

@ -1,267 +1,394 @@
# CLAUDE.md ---
alwaysApply: true
---
---
alwaysApply: true
---
# RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 目录
- [RIPER-5 + O1 THINKING + AGENT EXECUTION PROTOCOL (OPTIMIZED)](#riper-5--o1-thinking--agent-execution-protocol-optimized)
- [目录](#目录)
- [上下文与设置](#上下文与设置)
- [核心思维原则](#核心思维原则)
- [模式详解](#模式详解)
- [模式1: RESEARCH](#模式1-research)
- [模式2: INNOVATE](#模式2-innovate)
- [模式3: PLAN](#模式3-plan)
- [模式4: EXECUTE](#模式4-execute)
- [模式5: REVIEW](#模式5-review)
- [关键协议指南](#关键协议指南)
- [代码处理指南](#代码处理指南)
- [任务文件模板](#任务文件模板)
- [性能期望](#性能期望)
## Project Overview ## 上下文与设置
<a id="上下文与设置"></a>
Deploy Ease Platform is a modern enterprise deployment management system frontend built with React 18, TypeScript, and Vite. It provides a comprehensive UI for managing applications, workflows, deployments, servers, and system configurations. 你是超智能AI编程助手集成在Cursor IDE中一个基于VS Code的AI增强IDE。由于你的先进能力你经常过于热衷于在未经明确请求的情况下实现更改这可能导致代码逻辑破坏。为防止这种情况你必须严格遵循本协议。
## Tech Stack **语言设置**:除非用户另有指示,所有常规交互响应应使用中文。然而,模式声明(如[MODE: RESEARCH])和特定格式化输出(如代码块、检查清单等)应保持英文以确保格式一致性。
- **Framework**: React 18 + TypeScript 5.3 **自动模式启动**:本优化版支持自动启动所有模式,无需显式过渡命令。每个模式完成后将自动进入下一个模式。
- **Build Tool**: Vite 5.x
- **Routing**: React Router 6
- **State Management**: Redux Toolkit
- **UI Components**: Radix UI + Ant Design + shadcn/ui
- **Styling**: TailwindCSS 3.x
- **Forms**: React Hook Form + Zod validation
- **HTTP Client**: Axios (with centralized request handler in `src/utils/request.ts`)
- **Code Editor**: Monaco Editor
- **Flow Visualization**: XYFlow + ReactFlow + Dagre
- **Form Builder**: @react-form-builder libraries
## Development Commands **模式声明要求**:你必须在每个响应的开头以方括号声明当前模式,没有例外。格式:`[MODE: MODE_NAME]`
```bash **初始默认模式**除非另有指示每次新对话默认从RESEARCH模式开始。然而如果用户的初始请求非常明确地指向特定阶段例如提供了一个完整的计划要求执行可以直接进入相应的模式如 EXECUTE
# Install dependencies
pnpm install
# Start development server (runs on http://localhost:3000) **代码修复指令**请修复所有预期表达式问题从第x行到第y行请确保修复所有问题不要遗漏任何问题。
pnpm dev
# Build for production (TypeScript + Vite) ## 核心思维原则
pnpm build <a id="核心思维原则"></a>
# Lint code (ESLint for .ts, .tsx files) 在所有模式中,这些基本思维原则将指导你的操作:
pnpm lint
# Preview production build locally - **系统思维**:从整体架构到具体实现进行分析
pnpm preview - **辩证思维**:评估多种解决方案及其利弊
- **创新思维**:打破常规模式,寻求创新解决方案
- **批判思维**:从多角度验证和优化解决方案
# Compile page generator script 在所有响应中平衡这些方面:
npx tsc -p scripts/tsconfig.json - 分析与直觉
- 细节检查与全局视角
- 理论理解与实际应用
- 深度思考与前进动力
- 复杂性与清晰度
# Run page generator (interactive mode) ## 模式详解
node dist/scripts/generate-page.js <a id="模式详解"></a>
# Run page generator (with CLI arguments) ### 模式1: RESEARCH
node dist/scripts/generate-page.js <moduleName> "<displayName>" "<apiEndpoint>" <a id="模式1-research"></a>
**目的**:信息收集和深入理解
**核心思维应用**
- 系统性地分解技术组件
- 清晰地映射已知/未知元素
- 考虑更广泛的架构影响
- 识别关键技术约束和需求
**允许**
- 阅读文件
- 提出澄清问题
- 理解代码结构
- 分析系统架构
- 识别技术债务或约束
- 创建任务文件(参见下方任务文件模板)
- 使用文件工具创建或更新任务文件的Analysis部分
**禁止**
- 提出建议
- 实施任何改变
- 规划
- 任何行动或解决方案的暗示
**研究协议步骤**
1. 分析与任务相关的代码:
- 识别核心文件/功能
- 追踪代码流程
- 记录发现以供后续使用
**思考过程**
```md
嗯... [系统思维方法的推理过程]
``` ```
## High-Level Architecture **输出格式**
以[MODE: RESEARCH]开始,然后仅提供观察和问题。
使用markdown语法格式化答案。
除非明确要求,否则避免使用项目符号。
### Directory Structure **持续时间**自动在完成研究后进入INNOVATE模式
``` ### 模式2: INNOVATE
src/ <a id="模式2-innovate"></a>
├── components/ # Reusable components
│ └── ui/ # Radix UI + shadcn/ui component library **目的**:头脑风暴潜在方法
├── pages/ # Page components (organized by feature module)
│ ├── Dashboard/ # Dashboard and overview **核心思维应用**
│ ├── Deploy/ # Deployment features (applications, deployments, servers, etc.) - 运用辩证思维探索多种解决路径
│ ├── System/ # System management (users, roles, menus, departments, etc.) - 应用创新思维打破常规模式
│ ├── Workflow/ # Workflow engine (design, instance, monitoring) - 平衡理论优雅与实际实现
│ ├── Form/ # Form management system - 考虑技术可行性、可维护性和可扩展性
│ ├── LogStream/ # Log streaming and viewing
│ └── Login/ # Authentication **允许**
├── layouts/ # Layout components (BasicLayout, etc.) - 讨论多种解决方案想法
├── router/ # React Router configuration (index.tsx) - 评估优点/缺点
├── store/ # Redux state management (user slice, etc.) - 寻求方法反馈
├── services/ # API service layer (weather.ts, etc.) - 探索架构替代方案
├── utils/ # Utility functions - 在"提议的解决方案"部分记录发现
│ ├── request.ts # Axios HTTP client with interceptors and error handling - 使用文件工具更新任务文件的Proposed Solution部分
│ ├── table.ts # Table-related utilities
│ ├── page.ts # Page-related utilities **禁止**
│ └── jsonSchemaUtils.ts - 具体规划
├── hooks/ # Custom React hooks (useTableData, usePageData, etc.) - 实现细节
├── types/ # TypeScript type definitions - 任何代码编写
├── config/ # Configuration files (icons, etc.) - 承诺特定解决方案
└── main.tsx # Application entry point
**创新协议步骤**
1. 基于研究分析创建方案:
- 研究依赖关系
- 考虑多种实现方法
- 评估每种方法的利弊
- 添加到任务文件的"提议的解决方案"部分
2. 暂不进行代码更改
**思考过程**
```md
嗯... [创造性、辩证的推理过程]
``` ```
### Architecture Patterns **输出格式**
以[MODE: INNOVATE]开始,然后仅提供可能性和考虑事项。
以自然流畅的段落呈现想法。
保持不同解决方案元素之间的有机联系。
#### 1. **Page Structure** **持续时间**自动在完成创新阶段后进入PLAN模式
Each feature page follows a consistent pattern:
### 模式3: PLAN
<a id="模式3-plan"></a>
**目的**:创建详尽的技术规范
**核心思维应用**
- 应用系统思维确保全面的解决方案架构
- 使用批判思维评估和优化计划
- 制定彻底的技术规范
- 确保目标专注,将所有计划与原始需求连接起来
**允许**
- 带有确切文件路径的详细计划
- 精确的函数名称和签名
- 具体的更改规范
- 完整的架构概述
**禁止**
- 任何实现或代码编写
- 甚至"示例代码"也不可实现
- 跳过或简化规范
**规划协议步骤**
1. 查看"任务进度"历史(如果存在)
2. 详细规划下一步更改
3. 提供明确理由和详细说明:
```
[更改计划]
- 文件:[更改的文件]
- 理由:[解释]
```
**所需规划元素**
- 文件路径和组件关系
- 函数/类修改及其签名
- 数据结构更改
- 错误处理策略
- 完整依赖管理
- 测试方法
**强制最终步骤**
将整个计划转换为编号的、按顺序排列的检查清单,每个原子操作作为单独的项目
**检查清单格式**
``` ```
pages/[Feature]/[Module]/ 实施检查清单:
├── List/ 1. [具体操作1]
│ ├── index.tsx # Main list component 2. [具体操作2]
│ ├── components/ # Page-specific components (modals, etc.) ...
│ ├── service.ts # API service calls n. [最终操作]
│ ├── types.ts # TypeScript interfaces
│ └── schema.ts # Form validation schemas (Zod)
└── Detail/ # If applicable
``` ```
#### 2. **API Layer** **输出格式**
All HTTP requests use the centralized `http` client from `src/utils/request.ts`: 以[MODE: PLAN]开始,然后仅提供规范和实现细节。
- **Request Interceptor**: Automatically adds Bearer token from localStorage 使用markdown语法格式化答案。
- **Response Interceptor**: Handles standardized Response<T> format with error handling
- **Error Handling**: Global error handling with toast notifications
- **Token Expiration**: Auto-logout on 401 with localStorage cleanup
Example: **持续时间**自动在计划完成后进入EXECUTE模式
```typescript
import http from '@/utils/request'; ### 模式4: EXECUTE
const data = await http.get<DataType>('/api/endpoint'); <a id="模式4-execute"></a>
const result = await http.post<ResultType>('/api/endpoint', payload);
**目的**完全按照模式3中的计划实施
**核心思维应用**
- 专注于精确实现规范
- 在实现过程中应用系统验证
- 保持对计划的精确遵守
- 实现完整功能,包括适当的错误处理
**允许**
- 仅实现已在批准的计划中明确详述的内容
- 严格按照编号的检查清单执行
- 标记已完成的检查清单项目
- 在实现后更新"任务进度"部分(这是执行过程的标准部分,被视为计划的内置步骤)
**禁止**
- 任何偏离计划的行为
- 计划中未规定的改进
- 创意补充或"更好的想法"
- 跳过或简化代码部分
**执行协议步骤**
1. 完全按计划实施更改
2. 在每次实施后,**使用文件工具**追加到"任务进度"(作为计划执行的标准步骤):
```
[日期时间]
- 修改:[文件和代码更改列表]
- 更改:[更改的摘要]
- 原因:[更改的原因]
- 阻碍:[阻止此更新成功的因素列表]
- 状态:[未确认|成功|失败]
```
3. 要求用户确认:"状态:成功/失败?"
4. 如果失败返回PLAN模式
5. 如果成功且需要更多更改:继续下一项
6. 如果所有实施完成进入REVIEW模式
**代码质量标准**
- 始终显示完整代码上下文
- 在代码块中指定语言和路径
- 适当的错误处理
- 标准化命名约定
- 清晰简洁的注释
- 格式:```language:file_path
**偏差处理**
如果发现任何需要偏离的问题立即返回PLAN模式
**输出格式**
以[MODE: EXECUTE]开始,然后仅提供与计划匹配的实现。
包括已完成的检查清单项目。
### 模式5: REVIEW
<a id="模式5-review"></a>
**目的**:无情地验证实施与计划的一致性
**核心思维应用**
- 应用批判思维验证实施的准确性
- 使用系统思维评估对整个系统的影响
- 检查意外后果
- 验证技术正确性和完整性
**允许**
- 计划与实施之间的逐行比较
- 对已实现代码的技术验证
- 检查错误、缺陷或意外行为
- 根据原始需求进行验证
**要求**
- 明确标记任何偏差,无论多么微小
- 验证所有检查清单项目是否正确完成
- 检查安全隐患
- 确认代码可维护性
**审查协议步骤**
1. 根据计划验证所有实施
2. **使用文件工具**完成任务文件中的"最终审查"部分
**偏差格式**
`检测到偏差:[确切偏差描述]`
**报告**
必须报告实施是否与计划完全一致
**结论格式**
`实施与计划完全匹配``实施偏离计划`
**输出格式**
以[MODE: REVIEW]开始,然后进行系统比较和明确判断。
使用markdown语法格式化。
## 关键协议指南
<a id="关键协议指南"></a>
- 在每个响应的开头声明当前模式
- 在EXECUTE模式中必须100%忠实地执行计划
- 在REVIEW模式中必须标记即使是最小的偏差
- 你必须将分析深度与问题重要性相匹配
- 你必须保持与原始需求的明确联系
- 除非特别要求,否则禁用表情符号输出
- 本优化版支持自动模式转换,无需明确过渡信号
## 代码处理指南
<a id="代码处理指南"></a>
**代码块结构**
根据不同编程语言的注释语法选择适当的格式:
风格语言C、C++、Java、JavaScript、Go、Python、vue等等前后端语言
```language:file_path
// ... existing code ...
{{ modifications }}
// ... existing code ...
``` ```
#### 3. **State Management** 如果语言类型不确定,使用通用格式:
Redux Toolkit is used minimally for global state (currently just user state): ```language:file_path
```typescript [... existing code ...]
// src/store/userSlice.ts - User authentication and info {{ modifications }}
// Access via: useSelector((state: RootState) => state.user) [... existing code ...]
``` ```
Form-level state uses React Hook Form (not Redux) for better performance and reduced boilerplate. **编辑指南**
- 仅显示必要的修改
- 包括文件路径和语言标识符
- 提供上下文注释
- 考虑对代码库的影响
- 验证与请求的相关性
- 保持范围合规性
- 避免不必要的更改
#### 4. **Routing** **禁止行为**
React Router 6 with lazy-loaded pages and route guards: - 使用未经验证的依赖项
- Private routes check Redux store for user token - 留下不完整的功能
- Pages are code-split with Suspense boundaries - 包含未测试的代码
- Dynamic menu loading from backend (stored in localStorage) - 使用过时的解决方案
- 在未明确要求时使用项目符号
- 跳过或简化代码部分
- 修改不相关的代码
- 使用代码占位符
#### 5. **Form Management** ## 任务文件模板
- **React Hook Form**: Form state and submission <a id="任务文件模板"></a>
- **Zod**: Schema validation (type-safe)
- **Modal Pattern**: Most forms are in modal dialogs using Radix UI Dialog
#### 6. **Styling** ```
- **TailwindCSS**: Utility-first CSS framework # 上下文
- **Ant Design**: Used for complex components (Table, Form, etc.) 文件名:[任务文件名]
- **Radix UI**: Headless UI for modals, dialogs, popover, etc. 创建于:[日期时间]
- **shadcn/ui**: Pre-built components built on Radix UI 创建者:[用户名]
Yolo模式[YOLO模式]
## Key Development Patterns # 任务描述
[用户完整任务描述]
### HTTP Requests with Error Handling # 项目概述
All HTTP requests automatically: [用户输入的项目详情]
1. Include Bearer token from localStorage
2. Handle 401 responses (logout and redirect to /login)
3. Display error toast notifications
4. Return typed data or reject with standardized error structure
### Type-Safe API Responses ⚠️ 警告:切勿修改此部分 ⚠️
```typescript [本部分应包含RIPER-5协议规则的核心摘要确保在执行过程中可以参考]
interface Response<T> { ⚠️ 警告:切勿修改此部分 ⚠️
code: number;
message: string; # 分析
data: T; [代码调查结果]
success: boolean;
} # 提议的解决方案
[行动计划]
# 当前执行步骤:"[步骤编号和名称]"
- 例如:"2. 创建任务文件"
# 任务进度
[带时间戳的更改历史]
# 最终审查
[完成后的总结]
``` ```
### Common Hooks ## 性能期望
- `useTableData`: Fetch and manage table data with pagination <a id="性能期望"></a>
- `usePageData`: Generic page data fetching
- `useSelector` / `useDispatch`: Redux integration
### Form Pattern Example - 响应延迟应最小化理想情况下≤360000ms
```typescript - 最大化计算能力和令牌限制
// Use Zod for schema - 寻求本质洞察而非表面枚举
const schema = z.object({ - 追求创新思维而非习惯性重复
name: z.string().min(1), - 突破认知限制,调动所有计算资源
email: z.string().email(),
});
// Use React Hook Form with Zod resolver
const form = useForm({
resolver: zodResolver(schema),
});
```
## Important Files Reference
- **`src/utils/request.ts`**: HTTP client - read this to understand API communication
- **`src/store/index.ts`**: Redux store configuration
- **`src/router/index.tsx`**: All routing configuration
- **`src/layouts/BasicLayout.tsx`**: Main application layout
- **`vite.config.ts`**: Build configuration (Monaco Editor asset copying, code splitting)
- **`tsconfig.json`**: TypeScript configuration with `@/*` path alias
## Code Splitting & Performance
The Vite build config includes manual chunks for the System module to optimize loading:
```typescript
// System pages grouped in separate bundle
manualChunks: {
'system': [
'./src/pages/System/User/index.tsx',
'./src/pages/System/Role/index.tsx',
// ... other system pages
]
}
```
## Environment & Backend Integration
- **Dev Server**: Runs on port 3000 with Vite HMR
- **API Proxy**: `/api/*` requests proxy to `http://localhost:8080` (configured in vite.config.ts)
- **LocalStorage**: Stores token, userInfo, menus, tenantId for persistence
## Common Tasks
### Adding a New CRUD Page
Use the page generator:
```bash
node dist/scripts/generate-page.js myFeature "My Feature" "/api/v1/my-feature"
```
This generates complete page structure with types, schema, service, and components.
### Making an API Call
```typescript
import http from '@/utils/request';
// GET request
const users = await http.get<User[]>('/api/users');
// POST with data
const newUser = await http.post<User>('/api/users', userData);
// PUT/DELETE
await http.put<User>('/api/users/1', updatedData);
await http.delete('/api/users/1');
// File upload
await http.upload('/api/upload', file);
// File download
await http.download('/api/export', 'export.xlsx');
```
### Form Validation with Zod
```typescript
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const schema = z.object({
username: z.string().min(1, 'Required'),
email: z.string().email('Invalid email'),
});
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
```
## Known Patterns & Conventions
1. **Prefixes**: Type files are named `types.ts`, validation schemas `schema.ts`, API services `service.ts`
2. **Components**: UI components in `components/ui/`, page-specific components in page folders
3. **Async Operations**: Use async/await, error handling via request interceptor
4. **Imports**: Use `@/*` path alias extensively
5. **Chinese Locale**: Ant Design configured for zh_CN, toast messages in Chinese
6. **Modules**: Pages are organized by business module first, then by feature (List, Detail, Design, etc.)
## Notes for Future Development
- The page generator tool is powerful for rapid CRUD page creation
- Monaco Editor assets need to be copied at build time (see vite.config.ts)
- Token authentication is automatically handled; focus on feature logic
- All error handling is centralized; individual components don't need error boundaries for HTTP calls
- Form state is separate from Redux; Redux only for user/auth data
- The architecture supports adding new Redux slices as the app grows

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react'; import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant, NodeProps } from '@xyflow/react'; import { ReactFlowProvider, ReactFlow, Background, Node, Edge, Handle, Position, BackgroundVariant, NodeProps, Controls, MiniMap } from '@xyflow/react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -7,6 +7,7 @@ import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { import {
Loader2, Loader2,
AlertCircle, AlertCircle,
@ -18,10 +19,13 @@ import {
CheckCircle2, CheckCircle2,
XCircle, XCircle,
AlertTriangle, AlertTriangle,
FileText FileText,
ArrowRightLeft,
ArrowDownUp
} from 'lucide-react'; } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import dagre from 'dagre';
import { getDeployRecordFlowGraph } from '../service'; import { getDeployRecordFlowGraph } from '../service';
import type { DeployRecordFlowGraph, WorkflowNodeInstance } from '../types'; import type { DeployRecordFlowGraph, WorkflowNodeInstance } from '../types';
import { import {
@ -54,6 +58,55 @@ interface CustomNodeData {
onViewLog?: (nodeId: string, nodeName: string) => void; onViewLog?: (nodeId: string, nodeName: string) => void;
} }
type LayoutDirection = 'TB' | 'LR';
/**
* 使 dagre
*/
const getLayoutedElements = (
nodes: Node[],
edges: Edge[],
direction: LayoutDirection = 'TB'
) => {
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeWidth = 180;
const nodeHeight = 120;
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({
rankdir: direction,
nodesep: 50,
ranksep: 80,
marginx: 20,
marginy: 20,
});
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
return {
...node,
position: {
x: nodeWithPosition.x - nodeWidth / 2,
y: nodeWithPosition.y - nodeHeight / 2,
},
};
});
return { nodes: layoutedNodes, edges };
};
/** /**
* *
*/ */
@ -84,7 +137,7 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
const nodeContent = ( const nodeContent = (
<div <div
className={cn( className={cn(
'px-4 py-3 rounded-md min-w-[160px] transition-all', 'px-3 py-2 rounded-md min-w-[160px] transition-all',
isNotStarted && 'border-2 border-dashed', isNotStarted && 'border-2 border-dashed',
!isNotStarted && 'border-2 border-solid shadow-sm', !isNotStarted && 'border-2 border-solid shadow-sm',
isRunning && 'animate-pulse', isRunning && 'animate-pulse',
@ -97,7 +150,7 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
> >
{/* 节点名称 */} {/* 节点名称 */}
<div className="flex items-center justify-between gap-2 mb-1"> <div className="flex items-center justify-between gap-2 mb-1">
<div className="font-medium text-sm">{nodeName}</div> <div className="font-medium text-sm truncate">{nodeName}</div>
{canViewLog && ( {canViewLog && (
<FileText className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" /> <FileText className="h-3.5 w-3.5 text-muted-foreground flex-shrink-0" />
)} )}
@ -114,11 +167,9 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
{/* 时间信息 */} {/* 时间信息 */}
{!isNotStarted && ( {!isNotStarted && (
<div className="text-xs text-muted-foreground space-y-0.5"> <div className="text-xs text-muted-foreground space-y-0.5">
{startTime && <div>: {formatTime(startTime)}</div>}
{endTime && <div>: {formatTime(endTime)}</div>}
{displayDuration && ( {displayDuration && (
<div className="font-medium"> <div className="font-medium">
{isRunning ? '运行: ' : '时长: '} {isRunning ? '运行: ' : '时长: '}
{displayDuration} {displayDuration}
</div> </div>
)} )}
@ -127,7 +178,7 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
{/* 错误提示 */} {/* 错误提示 */}
{hasFailed && errorMessage && ( {hasFailed && errorMessage && (
<div className="mt-2 flex items-center gap-1 text-red-600"> <div className="mt-1 flex items-center gap-1 text-red-600">
<AlertCircle className="h-3 w-3" /> <AlertCircle className="h-3 w-3" />
<span className="text-xs"></span> <span className="text-xs"></span>
</div> </div>
@ -135,7 +186,7 @@ const CustomFlowNode: React.FC<any> = ({ data }) => {
{/* 查看日志提示 */} {/* 查看日志提示 */}
{canViewLog && ( {canViewLog && (
<div className="mt-2 text-xs text-blue-600 flex items-center gap-1"> <div className="mt-1 text-xs text-blue-600 flex items-center gap-1">
<span></span> <span></span>
</div> </div>
)} )}
@ -401,6 +452,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
}) => { }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [flowData, setFlowData] = useState<DeployRecordFlowGraph | null>(null); const [flowData, setFlowData] = useState<DeployRecordFlowGraph | null>(null);
const [layoutDirection, setLayoutDirection] = useState<LayoutDirection>('TB');
// 日志对话框状态 // 日志对话框状态
const [logDialogOpen, setLogDialogOpen] = useState(false); const [logDialogOpen, setLogDialogOpen] = useState(false);
@ -414,6 +466,11 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
setLogDialogOpen(true); setLogDialogOpen(true);
}; };
// 切换布局方向
const toggleLayoutDirection = () => {
setLayoutDirection(prev => prev === 'TB' ? 'LR' : 'TB');
};
// ReactFlow 节点点击事件处理 // ReactFlow 节点点击事件处理
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
const nodeData = node.data as unknown as CustomNodeData; const nodeData = node.data as unknown as CustomNodeData;
@ -516,7 +573,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return []; if (!flowData?.graph?.nodes || !flowData?.nodeInstances) return [];
// 过滤并转换为 React Flow 节点 // 过滤并转换为 React Flow 节点
return flowData.graph.nodes const nodes = flowData.graph.nodes
.filter(node => visibleNodeIds.has(node.id)) // 只显示可见节点 .filter(node => visibleNodeIds.has(node.id)) // 只显示可见节点
.map((node) => { .map((node) => {
const instance = nodeInstanceMap.get(node.id); const instance = nodeInstanceMap.get(node.id);
@ -538,6 +595,8 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
}, },
}; };
}); });
return nodes;
}, [flowData, nodeInstanceMap, visibleNodeIds]); }, [flowData, nodeInstanceMap, visibleNodeIds]);
// 转换为 React Flow 边(只显示连接可见节点的边) // 转换为 React Flow 边(只显示连接可见节点的边)
@ -590,7 +649,7 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
id: edge.id || `edge-${source}-${target}-${index}`, id: edge.id || `edge-${source}-${target}-${index}`,
source, source,
target, target,
type: 'straight', // 使用直线类型 type: 'smoothstep', // 使用平滑曲线类型
animated, animated,
style: { style: {
stroke: strokeColor, stroke: strokeColor,
@ -607,6 +666,14 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
}); });
}, [flowData, nodeInstanceMap, visibleNodeIds]); }, [flowData, nodeInstanceMap, visibleNodeIds]);
// 应用自动布局
const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
if (flowNodes.length === 0) {
return { nodes: [], edges: [] };
}
return getLayoutedElements(flowNodes, flowEdges, layoutDirection);
}, [flowNodes, flowEdges, layoutDirection]);
// 获取部署状态信息 // 获取部署状态信息
const deployStatusInfo = flowData const deployStatusInfo = flowData
? (() => { ? (() => {
@ -624,27 +691,49 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="!max-w-7xl w-[90vw] h-[85vh] flex flex-col p-0 overflow-hidden"> <DialogContent className="!max-w-7xl w-[90vw] h-[85vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0"> <DialogHeader className="px-6 pt-6 pb-4 border-b flex-shrink-0">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center justify-between">
{flowData && ( <div className="flex items-center gap-2">
<span className="text-muted-foreground"> {flowData && (
#{flowData.deployRecordId} <span className="text-muted-foreground">
</span> #{flowData.deployRecordId}
)} </span>
<span></span> )}
{deployStatusInfo && ( <span></span>
<Badge {deployStatusInfo && (
variant="outline" <Badge
className={cn('flex items-center gap-1', deployStatusInfo.color)} variant="outline"
> className={cn('flex items-center gap-1', deployStatusInfo.color)}
<deployStatusInfo.icon >
className={cn( <deployStatusInfo.icon
'h-3 w-3', className={cn(
flowData?.deployStatus === 'RUNNING' && 'animate-spin' 'h-3 w-3',
)} flowData?.deployStatus === 'RUNNING' && 'animate-spin'
/> )}
{deployStatusInfo.text} />
</Badge> {deployStatusInfo.text}
)} </Badge>
)}
</div>
{/* 布局切换按钮 */}
<Button
variant="outline"
size="sm"
onClick={toggleLayoutDirection}
className="flex items-center gap-2"
>
{layoutDirection === 'TB' ? (
<>
<ArrowDownUp className="h-4 w-4" />
<span></span>
</>
) : (
<>
<ArrowRightLeft className="h-4 w-4" />
<span></span>
</>
)}
</Button>
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
@ -665,13 +754,13 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
<div className="flex-1 relative"> <div className="flex-1 relative">
<ReactFlowProvider> <ReactFlowProvider>
<ReactFlow <ReactFlow
nodes={flowNodes} nodes={layoutedNodes}
edges={flowEdges} edges={layoutedEdges}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
fitView fitView
className="bg-muted/10" className="bg-muted/10"
fitViewOptions={{ padding: 0.2, maxZoom: 1, minZoom: 0.5 }} fitViewOptions={{ padding: 0.15, maxZoom: 1.2, minZoom: 0.3 }}
nodesDraggable={false} nodesDraggable={false}
nodesConnectable={false} nodesConnectable={false}
elementsSelectable={true} elementsSelectable={true}
@ -679,6 +768,8 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
zoomOnScroll={true} zoomOnScroll={true}
zoomOnPinch={true} zoomOnPinch={true}
preventScrolling={false} preventScrolling={false}
minZoom={0.1}
maxZoom={2}
> >
<Background <Background
variant={BackgroundVariant.Dots} variant={BackgroundVariant.Dots}
@ -686,6 +777,26 @@ export const DeployFlowGraphModal: React.FC<DeployFlowGraphModalProps> = ({
size={1} size={1}
className="opacity-30" className="opacity-30"
/> />
<Controls
position="bottom-right"
showZoom={true}
showFitView={true}
showInteractive={false}
className="!shadow-lg !border !border-border"
/>
<MiniMap
position="bottom-left"
nodeColor={(node: Node) => {
const nodeData = node.data as unknown as CustomNodeData;
return getNodeStatusColor(nodeData.status);
}}
maskColor="rgba(0, 0, 0, 0.1)"
className="!shadow-lg !border !border-border"
style={{
height: 100,
width: 150,
}}
/>
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
</div> </div>

View File

@ -8,7 +8,7 @@ import {
DialogFooter, DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { Loader2, AlertCircle, Clock, FileText, RefreshCw } from 'lucide-react'; import { Loader2, AlertCircle, Clock, FileText, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { getDeployNodeLogs } from '../service'; import { getDeployNodeLogs } from '../service';
@ -149,7 +149,7 @@ const DeployNodeLogDialog: React.FC<DeployNodeLogDialogProps> = ({
</div> </div>
) : ( ) : (
<ScrollArea className="flex-1 border rounded-md bg-gray-50" ref={scrollAreaRef}> <ScrollArea className="flex-1 border rounded-md bg-gray-50" ref={scrollAreaRef}>
<div className="p-2 font-mono text-xs"> <div className="p-2 font-mono text-xs w-max min-w-full">
{logData?.logs && logData.logs.length > 0 ? ( {logData?.logs && logData.logs.length > 0 ? (
logData.logs.map((log, index) => { logData.logs.map((log, index) => {
// 计算行号宽度 // 计算行号宽度
@ -187,8 +187,8 @@ const DeployNodeLogDialog: React.FC<DeployNodeLogDialogProps> = ({
{log.level} {log.level}
</span> </span>
{/* 日志消息 - 占据剩余空间,不换行 */} {/* 日志消息 - 不换行显示 */}
<span className="flex-1 text-gray-800 whitespace-nowrap overflow-x-auto"> <span className="text-gray-800">
{log.message} {log.message}
</span> </span>
</div> </div>
@ -202,6 +202,7 @@ const DeployNodeLogDialog: React.FC<DeployNodeLogDialogProps> = ({
</div> </div>
)} )}
</div> </div>
<ScrollBar orientation="horizontal" />
</ScrollArea> </ScrollArea>
)} )}
</DialogBody> </DialogBody>