# 审批引擎 - UI 组件规范（自动化测试版）

> **版本**: v1.1  
> **创建日期**: 2025-12-25  
> **最后更新**: 2026-01-06  
> **状态**: ✅ 基于实际代码实现  
> **用途**: 前端开发 + 自动化测试

---

## 📌 文档说明

### 目标读者

1. **前端开发工程师** - 了解组件 API 和使用方法
2. **自动化测试工程师** - 编写组件级别测试用例
3. **QA 工程师** - 手动测试组件功能

### 文档特点

- ✅ 基于**实际代码实现**，非臆想
- ✅ 包含**详细的组件 API**
- ✅ 包含**测试选择器**（CSS Selector / Test ID）
- ✅ 详细的**交互步骤**和**预期结果**
- ✅ 状态管理和数据流说明
- ⚠️ 标注与 PRD 的差异

---

## 📐 架构定位

审批引擎是**引擎层**模块，提供可复用的**UI组件**，并在前端路由中被审批中心与审批详情页复用。

```
┌─────────────────────────────────────────────────────────────────┐
│                        业务应用层 (Pages)                        │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────┐       ┌─────────────────────┐          │
│  │      表单管理        │       │      审批中心        │          │
│  │   /forms            │       │   /approval-center  │          │
│  │                     │       │                     │          │
│  │  • 集成设计器页面    │       │  • 流程跟踪页面      │          │
│  │    (使用流程设计器)  │       │    (使用流程查看器)  │          │
│  └─────────────────────┘       └─────────────────────┘          │
└─────────────────────────────────────────────────────────────────┘
                          │                     │
                          ▼                     ▼
┌─────────────────────────────────────────────────────────────────┐
│                   引擎层 (Components/Services)                   │
├─────────────────────────────────────────────────────────────────┤
│  ┌─────────────────────┐       ┌─────────────────────┐          │
│  │      表单引擎        │       │      审批引擎 ⭐      │          │
│  │   form-engine       │       │   approval-engine   │          │
│  │                     │       │                     │          │
│  │  • FormRenderer     │       │  • DingProcessDesigner│         │
│  │  • FieldRenderers   │       │  • ApprovalFlowDiagram│         │
│  └─────────────────────┘       └─────────────────────┘          │
└─────────────────────────────────────────────────────────────────┘
```

### 职责边界

|| 层级 | 模块 | UI 职责 |
||------|------|---------|
|| **业务应用** | 表单管理 | 提供完整的集成设计器**页面** |
|| **业务应用** | 审批中心 | 提供完整的流程跟踪**页面** |
|| **引擎层** | 审批引擎 | 提供 `DingProcessDesigner` **组件**（流程设计器） |
|| **引擎层** | 审批引擎 | 提供 `ApprovalFlowDiagram` **组件**（流程查看器） |

> **⚠️ 重要**：本文档以组件规范为主，页面布局由 `/approval-center` 与 `/approval/[businessType]/[instanceId]` 负责。

---

## 📦 组件清单

|| 组件名 | 路径 | 用途 | 复用性 |
||--------|------|------|--------|
|| **DingProcessDesigner** ⭐ | `features/approval/designer/DingProcessDesigner.tsx` | 钉钉风格流程设计器 | ⭐⭐⭐ 高 |
|| DingStylePropertyPanel | `features/approval/designer/DingStylePropertyPanel.tsx` | 流程节点属性面板 | 高 |
|| ProcessDesigner | `features/approval/designer/ProcessDesigner.tsx` | 设计器容器 | 高 |
|| LarkProcessPreview | `features/approval/designer/LarkProcessPreview.tsx` | 飞书风格流程预览组件 | 高 |
|| ApprovalFlowDiagram | `features/approval/components/ApprovalFlowDiagram.tsx` | 流程只读查看器（运行时） | 中 |
|| ApprovalActions | `features/approval/components/ApprovalActions.tsx` | 审批任务操作组件 | 中 |
|| ApprovalHistory | `features/approval/components/ApprovalHistory.tsx` | 审批历史组件 | 中 |

---

## 📄 1. 核心组件：DingProcessDesigner

> **文件**: `frontend/src/features/approval/designer/DingProcessDesigner.tsx`  
> **大小**: ~1,646 行  
> **依赖**: Framer Motion, Lucide React, 自定义 types

### 1.1 组件职责

`DingProcessDesigner` 是审批引擎的核心组件，负责：

- ✅ 提供钉钉风格的**垂直流程图**设计器
- ✅ 支持**拖拽添加/删除**节点
- ✅ 支持节点**属性配置**（通过右侧属性面板）
- ✅ 支持**条件分支**和**并行分支**
- ✅ 支持**撤销/重做**历史记录
- ✅ 输出标准的**流程定义 JSON** (`ProcessModel`)

### 1.2 组件 API

```typescript
interface DingProcessDesignerProps {
  // 数据源
  initialModel?: ProcessModel;          // 初始流程定义（受控）
  
  // 关联的表单字段（用于字段权限配置）
  formFields?: FormField[];             // 表单字段列表
  
  // 事件回调
  onChange?: (model: ProcessModel) => void;  // 流程变化回调
  onValidate?: (errors: ValidationError[]) => void;  // 验证错误回调
  
  // 样式配置
  className?: string;
  style?: React.CSSProperties;
  
  // 其他配置
  readonly?: boolean;                   // 是否只读（默认 false）
  showValidation?: boolean;             // 是否显示验证错误（默认 true）
}

interface ProcessModel {
  id: string;
  name: string;
  nodes: FlowNode[];                    // 节点列表（扁平化）
  initiatorConfig?: InitiatorConfig;    // 发起人配置
}

interface FlowNode {
  id: string;
  type: ProcessNodeType;                // 节点类型
  name: string;                         // 节点名称
  config: ProcessNodeConfig;            // 节点配置
  // 条件分支专用
  conditions?: ConditionBranch[];
  // 并行分支专用
  parallelBranches?: ParallelBranch[];
}

type ProcessNodeType = 
  | 'USER_TASK'           // 审批人
  | 'EXECUTOR'            // 执行人
  | 'CC'                  // 抄送人
  | 'EXCLUSIVE_GATEWAY'   // 条件分支
  | 'PARALLEL_GATEWAY';   // 并行分支

interface ProcessNodeConfig {
  // 审批人配置
  approverType?: ApproverType;
  approvers?: string[];              // 指定审批人 ID 列表
  roles?: string[];                  // 指定角色 ID 列表
  
  // 多人审批配置
  approvalMode?: 'AND' | 'OR' | 'SEQUENTIAL';
  
  // 连续主管配置（主管链审批）
  chainConfig?: ManagerChainConfig;
  
  // 字段权限配置（与表单字段联动）
  editableFields?: string[];         // 可编辑字段列表
  requiredFields?: string[];         // 必填字段列表
  hiddenFields?: string[];           // 隐藏字段列表
  
  // 超时配置
  timeout?: TimeoutConfig;
  
  // 允许的操作
  allowedActions?: ('APPROVE' | 'REJECT' | 'RETURN' | 'FORWARD' | 'ADD_SIGN')[];
}

type ApproverType = 
  | 'FIXED_USER'            // 指定成员
  | 'ROLE'                  // 指定角色
  | 'DEPARTMENT'            // 部门主管
  | 'INITIATOR_MANAGER'     // 直属主管
  | 'MANAGER_CHAIN'         // 多级主管（连续审批）
  | 'FORM_FIELD'            // 表单字段
  | 'SELF_SELECT';          // 发起人自选

interface ManagerChainConfig {
  maxLevels: number;                 // 最大审批层级
  stopCondition?: string;            // 终止条件（如 "到CEO停止"）
  autoApprove?: 'none' | 'initiator' | 'duplicate' | 'both';
  // - none: 不自动通过
  // - initiator: 审批人=发起人时自动通过
  // - duplicate: 审批人已审批过时自动跳过
  // - both: 以上两种情况都自动通过
}

interface TimeoutConfig {
  hours: number;                     // 超时时间（小时）
  action: 'remind' | 'auto_approve' | 'auto_reject';
}
```

### 1.3 视觉布局

#### 整体结构

```
┌─────────────────────────────────────────────────────────────────┐
│  🔧 顶部工具栏                                                    │
│  [撤销] [重做] [复制] [校验] [预览]                 [关闭]        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌────────────────────────────────────┬──────────────────────┐ │
│  │  📝 流程画布 (垂直布局)             │  ⚙️ 属性面板        │ │
│  │                                    │                      │ │
│  │  ┌──────────────────────────┐     │  [节点类型]          │ │
│  │  │ 👤 发起人                 │     │  节点名称: [___]     │ │
│  │  └──────────────────────────┘     │  审批人设置:         │ │
│  │            ↓                       │    [指定成员 ▼]      │ │
│  │  ┌──────────────────────────┐     │    - 部门主管        │ │
│  │  │ ✓ 审批人                  │     │    - 直属主管        │ │
│  │  │ 部门经理                  │     │    - 多级主管        │ │
│  │  └──────────────────────────┘     │  多人审批:           │ │
│  │            ↓                       │    [会签 ▼]          │ │
│  │  ┌──────────────────────────┐     │  超时设置:           │ │
│  │  │ ✓ 审批人                  │     │    [24小时 ▼]        │ │
│  │  │ 财务主管                  │     │  字段权限:           │ │
│  │  └──────────────────────────┘     │    [配置 >]          │ │
│  │            ↓                       │                      │ │
│  │  ┌──────────────────────────┐     │                      │ │
│  │  │ ✓ 结束                    │     │                      │ │
│  │  └──────────────────────────┘     │                      │ │
│  │                                    │                      │ │
│  └────────────────────────────────────┴──────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### 1.4 节点类型配置

#### 1.4.1 节点颜色方案（钉钉风格）

```typescript
const NODE_COLORS = {
  // 审批人 - 蓝色
  USER_TASK: {
    primary: '#1677ff',
    bg: '#e6f4ff',
    border: '#1677ff',
    text: '#1677ff',
  },
  // 执行人 - 绿色
  EXECUTOR: {
    primary: '#52c41a',
    bg: '#f6ffed',
    border: '#52c41a',
    text: '#52c41a',
  },
  // 抄送人 - 紫色
  CC: {
    primary: '#722ed1',
    bg: '#f9f0ff',
    border: '#722ed1',
    text: '#722ed1',
  },
  // 条件分支 - 橙色
  CONDITION: {
    primary: '#fa8c16',
    bg: '#fff7e6',
    border: '#fa8c16',
    text: '#fa8c16',
  },
  // 并行分支 - 青色
  PARALLEL: {
    primary: '#13c2c2',
    bg: '#e6fffb',
    border: '#13c2c2',
    text: '#13c2c2',
  },
};
```

#### 1.4.2 节点类型选项

**测试选择器**:
```typescript
// 添加节点按钮（圆形蓝色加号）
'.w-7.h-7.rounded-full.bg-blue-500'
'button:has(> svg.lucide-plus)'

// 节点类型菜单（黑色弹窗）
'.bg-\\[\\#1a1a1a\\].rounded-2xl'

// 节点类型按钮
'button:has-text("审批人")'
'button:has-text("执行人")'
'button:has-text("抄送人")'
'button:has-text("条件分支")'
'button:has-text("并行分支")'
```

**节点类型配置**:
```typescript
const NODE_TYPE_OPTIONS = [
  { 
    type: 'USER_TASK', 
    label: '审批人', 
    icon: UserCheck, 
    color: NODE_COLORS.USER_TASK, 
    category: 'approver' 
  },
  { 
    type: 'EXECUTOR', 
    label: '执行人', 
    icon: UserCog, 
    color: NODE_COLORS.EXECUTOR, 
    category: 'approver' 
  },
  { 
    type: 'CC', 
    label: '抄送人', 
    icon: Users, 
    color: NODE_COLORS.CC, 
    category: 'approver' 
  },
  { 
    type: 'EXCLUSIVE_GATEWAY', 
    label: '条件分支', 
    icon: GitBranch, 
    color: NODE_COLORS.CONDITION, 
    category: 'branch' 
  },
  { 
    type: 'PARALLEL_GATEWAY', 
    label: '并行分支', 
    icon: GitMerge, 
    color: NODE_COLORS.PARALLEL, 
    category: 'branch' 
  },
];
```

### 1.5 节点卡片组件

#### 1.5.1 普通节点卡片结构

**测试选择器**:
```typescript
// 节点卡片
'.relative.bg-white.rounded-xl.border-2'
`[data-node-id="${nodeId}"]` // 推荐添加

// 节点选中状态
'.border-blue-500' // 选中时的蓝色边框

// 节点标题
'.text-sm.font-medium'

// 删除按钮（hover时显示）
'button:has(> svg.lucide-x)'
```

**样式特征**:
```typescript
// 基础样式
className="relative bg-white rounded-xl border-2 shadow-sm transition-all duration-200 cursor-pointer hover:shadow-md"
style={{
  width: '200px',
  borderColor: isSelected ? '#1677ff' : '#e5e7eb',
}}

// Hover 效果
onMouseEnter: borderColor → nodeColor
onMouseLeave: borderColor → '#e5e7eb'

// 选中效果
isSelected: borderColor → '#1677ff' (蓝色)
```

**内容结构**:
```html
<div class="relative bg-white rounded-xl border-2 shadow-sm" style="width: 200px">
  <!-- 顶部色块 -->
  <div class="h-1 rounded-t-xl" style="background-color: {nodeColor}"></div>
  
  <!-- 内容区域 -->
  <div class="px-4 py-3">
    <!-- 图标 + 节点类型 -->
    <div class="flex items-center gap-2 mb-2">
      <div class="w-6 h-6 rounded-lg flex items-center justify-center"
           style="background-color: {nodeColor}20">
        <UserCheck class="w-4 h-4" style="color: {nodeColor}" />
      </div>
      <span class="text-xs font-medium" style="color: {nodeColor}">
        审批人
      </span>
    </div>
    
    <!-- 节点名称 -->
    <div class="text-sm font-medium text-gray-900 mb-1">
      {node.name || '未命名'}
    </div>
    
    <!-- 配置摘要（根据节点类型动态显示） -->
    <div class="text-xs text-gray-500">
      {getNodeSummary(node)}
    </div>
  </div>
  
  <!-- 删除按钮（hover时显示） -->
  {!readonly && isHovered && (
    <button class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-red-500 hover:bg-red-600">
      <X class="w-3 h-3 text-white" />
    </button>
  )}
</div>
```

#### 1.5.2 节点配置摘要显示

**摘要生成逻辑**:
```typescript
const getNodeSummary = (node: FlowNode) => {
  const { type, config } = node;
  
  switch (type) {
    case 'USER_TASK':
    case 'EXECUTOR':
      if (config.approverType === 'FIXED_USER') {
        return `指定成员 (${config.approvers?.length || 0}人)`;
      } else if (config.approverType === 'INITIATOR_MANAGER') {
        return '直属主管';
      } else if (config.approverType === 'MANAGER_CHAIN') {
        return `连续${config.chainConfig?.maxLevels || 1}级主管`;
      } else if (config.approverType === 'DEPARTMENT') {
        return '部门主管';
      } else if (config.approverType === 'ROLE') {
        return `指定角色 (${config.roles?.length || 0}个)`;
      } else if (config.approverType === 'FORM_FIELD') {
        return '由表单字段指定';
      } else if (config.approverType === 'SELF_SELECT') {
        return '发起人自选';
      }
      return '未配置审批人';
    
    case 'CC':
      return `抄送给 ${config.approvers?.length || 0} 人`;
    
    case 'EXCLUSIVE_GATEWAY':
      return `${node.conditions?.length || 0} 个分支`;
    
    case 'PARALLEL_GATEWAY':
      return `${node.parallelBranches?.length || 0} 个并行分支`;
    
    default:
      return '';
  }
};
```

### 1.6 属性面板（DingStylePropertyPanel）

> **组件**: `frontend/src/features/approval/designer/DingStylePropertyPanel.tsx`

#### 1.6.1 面板布局

**测试选择器**:
```typescript
// 属性面板容器
'.w-80.h-full.bg-white.border-l'

// 关闭按钮
'button:has-text("关闭")'
'button:has(> svg.lucide-x)'

// 节点名称输入框
'input[value*="审批人"]'
'input[placeholder="输入节点名称"]'
```

**基本结构**:
```html
<div class="w-80 h-full bg-white border-l border-gray-200 flex flex-col">
  <!-- 头部 -->
  <div class="px-6 py-4 border-b border-gray-200">
    <div class="flex items-center justify-between">
      <h2 class="text-base font-semibold text-gray-900">节点配置</h2>
      <button onClick={onClose} class="text-gray-400 hover:text-gray-600">
        <X class="w-5 h-5" />
      </button>
    </div>
  </div>
  
  <!-- 内容滚动区域 -->
  <div class="flex-1 overflow-y-auto px-6 py-4 space-y-6">
    <!-- 基本信息 -->
    <div>
      <label class="text-sm font-medium text-gray-700 mb-2 block">
        节点名称
      </label>
      <input
        value={editingName}
        onChange={(e) => setEditingName(e.target.value)}
        onBlur={handleNameChange}
        class="w-full px-3 py-2 border border-gray-300 rounded-lg"
        placeholder="输入节点名称"
      />
    </div>
    
    <!-- 节点类型特定配置 -->
    {renderNodeTypeConfig()}
    
    <!-- 高级配置 -->
    {renderAdvancedConfig()}
  </div>
</div>
```

#### 1.6.2 审批人类型配置

**测试选择器**:
```typescript
// 审批人类型选择器
'select[value="INITIATOR_MANAGER"]'
'[data-testid="approver-type-select"]' // 推荐添加

// 审批人类型选项
'option[value="FIXED_USER"]'
'option[value="INITIATOR_MANAGER"]'
'option[value="MANAGER_CHAIN"]'
```

**配置选项**:
```typescript
<div class="space-y-2">
  <label class="text-sm font-medium text-gray-700">审批人类型</label>
  <select
    value={config.approverType || 'FIXED_USER'}
    onChange={(e) => updateConfig({ approverType: e.target.value })}
    class="w-full px-3 py-2 border border-gray-300 rounded-lg"
  >
    <option value="FIXED_USER">指定成员</option>
    <option value="ROLE">指定角色</option>
    <option value="DEPARTMENT">部门主管</option>
    <option value="INITIATOR_MANAGER">直属主管</option>
    <option value="MANAGER_CHAIN">多级主管（连续审批）</option>
    <option value="FORM_FIELD">由表单字段指定</option>
    <option value="SELF_SELECT">发起人自选</option>
  </select>
</div>
```

#### 1.6.3 多级主管配置（`initiator:managerChain`）

**显示条件**: `config.approverType === 'MANAGER_CHAIN'`

**测试选择器**:
```typescript
// 最大层级数输入框
'input[type="number"][min="1"][max="10"]'
'[data-testid="manager-chain-max-levels"]' // 推荐添加

// 自动通过选项
'[data-testid="auto-approve-select"]'
```

**配置界面**:
```typescript
{config.approverType === 'MANAGER_CHAIN' && (
  <div class="space-y-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
    <h3 class="text-sm font-medium text-blue-900">连续主管审批配置</h3>
    
    <!-- 最大层级数 -->
    <div>
      <label class="text-sm font-medium text-gray-700 mb-2 block">
        最大审批层级
      </label>
      <input
        type="number"
        min="1"
        max="10"
        value={config.chainConfig?.maxLevels || 1}
        onChange={(e) => updateChainConfig({ maxLevels: parseInt(e.target.value) })}
        class="w-full px-3 py-2 border border-gray-300 rounded-lg"
      />
      <p class="text-xs text-gray-500 mt-1">
        从发起人的直属主管开始，最多向上审批 {config.chainConfig?.maxLevels || 1} 级
      </p>
    </div>
    
    <!-- 终止条件 -->
    <div>
      <label class="text-sm font-medium text-gray-700 mb-2 block">
        终止条件（可选）
      </label>
      <input
        type="text"
        value={config.chainConfig?.stopCondition || ''}
        onChange={(e) => updateChainConfig({ stopCondition: e.target.value })}
        class="w-full px-3 py-2 border border-gray-300 rounded-lg"
        placeholder="例如：到CEO停止"
      />
      <p class="text-xs text-gray-500 mt-1">
        满足条件时停止向上查找主管（如职位、部门等）
      </p>
    </div>
    
    <!-- 自动通过策略 -->
    <div>
      <label class="text-sm font-medium text-gray-700 mb-2 block">
        自动通过策略
      </label>
      <select
        value={config.chainConfig?.autoApprove || 'none'}
        onChange={(e) => updateChainConfig({ autoApprove: e.target.value as any })}
        class="w-full px-3 py-2 border border-gray-300 rounded-lg"
      >
        <option value="none">不自动通过</option>
        <option value="initiator">审批人=发起人时自动通过</option>
        <option value="duplicate">审批人已审批过时自动跳过</option>
        <option value="both">以上两种情况都自动通过</option>
      </select>
      <p class="text-xs text-gray-500 mt-1">
        避免重复审批和自己审批自己的情况
      </p>
    </div>
  </div>
)}
```

**数据结构**:
```typescript
interface ManagerChainConfig {
  maxLevels: number;          // 最大层级，如 3 表示最多向上3级
  stopCondition?: string;     // 终止条件描述，如 "到CEO停止"
  autoApprove?: 'none' | 'initiator' | 'duplicate' | 'both';
  // - none: 不自动通过（默认）
  // - initiator: 当审批人=发起人时自动通过
  // - duplicate: 当审批人在之前的节点已审批过时自动跳过
  // - both: 以上两种情况都自动处理
}
```

#### 1.6.4 多人审批方式配置

**显示条件**: 审批人数量 > 1

**测试选择器**:
```typescript
'[data-testid="approval-mode-select"]'
'select:has(option[value="AND"])'
```

**配置界面**:
```typescript
{(config.approvers?.length || 0) > 1 && (
  <div class="space-y-2">
    <label class="text-sm font-medium text-gray-700">多人审批方式</label>
    <select
      value={config.approvalMode || 'AND'}
      onChange={(e) => updateConfig({ approvalMode: e.target.value as any })}
      class="w-full px-3 py-2 border border-gray-300 rounded-lg"
    >
      <option value="AND">会签（所有人同意）</option>
      <option value="OR">或签（任一人同意）</option>
      <option value="SEQUENTIAL">依次审批（按顺序）</option>
    </select>
    <p class="text-xs text-gray-500">
      {config.approvalMode === 'AND' && '所有审批人都需要同意才能通过'}
      {config.approvalMode === 'OR' && '任一审批人同意即可通过'}
      {config.approvalMode === 'SEQUENTIAL' && '按添加顺序依次审批'}
    </p>
  </div>
)}
```

#### 1.6.5 超时配置

**测试选择器**:
```typescript
'[data-testid="timeout-hours-input"]'
'[data-testid="timeout-action-select"]'
```

**配置界面**:
```typescript
<div class="space-y-4 p-4 bg-amber-50 border border-amber-200 rounded-lg">
  <h3 class="text-sm font-medium text-amber-900 flex items-center gap-2">
    <Clock class="w-4 h-4" />
    超时设置
  </h3>
  
  <div class="space-y-2">
    <label class="text-sm font-medium text-gray-700">超时时间（小时）</label>
    <input
      type="number"
      min="1"
      max="720"
      value={config.timeout?.hours || 24}
      onChange={(e) => updateTimeout({ hours: parseInt(e.target.value) })}
      class="w-full px-3 py-2 border border-gray-300 rounded-lg"
    />
  </div>
  
  <div class="space-y-2">
    <label class="text-sm font-medium text-gray-700">超时后操作</label>
    <select
      value={config.timeout?.action || 'remind'}
      onChange={(e) => updateTimeout({ action: e.target.value as any })}
      class="w-full px-3 py-2 border border-gray-300 rounded-lg"
    >
      <option value="remind">仅提醒</option>
      <option value="auto_approve">自动通过</option>
      <option value="auto_reject">自动驳回</option>
    </select>
  </div>
</div>
```

### 1.7 状态管理

**内部状态**:
```typescript
const [nodes, setNodes] = useState<FlowNode[]>([]);
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [history, setHistory] = useState<FlowNode[][]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
const [isDirty, setIsDirty] = useState(false);
```

**撤销/重做实现**:
```typescript
// 添加历史记录
const addHistory = (newNodes: FlowNode[]) => {
  const newHistory = history.slice(0, historyIndex + 1);
  newHistory.push(newNodes);
  setHistory(newHistory);
  setHistoryIndex(newHistory.length - 1);
};

// 撤销
const handleUndo = () => {
  if (historyIndex > 0) {
    setHistoryIndex(historyIndex - 1);
    setNodes(history[historyIndex - 1]);
  }
};

// 重做
const handleRedo = () => {
  if (historyIndex < history.length - 1) {
    setHistoryIndex(historyIndex + 1);
    setNodes(history[historyIndex + 1]);
  }
};
```

**数据同步**:
```typescript
// 当 nodes 变化时，触发 onChange 回调
useEffect(() => {
  if (onChange && isDirty) {
    const model: ProcessModel = {
      id: initialModel?.id || generateNodeId('process'),
      name: initialModel?.name || '新流程',
      nodes,
    };
    onChange(model);
  }
}, [nodes, onChange, isDirty]);
```

### 1.8 API 调用总结

| 方法 | 说明 | 触发时机 |
|------|------|---------|
| `onChange(model)` | 流程模型变化回调 | 节点增删改、配置更新 |
| `onValidate(errors)` | 验证错误回调 | 节点配置不完整、逻辑错误 |

### 1.9 测试用例建议（待补充）

```typescript

describe('DingProcessDesigner 组件测试', () => {
  test('应该渲染发起人节点和结束节点', () => {
    render(<DingProcessDesigner />);
    
    expect(screen.getByText('发起人')).toBeInTheDocument();
    expect(screen.getByText('结束')).toBeInTheDocument();
  });
  
  test('点击添加按钮应显示节点类型菜单', async () => {
    render(<DingProcessDesigner />);
    
    const addButton = screen.getByRole('button', { name: /添加/i });
    await userEvent.click(addButton);
    
    expect(screen.getByText('审批人')).toBeVisible();
    expect(screen.getByText('执行人')).toBeVisible();
    expect(screen.getByText('抄送人')).toBeVisible();
  });
  
  test('添加审批人节点应触发 onChange 回调', async () => {
    const onChange = jest.fn();
    render(<DingProcessDesigner onChange={onChange} />);
    
    const addButton = screen.getByRole('button', { name: /添加/i });
    await userEvent.click(addButton);
    
    const approverButton = screen.getByText('审批人');
    await userEvent.click(approverButton);
    
    expect(onChange).toHaveBeenCalled();
    expect(onChange.mock.calls[0][0].nodes).toHaveLength(1);
  });
  
  test('选中节点应显示属性面板', async () => {
    render(<DingProcessDesigner initialModel={mockModel} />);
    
    const nodeCard = screen.getByText('部门经理');
    await userEvent.click(nodeCard);
    
    expect(screen.getByText('节点配置')).toBeVisible();
    expect(screen.getByLabelText('节点名称')).toBeVisible();
  });
  
  test('修改节点名称应更新节点配置', async () => {
    const onChange = jest.fn();
    render(<DingProcessDesigner initialModel={mockModel} onChange={onChange} />);
    
    const nodeCard = screen.getByText('部门经理');
    await userEvent.click(nodeCard);
    
    const nameInput = screen.getByLabelText('节点名称');
    await userEvent.clear(nameInput);
    await userEvent.type(nameInput, '财务主管');
    
    // 失焦触发保存
    await userEvent.tab();
    
    expect(onChange).toHaveBeenCalled();
    const updatedNode = onChange.mock.calls[onChange.mock.calls.length - 1][0].nodes.find(
      n => n.name === '财务主管'
    );
    expect(updatedNode).toBeDefined();
  });
  
  test('配置多级主管审批', async () => {
    const onChange = jest.fn();
    render(<DingProcessDesigner onChange={onChange} />);
    
    // 添加审批人节点
    const addButton = screen.getByRole('button', { name: /添加/i });
    await userEvent.click(addButton);
    await userEvent.click(screen.getByText('审批人'));
    
    // 选中节点
    const nodeCard = screen.getAllByText('审批人')[1]; // 第二个是节点卡片
    await userEvent.click(nodeCard);
    
    // 选择多级主管
    const approverTypeSelect = screen.getByLabelText('审批人类型');
    await userEvent.selectOptions(approverTypeSelect, 'MANAGER_CHAIN');
    
    // 设置最大层级
    const maxLevelsInput = screen.getByLabelText('最大审批层级');
    await userEvent.clear(maxLevelsInput);
    await userEvent.type(maxLevelsInput, '3');
    
    // 设置自动通过策略
    const autoApproveSelect = screen.getByLabelText('自动通过策略');
    await userEvent.selectOptions(autoApproveSelect, 'both');
    
    expect(onChange).toHaveBeenCalled();
    const updatedNode = onChange.mock.calls[onChange.mock.calls.length - 1][0].nodes[0];
    expect(updatedNode.config.approverType).toBe('MANAGER_CHAIN');
    expect(updatedNode.config.chainConfig?.maxLevels).toBe(3);
    expect(updatedNode.config.chainConfig?.autoApprove).toBe('both');
  });
  
  test('撤销/重做功能', async () => {
    const onChange = jest.fn();
    render(<DingProcessDesigner onChange={onChange} />);
    
    // 添加节点
    const addButton = screen.getByRole('button', { name: /添加/i });
    await userEvent.click(addButton);
    await userEvent.click(screen.getByText('审批人'));
    
    const nodesAfterAdd = onChange.mock.calls[onChange.mock.calls.length - 1][0].nodes;
    expect(nodesAfterAdd).toHaveLength(1);
    
    // 撤销
    const undoButton = screen.getByRole('button', { name: /撤销/i });
    await userEvent.click(undoButton);
    
    const nodesAfterUndo = onChange.mock.calls[onChange.mock.calls.length - 1][0].nodes;
    expect(nodesAfterUndo).toHaveLength(0);
    
    // 重做
    const redoButton = screen.getByRole('button', { name: /重做/i });
    await userEvent.click(redoButton);
    
    const nodesAfterRedo = onChange.mock.calls[onChange.mock.calls.length - 1][0].nodes;
    expect(nodesAfterRedo).toHaveLength(1);
  });
});
```

### 1.10 与 PRD 的差异

| 项目 | PRD 要求 | 实际实现 | 差异说明 |
|------|---------|---------|---------|
| 节点类型数量 | 7种 | ✅ 5种 | 实现了主要的5种（缺少开始/结束节点配置） |
| 审批人类型 | 7种 | ✅ 7种 | 完全一致 |
| 多人审批方式 | 3种 | ✅ 3种 | 会签、或签、依次审批 |
| 连续主管审批 | ✅ 需要 | ✅ 已实现 | 支持 maxLevels 和 autoApprove |
| 条件分支 | ✅ 需要 | ✅ 已实现 | 支持多条件配置 |
| 并行分支 | ✅ 需要 | ✅ 已实现 | 支持多分支并行 |
| 字段权限配置 | ✅ 需要 | ✅ 已实现 | editableFields, requiredFields, hiddenFields |
| 超时配置 | ✅ 需要 | ✅ 已实现 | hours + action |
| 节点连线 | 自动连接 | ✅ 自动连接 | 垂直布局，自动生成连线 |
| 流程验证 | ✅ 需要 | ⚠️ 基础验证 | 基础验证已实现，高级验证（如循环检测）缺失 |
| 流程模拟 | ✅ PRD提及 | ❌ 未实现 | 无法模拟流程运行 |

---

## 📄 2. 流程预览组件：LarkProcessPreview

> **文件**: `frontend/src/features/approval/designer/LarkProcessPreview.tsx`  
> **用途**: 飞书风格的流程只读预览

### 2.1 组件 API

```typescript
interface LarkProcessPreviewProps {
  model: ProcessModel;              // 流程定义
  showInitiator?: boolean;          // 是否显示发起人节点（默认 true）
  initiatorName?: string;           // 发起人名称（用于预览）
  showEndNode?: boolean;            // 是否显示结束节点（默认 true）
  compact?: boolean;                // 紧凑模式（默认 false）
  className?: string;
}
```

### 2.2 测试选择器

```typescript
// 预览容器
'.lark-process-preview'
'[data-testid="process-preview"]' // 推荐添加

// 发起人节点
'[data-node-type="initiator"]'

// 审批节点
'[data-node-type="USER_TASK"]'
'[data-node-type="EXECUTOR"]'
'[data-node-type="CC"]'

// 结束节点
'[data-node-type="end"]'
```

### 2.3 视觉风格

- 飞书风格：圆角、柔和色彩、清晰的层次
- 节点卡片：白色背景 + 彩色左边框
- 连接线：灰色虚线，垂直布局
- 紧凑模式：减小节点间距和内边距

---

## 📄 3. 其他组件（简略）

### 3.1 ApprovalFlowDiagram（运行时查看器）

**用途**: 查看流程实例的实时状态和历史记录  
**文件**: `frontend/src/features/approval/components/ApprovalFlowDiagram.tsx`

**主要功能**:
- 显示当前进行到哪个节点
- 显示已完成节点（绿色勾选）
- 显示待处理节点（黄色提示）
- 显示审批历史（时间线）

### 3.2 ApprovalActions（任务操作组件）

**用途**: 审批任务的操作按钮组  
**文件**: `frontend/src/features/approval/components/ApprovalActions.tsx`

**主要功能**:
- 通过（Approve）
- 驳回（Reject）
- 退回（Return）
- 转发（Forward）
- 加签（Add Sign）

### 3.3 ApprovalHistory（审批历史组件）

**用途**: 审批历史时间线展示  
**文件**: `frontend/src/features/approval/components/ApprovalHistory.tsx`

---

## ✅ 文档完成总结

### 已完成的组件规范

1. ✅ **DingProcessDesigner** - 钉钉风格流程设计器（核心组件，1600+行）
2. ✅ **DingStylePropertyPanel** - 节点属性面板（600+行）
3. ✅ **LarkProcessPreview** - 飞书风格流程预览（简略）
4. ✅ **ApprovalFlowDiagram** - 运行时查看器（简略）
5. ✅ **ApprovalActions** - 任务操作组件（简略）
6. ✅ **ApprovalHistory** - 审批历史组件（简略）

### 文档特点

- ✅ 基于**实际代码实现**（1,646行组件代码）
- ✅ 包含**详细的组件 API**
- ✅ 包含**测试选择器**
- ✅ 提供**完整的交互步骤**
- ✅ 详细的**状态管理说明**
- ✅ 提供**组件级别测试用例建议**
- ✅ 标注**与 PRD 的差异**（10项对比）

---

**文档创建日期**: 2025-12-25  
**最后更新**: 2026-01-06  
**维护者**: FFOA 开发团队  
**版本**: v1.1  
**状态**: ✅ 完成
