规则拖拽交互设计方案.md 18 KB

规则拖拽交互设计方案(工作流画布版)

1. 概述

本方案设计一种工作流画布式的规则配置交互,类似 n8n / Dify / 飞书自动化的节点连线模式。用户在画布上拖拽节点、连接边,可视化地构建数据提取规则。

1.1 核心概念

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  来源节点    │─────▶│  动作节点    │─────▶│  输出节点    │
│ (Source)    │      │ (Action)    │      │ (Element)   │
└─────────────┘      └─────────────┘      └─────────────┘
     附件              AI提取/引用           目标要素

1.2 设计目标

  • 可视化:节点+连线,一目了然的数据流向
  • 灵活:自由拖拽、连接、组合
  • 可扩展:易于添加新的节点类型

2. 节点类型定义

2.1 来源节点 (Source Node)

从左侧面板拖入画布,代表数据来源。

节点类型 图标 说明 配置项
原文 📄 项目原文档 locatorType: 全文/章节/段落
附件 📎 已上传的附件 attachmentId, entryPath
摘选 ✂️ 手动框选的文本片段 sourceText, locator

2.2 动作节点 (Action Node)

处理数据的中间节点。

节点类型 图标 颜色 说明 配置项
引用 📌 绿色 直接使用来源文本
AI 总结 橙色 AI 总结内容 prompt, maxLength
AI 提取 🔍 蓝色 AI 提取特定信息 prompt, format
表格提取 📊 灰色 从表格提取数据 tableSelector, column

2.3 输出节点 (Element Node)

从右侧要素面板拖入,代表目标要素。

节点类型 图标 说明
要素 🏷️ 目标要素,如 basicInfo.projectCode

3. 画布布局设计

3.1 整体布局

┌─────────────────────────────────────────────────────────────────────────┐
│  [保存] [运行全部] [清空]                              [缩放] [适应] [网格] │
├────────────┬────────────────────────────────────────┬───────────────────┤
│            │                                        │                   │
│  节点面板   │              工作流画布                 │    属性面板        │
│            │                                        │                   │
│ ┌────────┐ │   ┌──────┐      ┌──────┐      ┌─────┐ │  ┌─────────────┐  │
│ │ 来源   │ │   │ 附件1 │─────▶│AI提取│─────▶│要素1│ │  │ 节点属性     │  │
│ │ ├ 原文 │ │   └──────┘      └──────┘      └─────┘ │  │             │  │
│ │ ├ 附件 │ │                                       │  │ 名称: ...   │  │
│ │ └ 摘选 │ │   ┌──────┐      ┌──────┐      ┌─────┐ │  │ 类型: ...   │  │
│ ├────────┤ │   │ 附件2 │─────▶│ 引用 │─────▶│要素2│ │  │ 配置: ...   │  │
│ │ 动作   │ │   └──────┘      └──────┘      └─────┘ │  │             │  │
│ │ ├ 引用 │ │                                       │  └─────────────┘  │
│ │ ├ 总结 │ │   ┌──────┐                    ┌─────┐ │                   │
│ │ ├ 提取 │ │   │ 原文  │───────────────────▶│要素3│ │  ┌─────────────┐  │
│ │ └ 表格 │ │   └──────┘                    └─────┘ │  │ 连线属性     │  │
│ ├────────┤ │                                       │  │             │  │
│ │ 要素   │ │          (可缩放、平移的画布)           │  │ 来源: ...   │  │
│ │ ├ 项目 │ │                                       │  │ 目标: ...   │  │
│ │ ├ 评审 │ │                                       │  └─────────────┘  │
│ │ └ ... │ │                                       │                   │
│ └────────┘ │                                       │                   │
│            │                                        │                   │
└────────────┴────────────────────────────────────────┴───────────────────┘

3.2 面板说明

面板 宽度 功能
节点面板 200px 可拖拽的节点列表,分组显示
工作流画布 自适应 节点拖放、连线、缩放、平移
属性面板 280px 选中节点/边的配置表单

4. 交互流程

4.1 创建规则流程

1. 从节点面板拖拽「附件」节点到画布
   └─ 弹出附件选择器,选择具体附件
   
2. 从节点面板拖拽「AI提取」节点到画布
   └─ 右侧属性面板显示配置表单
   
3. 从节点面板拖拽「要素」节点到画布
   └─ 弹出要素选择器,选择目标要素
   
4. 连接节点:从「附件」输出端口拖线到「AI提取」输入端口
   
5. 连接节点:从「AI提取」输出端口拖线到「要素」输入端口
   
6. 配置「AI提取」节点:在属性面板填写 prompt
   
7. 点击「保存」,生成规则

4.2 快捷操作

操作 快捷键 说明
删除节点/边 Delete / Backspace 删除选中项
全选 Ctrl+A 选中所有节点
撤销 Ctrl+Z 撤销上一步
重做 Ctrl+Shift+Z 重做
缩放 Ctrl+滚轮 缩放画布
平移 空格+拖拽 平移画布
复制 Ctrl+C 复制选中节点
粘贴 Ctrl+V 粘贴节点

4.3 连线规则

来源节点 ──▶ 动作节点 ──▶ 输出节点
   │              │            │
   │              │            └── 只能接收,不能输出
   │              └── 可接收多个来源,输出到一个要素
   └── 只能输出,不能接收

简化模式:来源节点 ──▶ 输出节点(默认使用「引用」动作)

5. 节点数据结构

5.1 画布数据模型

interface WorkflowData {
  nodes: WorkflowNode[]
  edges: WorkflowEdge[]
}

interface WorkflowNode {
  id: string
  type: 'source' | 'action' | 'element'
  subType: string          // 'document' | 'attachment' | 'quote' | 'ai_extract' | ...
  position: { x: number, y: number }
  data: {
    label: string
    // 来源节点
    sourceNodeId?: number
    sourceName?: string
    sourceText?: string
    locator?: { type: string, value: string }
    // 动作节点
    actionType?: string
    prompt?: string
    // 输出节点
    elementKey?: string
    elementName?: string
  }
}

interface WorkflowEdge {
  id: string
  source: string           // 源节点 ID
  target: string           // 目标节点 ID
  sourceHandle?: string    // 源端口
  targetHandle?: string    // 目标端口
}

5.2 转换为后端 RuleCreateDTO

function workflowToRules(workflow: WorkflowData): RuleCreateDTO[] {
  const rules = []
  
  // 找到所有输出节点(要素)
  const elementNodes = workflow.nodes.filter(n => n.type === 'element')
  
  for (const elementNode of elementNodes) {
    // 反向追溯连接链
    const chain = traceInputChain(workflow, elementNode.id)
    
    // 构建规则
    const rule: RuleCreateDTO = {
      elementKey: elementNode.data.elementKey,
      ruleName: `${elementNode.data.elementName}-自动规则`,
      ruleType: 'extraction',
      actionType: chain.actionNode?.data.actionType || 'quote',
      actionConfig: JSON.stringify({
        prompt: chain.actionNode?.data.prompt,
        locatorType: chain.sourceNode?.data.locator?.type || 'full_text'
      }),
      inputs: [{
        sourceNodeId: chain.sourceNode?.data.sourceNodeId,
        inputType: chain.sourceNode?.subType,
        inputName: chain.sourceNode?.data.sourceName,
        sourceText: chain.sourceNode?.data.sourceText
      }]
    }
    
    rules.push(rule)
  }
  
  return rules
}

6. 技术选型

6.1 画布库选择

优点 缺点 推荐度
Vue Flow Vue 3 原生、轻量、API 简洁 社区较小 ⭐⭐⭐⭐⭐
X6 (AntV) 功能强大、文档完善 体积大、学习曲线陡 ⭐⭐⭐⭐
LogicFlow 滴滴开源、流程图专用 Vue 3 支持一般 ⭐⭐⭐
原生实现 完全可控 开发成本高 ⭐⭐

推荐:Vue Flow - 轻量、Vue 3 友好、满足需求

npm install @vue-flow/core @vue-flow/background @vue-flow/controls

6.2 组件结构

src/
├── components/
│   └── workflow/
│       ├── RuleWorkflow.vue        # 主画布组件
│       ├── nodes/
│       │   ├── SourceNode.vue      # 来源节点
│       │   ├── ActionNode.vue      # 动作节点
│       │   └── ElementNode.vue     # 输出节点
│       ├── panels/
│       │   ├── NodePanel.vue       # 左侧节点面板
│       │   └── PropertyPanel.vue   # 右侧属性面板
│       └── hooks/
│           ├── useWorkflow.js      # 画布逻辑
│           └── useRuleConvert.js   # 规则转换
├── stores/
│   └── workflow.js                 # 画布状态管理

7. 节点组件设计

7.1 来源节点 (SourceNode.vue)

<template>
  <div class="workflow-node source-node" :class="subType">
    <div class="node-icon">{{ icon }}</div>
    <div class="node-content">
      <div class="node-label">{{ data.label }}</div>
      <div class="node-desc" v-if="data.sourceName">{{ data.sourceName }}</div>
    </div>
    <!-- 输出端口 -->
    <Handle type="source" position="right" />
  </div>
</template>

<style scoped>
.source-node {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 8px;
  padding: 12px 16px;
  min-width: 140px;
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.source-node.document { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
.source-node.attachment { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.source-node.excerpt { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
</style>

7.2 动作节点 (ActionNode.vue)

<template>
  <div class="workflow-node action-node" :class="data.actionType">
    <!-- 输入端口 -->
    <Handle type="target" position="left" />
    
    <div class="node-icon">{{ icon }}</div>
    <div class="node-content">
      <div class="node-label">{{ actionLabel }}</div>
      <div class="node-config" v-if="data.prompt">
        {{ data.prompt.slice(0, 20) }}...
      </div>
    </div>
    
    <!-- 输出端口 -->
    <Handle type="source" position="right" />
  </div>
</template>

<style scoped>
.action-node {
  background: white;
  border: 2px solid #ddd;
  border-radius: 8px;
  padding: 12px 16px;
  min-width: 120px;
}
.action-node.quote { border-color: #67c23a; }
.action-node.summary { border-color: #e6a23c; }
.action-node.ai_extract { border-color: #409eff; }
.action-node.table_extract { border-color: #909399; }
</style>

7.3 输出节点 (ElementNode.vue)

<template>
  <div class="workflow-node element-node">
    <!-- 输入端口 -->
    <Handle type="target" position="left" />
    
    <div class="node-icon">🏷️</div>
    <div class="node-content">
      <div class="node-label">{{ data.elementName }}</div>
      <div class="node-key">{{ data.elementKey }}</div>
    </div>
  </div>
</template>

<style scoped>
.element-node {
  background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
  color: white;
  border-radius: 8px;
  padding: 12px 16px;
  min-width: 140px;
  box-shadow: 0 4px 12px rgba(250, 112, 154, 0.3);
}
</style>

8. 属性面板设计

8.1 来源节点属性

<template>
  <div class="property-panel">
    <h3>来源配置</h3>
    
    <el-form label-position="top" size="small">
      <el-form-item label="来源类型">
        <el-tag>{{ sourceTypeLabel }}</el-tag>
      </el-form-item>
      
      <el-form-item label="选择附件" v-if="node.subType === 'attachment'">
        <el-select v-model="node.data.sourceNodeId" @change="onAttachmentChange">
          <el-option 
            v-for="att in attachments" 
            :key="att.id" 
            :label="att.fileName" 
            :value="att.id" 
          />
        </el-select>
      </el-form-item>
      
      <el-form-item label="内容定位">
        <el-select v-model="node.data.locator.type">
          <el-option label="全文" value="full_text" />
          <el-option label="章节" value="chapter" />
          <el-option label="评审代码" value="review_code" />
        </el-select>
      </el-form-item>
      
      <el-form-item label="章节标题" v-if="node.data.locator.type === 'chapter'">
        <el-input v-model="node.data.locator.value" placeholder="如:一、工作目的" />
      </el-form-item>
    </el-form>
  </div>
</template>

8.2 动作节点属性

<template>
  <div class="property-panel">
    <h3>动作配置</h3>
    
    <el-form label-position="top" size="small">
      <el-form-item label="动作类型">
        <el-radio-group v-model="node.data.actionType">
          <el-radio-button value="quote">引用</el-radio-button>
          <el-radio-button value="summary">总结</el-radio-button>
          <el-radio-button value="ai_extract">提取</el-radio-button>
        </el-radio-group>
      </el-form-item>
      
      <el-form-item 
        label="AI 提示词" 
        v-if="['summary', 'ai_extract'].includes(node.data.actionType)"
      >
        <el-input 
          v-model="node.data.prompt" 
          type="textarea" 
          :rows="4"
          placeholder="请输入 AI 处理提示词..."
        />
      </el-form-item>
      
      <el-form-item label="输出格式" v-if="node.data.actionType === 'ai_extract'">
        <el-select v-model="node.data.format">
          <el-option label="文本" value="text" />
          <el-option label="日期" value="date" />
          <el-option label="数字" value="number" />
          <el-option label="列表" value="list" />
        </el-select>
      </el-form-item>
    </el-form>
  </div>
</template>

9. 实现计划

Phase 1: 画布基础 (3天)

  • 安装 Vue Flow
  • 创建 RuleWorkflow.vue 主组件
  • 实现三种节点组件
  • 实现节点拖放到画布
  • 实现节点连线

Phase 2: 面板交互 (2天)

  • 左侧节点面板(分组、搜索)
  • 右侧属性面板(动态表单)
  • 节点选中/编辑状态

Phase 3: 规则转换 (2天)

  • 画布数据 → RuleCreateDTO 转换
  • 保存规则到后端
  • 加载已有规则到画布

Phase 4: 体验优化 (2天)

  • 快捷键支持
  • 撤销/重做
  • 小地图
  • 自动布局

10. 与现有系统集成

10.1 入口位置

Editor.vue 中添加「规则工作流」入口:

<!-- 规则管理弹窗改为全屏工作流 -->
<el-dialog
  v-model="showRuleWorkflow"
  title="规则工作流"
  fullscreen
  :close-on-click-modal="false"
>
  <RuleWorkflow 
    :project-id="currentProjectId"
    :attachments="attachments"
    :elements="elements"
    @save="handleWorkflowSave"
  />
</el-dialog>

10.2 数据同步

  • 画布保存时,批量创建/更新规则
  • 规则列表与画布双向同步
  • 支持从现有规则反向生成画布

11. 附录:Vue Flow 示例

<script setup>
import { ref } from 'vue'
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'

import SourceNode from './nodes/SourceNode.vue'
import ActionNode from './nodes/ActionNode.vue'
import ElementNode from './nodes/ElementNode.vue'

const nodeTypes = {
  source: SourceNode,
  action: ActionNode,
  element: ElementNode
}

const nodes = ref([])
const edges = ref([])

const { onConnect, addEdges } = useVueFlow()

onConnect((params) => {
  addEdges([params])
})

function onDrop(event) {
  const type = event.dataTransfer.getData('application/vueflow')
  const position = { x: event.clientX, y: event.clientY }
  
  nodes.value.push({
    id: `node-${Date.now()}`,
    type,
    position,
    data: { label: type }
  })
}
</script>

<template>
  <VueFlow 
    :nodes="nodes" 
    :edges="edges"
    :node-types="nodeTypes"
    @drop="onDrop"
    @dragover.prevent
  >
    <Background />
    <Controls />
  </VueFlow>
</template>