本方案设计一种工作流画布式的规则配置交互,类似 n8n / Dify / 飞书自动化的节点连线模式。用户在画布上拖拽节点、连接边,可视化地构建数据提取规则。
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 来源节点 │─────▶│ 动作节点 │─────▶│ 输出节点 │
│ (Source) │ │ (Action) │ │ (Element) │
└─────────────┘ └─────────────┘ └─────────────┘
附件 AI提取/引用 目标要素
从左侧面板拖入画布,代表数据来源。
| 节点类型 | 图标 | 说明 | 配置项 |
|---|---|---|---|
| 原文 | 📄 | 项目原文档 | locatorType: 全文/章节/段落 |
| 附件 | 📎 | 已上传的附件 | attachmentId, entryPath |
| 摘选 | ✂️ | 手动框选的文本片段 | sourceText, locator |
处理数据的中间节点。
| 节点类型 | 图标 | 颜色 | 说明 | 配置项 |
|---|---|---|---|---|
| 引用 | 📌 | 绿色 | 直接使用来源文本 | 无 |
| AI 总结 | ✨ | 橙色 | AI 总结内容 | prompt, maxLength |
| AI 提取 | 🔍 | 蓝色 | AI 提取特定信息 | prompt, format |
| 表格提取 | 📊 | 灰色 | 从表格提取数据 | tableSelector, column |
从右侧要素面板拖入,代表目标要素。
| 节点类型 | 图标 | 说明 |
|---|---|---|
| 要素 | 🏷️ | 目标要素,如 basicInfo.projectCode |
┌─────────────────────────────────────────────────────────────────────────┐
│ [保存] [运行全部] [清空] [缩放] [适应] [网格] │
├────────────┬────────────────────────────────────────┬───────────────────┤
│ │ │ │
│ 节点面板 │ 工作流画布 │ 属性面板 │
│ │ │ │
│ ┌────────┐ │ ┌──────┐ ┌──────┐ ┌─────┐ │ ┌─────────────┐ │
│ │ 来源 │ │ │ 附件1 │─────▶│AI提取│─────▶│要素1│ │ │ 节点属性 │ │
│ │ ├ 原文 │ │ └──────┘ └──────┘ └─────┘ │ │ │ │
│ │ ├ 附件 │ │ │ │ 名称: ... │ │
│ │ └ 摘选 │ │ ┌──────┐ ┌──────┐ ┌─────┐ │ │ 类型: ... │ │
│ ├────────┤ │ │ 附件2 │─────▶│ 引用 │─────▶│要素2│ │ │ 配置: ... │ │
│ │ 动作 │ │ └──────┘ └──────┘ └─────┘ │ │ │ │
│ │ ├ 引用 │ │ │ └─────────────┘ │
│ │ ├ 总结 │ │ ┌──────┐ ┌─────┐ │ │
│ │ ├ 提取 │ │ │ 原文 │───────────────────▶│要素3│ │ ┌─────────────┐ │
│ │ └ 表格 │ │ └──────┘ └─────┘ │ │ 连线属性 │ │
│ ├────────┤ │ │ │ │ │
│ │ 要素 │ │ (可缩放、平移的画布) │ │ 来源: ... │ │
│ │ ├ 项目 │ │ │ │ 目标: ... │ │
│ │ ├ 评审 │ │ │ └─────────────┘ │
│ │ └ ... │ │ │ │
│ └────────┘ │ │ │
│ │ │ │
└────────────┴────────────────────────────────────────┴───────────────────┘
| 面板 | 宽度 | 功能 |
|---|---|---|
| 节点面板 | 200px | 可拖拽的节点列表,分组显示 |
| 工作流画布 | 自适应 | 节点拖放、连线、缩放、平移 |
| 属性面板 | 280px | 选中节点/边的配置表单 |
1. 从节点面板拖拽「附件」节点到画布
└─ 弹出附件选择器,选择具体附件
2. 从节点面板拖拽「AI提取」节点到画布
└─ 右侧属性面板显示配置表单
3. 从节点面板拖拽「要素」节点到画布
└─ 弹出要素选择器,选择目标要素
4. 连接节点:从「附件」输出端口拖线到「AI提取」输入端口
5. 连接节点:从「AI提取」输出端口拖线到「要素」输入端口
6. 配置「AI提取」节点:在属性面板填写 prompt
7. 点击「保存」,生成规则
| 操作 | 快捷键 | 说明 |
|---|---|---|
| 删除节点/边 | Delete / Backspace |
删除选中项 |
| 全选 | Ctrl+A |
选中所有节点 |
| 撤销 | Ctrl+Z |
撤销上一步 |
| 重做 | Ctrl+Shift+Z |
重做 |
| 缩放 | Ctrl+滚轮 |
缩放画布 |
| 平移 | 空格+拖拽 |
平移画布 |
| 复制 | Ctrl+C |
复制选中节点 |
| 粘贴 | Ctrl+V |
粘贴节点 |
来源节点 ──▶ 动作节点 ──▶ 输出节点
│ │ │
│ │ └── 只能接收,不能输出
│ └── 可接收多个来源,输出到一个要素
└── 只能输出,不能接收
简化模式:来源节点 ──▶ 输出节点(默认使用「引用」动作)
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 // 目标端口
}
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
}
| 库 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 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
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 # 画布状态管理
<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>
<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>
<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>
<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>
<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>
RuleWorkflow.vue 主组件在 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>
<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>