epixa-canvas
v1.0.4
Published
基于 Vue Flow 的无限画布工作流 SDK,支持 AI 文本/图片/视频生成节点编排
Maintainers
Readme
无限画布集成指南 (Infinite Canvas Integration Guide)
本指南旨在帮助您将此 无限画布 (WorkflowCanvas) 快速集成到您的 Vue 3 业务项目中,并完成画布数据的持久化(保存、历史记录列表选择与读取、删除)。
📦 1. 快速上手
步骤 0:安装 SDK 包
在您的项目中安装本无限画布 SDK:
# 使用 pnpm
pnpm add epixa-canvas
# 使用 npm
npm install epixa-canvas步骤 A:安装核心对等依赖
我们的画布核心基于 @vue-flow 开发,请确保您的项目安装了以下必需依赖:
# 使用 pnpm 安装
pnpm add @vue-flow/core @vue-flow/background @vue-flow/controls @vue-flow/minimap @vue-flow/node-resizer lucide-vue-next pinia
# 或者使用 npm 安装
npm install @vue-flow/core @vue-flow/background @vue-flow/controls @vue-flow/minimap @vue-flow/node-resizer lucide-vue-next pinia --save步骤 B:引入组件与样式
在您的业务页面中直接导入画布组件:
<template>
<div class="canvas-container">
<WorkflowCanvas
:initial-title="canvasData.title"
:initial-nodes="canvasData.nodes"
:initial-edges="canvasData.edges"
:initial-viewport="canvasData.viewport"
:history-list="historyList"
:current-history-id="currentCanvasId"
:api-config="apiConfig"
@save="handleSave"
@load-history="handleLoadHistory"
@delete-history="handleDeleteHistory"
@workflow-change="handleWorkflowChange"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { WorkflowCanvas } from 'epixa-canvas'
// 注意:必须显式导入库的样式文件
import 'epixa-canvas/dist/style.css'
// 1. 配置 AI 接口与鉴权(解耦硬编码后,由宿主系统动态注入)
const apiConfig = {
baseUrl: 'https://api.yourdomain.com/v1',
cdnUrl: 'https://cdn.yourdomain.com',
headers: {
'Authorization': 'Bearer YOUR_USER_TOKEN' // 宿主系统中当前登录用户的鉴权 Token
}
}
// 2. 当前激活的画布状态与 ID
const currentCanvasId = ref(123)
const canvasData = ref({
title: '我的新工作流画布',
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 }
})
// 3. 传入画布的历史记录列表数据
const historyList = ref([])
// 初始化加载数据
onMounted(async () => {
await fetchCanvasDetail(currentCanvasId.value)
await fetchHistoryList()
})
// 从后端获取当前画布详情并渲染
const fetchCanvasDetail = async (id) => {
const response = await fetch(`/api/canvas/detail?id=${id}`)
const result = await response.json()
if (result.success) {
currentCanvasId.value = result.data.id
canvasData.value = {
title: result.data.title,
nodes: result.data.canvasData.nodes || [],
edges: result.data.canvasData.edges || [],
viewport: result.data.canvasData.viewport || { x: 0, y: 0, zoom: 1 }
}
}
}
// 获取已保存画布的历史记录列表
const fetchHistoryList = async () => {
const response = await fetch('/api/canvas/history-list')
const result = await response.json()
if (result.success) {
historyList.value = result.data.list // 数组,每个元素包含 id, title, updatedAt, nodes, edges, viewport
}
}
// 监听保存事件(命名后触发)
const handleSave = async (data) => {
console.log('保存画布:', data)
// data 结构: { title, nodes, edges, viewport }
const response = await fetch('/api/canvas/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: currentCanvasId.value,
title: data.title,
canvas_data: {
nodes: data.nodes,
edges: data.edges,
viewport: data.viewport
}
})
})
if (response.ok) {
alert('保存成功!')
await fetchHistoryList() // 刷新历史列表
}
}
// 监听用户在“历史记录列表”中选中某一项的加载事件
const handleLoadHistory = (item) => {
console.log('加载选中的历史版本:', item)
currentCanvasId.value = item.id
canvasData.value = {
title: item.title,
nodes: item.nodes,
edges: item.edges,
viewport: item.viewport
}
}
// 监听用户删除某一条历史记录的事件
const handleDeleteHistory = async (id) => {
const response = await fetch(`/api/canvas/delete-history?id=${id}`, {
method: 'DELETE'
})
if (response.ok) {
await fetchHistoryList() // 刷新历史列表
}
}
// 监听画布实时变动(用于同步草稿等)
const handleWorkflowChange = (graph) => {
console.log('画布内容实时变动:', graph)
}
</script>
<style scoped>
.canvas-container {
width: 100vw;
height: 100vh;
background-color: #000;
}
</style>🔌 2. API 参考 (Props & Events)
Props (输入属性)
| 属性名 | 类型 | 是否必传 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- | :--- |
| initial-title | string | 否 | '未命名画布' | 初始载入的画布标题名称 |
| initial-nodes | WorkflowNode[] | 否 | [] | 初始载入的节点列表 |
| initial-edges | WorkflowEdge[] | 否 | [] | 初始载入的连接线列表 |
| initial-viewport | { x: number, y: number, zoom: number } | 否 | { x: 0, y: 0, zoom: 1 } | 还原用户上次离开时的视口缩放与位移位置 |
| history-list | SavedCanvas[] | 否 | [] | 在“历史弹窗”中显示的历史画布记录列表 |
| current-history-id | string \| number | 否 | - | 当前激活/载入的历史画布 ID,激活的画布在列表中会有绿色勾选标识 |
| readonly | boolean | 否 | false | 只读模式,开启后用户无法编辑画布 |
| options | WorkflowOptions | 否 | - | 画布高级全局配置项(如支持的 AI 模型列表等) |
| api-config | ApiConfig | 否 | - | 配置 AI 接口的 baseUrl、鉴权 headers 及 cdnUrl,详细数据格式参考第 5 节 |
Events (输出事件)
| 事件名 | 参数格式 | 说明 |
| :--- | :--- | :--- |
| @save | (data: { title: string, nodes: any[], edges: any[], viewport: any }) => void | 用户点击“保存”按钮或使用快捷键 Ctrl+S / Cmd+S,并在弹窗确认命名后触发 |
| @load-history | (item: SavedCanvas) => void | 用户在历史弹窗中选择某项历史画布时触发,宿主项目应拦截并更新传入画布的数据 |
| @delete-history | (id: string \| number) => void | 用户在历史弹窗点击“删除”按钮并二次确认后触发,宿主项目应调用后端接口删除该记录 |
| @workflow-change | (data: { nodes: any[], edges: any[] }) => void | 画布节点、连线发生变化时实时触发的草稿同步事件(自带防抖合并) |
| @node-click | (node: WorkflowNode) => void | 点击单个节点时触发 |
| @node-add | (node: WorkflowNode) => void | 新增节点时触发 |
| @node-delete | (nodeId: string) => void | 删除节点时触发 |
🗄️ 3. 数据保存与数据库字段设计建议
1. 历史画布数据结构 (SavedCanvas JSON)
history-list 数组里的每个历史元素,应遵循如下结构:
interface SavedCanvas {
id: string | number // 历史记录唯一 ID
title: string // 画布重命名标题
updatedAt: string // 格式化好的修改日期,例如 "2026-05-29 10:00:23"
nodes: any[] // 对应版本的节点数据
edges: any[] // 对应版本的连线数据
viewport: { // 对应版本的视口配置
x: number
y: number
zoom: number
}
}2. 推荐数据库表结构设计 (后端)
若您支持用户保存多份画布,或者希望支持“历史快照版本归档”,推荐设计为以下关联关系:
-- 用户工作流画布主表
CREATE TABLE `user_workflows` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '画布ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`title` varchar(128) DEFAULT '未命名画布' COMMENT '画布当前标题',
`canvas_data` json DEFAULT NULL COMMENT '当前最新状态的 JSON 数据 (nodes, edges, viewport)',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作流画布主表';
-- 画布历史版本记录表 (用于提供给 history-list)
CREATE TABLE `workflow_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '历史记录ID',
`workflow_id` bigint(20) NOT NULL COMMENT '关联主表 ID',
`title` varchar(128) DEFAULT '历史版本' COMMENT '保存时的版本标题',
`canvas_data` json DEFAULT NULL COMMENT '历史状态数据 (nodes, edges, viewport)',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '保存时间',
PRIMARY KEY (`id`),
KEY `idx_workflow` (`workflow_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='画布历史快照表';💡 4. 集成注意事项与最佳实践
- 容器高度问题:
WorkflowCanvas必须被包裹在一个具有明确宽度和高度的容器中(例如width: 100%; height: 100vh;),否则画布的 SVG 视口可能会折叠为0px导致组件不可见。 - Pinia 共享机制:若您的宿主项目已经实例化了全局 Pinia,画布将自动共享此 Pinia 实例。若您的项目未使用 Pinia,需要在入口文件初始化以避免运行时警告:
import { createPinia } from 'pinia' app.use(createPinia()) - Tailwind CSS 兼容性:画布组件内部自带了完备的磨砂玻璃(glassmorphism)与夜间模式样式。若您的项目未使用 Tailwind,无需担心样式冲突,我们的 CSS 打包结果为独立的样式文件。
📡 5. AI 模型与服务对接接口规范
为了使画布的 AI 功能正常运行,集成者需要在 apiConfig 属性中配置后端的 baseUrl、请求头 headers 以及 endpoints 路径。
同时,集成的后端接口必须严格遵循以下定义的数据格式。
0. API 配置注入参数定义 (apiConfig)
interface ApiConfig {
baseUrl: string // 接口请求基地址,如 'https://api.yourdomain.com/v1'
headers?: Record<string, string> // 自定义请求头(如 Authorization: Bearer token)
cdnUrl?: string // CDN 资源域名(若返回的地址是相对路径,组件会自动拼接此域名前缀)
endpoints?: {
models?: string // 获取模型列表,默认值为 '/models'
upload?: string // 文件上传,默认值为 '/upload'
imageTask?: string // 创建生图任务,默认值为 '/tasks/image'
videoTask?: string // 创建生视频任务,默认值为 '/tasks/video'
taskPoll?: string // 任务状态轮询前缀,默认值为 '/tasks'
chatMessage?: string // 文本模型流式对话,默认值为 '/chats/messages'
saveCanvas?: string // 保存画布,默认值为 '/save'
listHistory?: string // 查询历史记录列表,默认值为 '/history_list'
deleteHistory?: string // 删除历史记录,默认值为 '/delete_history'
}
}1. 获取模型列表 (GET /models)
用于在画布挂载时及节点配置中,渲染可供用户选择的大模型列表。
- 请求方式:
GET - 响应格式 (Response):
{
"records": [
{
"id": "gpt-4o",
"name": "GPT-4o",
"type": "text",
"status": "active"
},
{
"id": "sdxl-turbo",
"name": "SDXL Turbo",
"type": "image",
"status": "active",
"supported_aspect_ratios": ["1:1", "16:9", "4:3"]
},
{
"id": "luma-dream-machine",
"name": "Luma Dream Machine",
"type": "video",
"status": "active"
}
]
}- 字段说明:
type:必须是'text'|'image'|'video'之一,画布会依据此字段将模型归类显示在不同的节点类型中。supported_aspect_ratios:仅对image/video模型生效,可选,用于限制该模型支持的生成宽高比例。
2. 文件上传 (POST /upload)
当用户在图片节点或视频节点双击上传本地素材时触发。
- 请求方式:
POST - 请求格式:
multipart/form-data - 请求体 (Request Body):
file:File 二进制对象
- 响应格式 (Response):
{
"url": "https://cdn.yourdomain.com/uploads/20260529/abc123xyz.jpg"
}- 字段说明:
- 返回的
url应当是可直接公网访问的图片或视频 file 地址。
- 返回的
3. 大语言模型流式对话 (POST /chats/messages)
在文本节点上点击“运行”时,流式获取 AI 文本回复。
- 请求方式:
POST - 请求参数 (Query):
?model={modelId} - 请求体 (Request Body):
{
"content": "用户当前节点输入的提示词,包含连线至本节点的所有上游文本",
"image_urls": [
"data:image/jpeg;base64,xxxx...",
"https://cdn.yourdomain.com/image.jpg"
],
"session_id": "xxxx-session-id-xxxx"
}- 请求头 (Headers):
Accept: text/event-stream
- 流式响应规范 (Server-Sent Events):
后端接口需要启用分块流式传输 (
text/event-stream),支持返回两种类型的事件数据:- 对话上下文 Session ID 传输(可选,作为流的第一帧或结束帧):
如果需要支持多轮对话上下文,请在响应的
Headers中附带X-Chat-Session-ID: <session_id>,或者在流中输出一条 JSON 元数据:data: {"session_id": "new-session-id-123"} - 文本内容传输:
以data: 欢 data: 迎 data: 使用 data: [DONE]data: [DONE]标识流的正常结束。
- 对话上下文 Session ID 传输(可选,作为流的第一帧或结束帧):
如果需要支持多轮对话上下文,请在响应的
4. 创建图片/视频生成任务 (POST /tasks/image 或 POST /tasks/video)
当用户在图片或视频生成节点点击“运行”时,由于生成时间长,采用异步任务制提交。
- 请求方式:
POST - 请求体 (Request Body):
{
"type": "image",
"model": "sdxl-turbo",
"prompt": "生成的提示词内容,包含上游相连的所有文本节点数据",
"aspect_ratio": "16:9",
"quality": "standard",
"image_urls": [
"data:image/jpeg;base64,xxxx...",
"https://cdn.yourdomain.com/image.jpg"
],
// 以下字段仅在 type = "video" 时传递
"duration": 5,
"generate_audio": true,
"ref_images": [], // 视频参考图列表
"head_image": null, // 视频首帧图(若采用首尾帧模式)
"tail_image": null // 视频尾帧图(若采用首尾帧模式)
}- 响应格式 (Response):
{
"id": "task_9876543210"
}- 字段说明:
- 请求体中
image_urls/ref_images/head_image的内容,若用户连线了包含本地 blob 图片的节点,组件会在请求前自动压缩并将其转换为Base64格式。
- 请求体中
5. 轮询异步任务状态 (GET /tasks/{id})
在任务提交成功并拿到 id 后,画布组件将以 2 秒为间隔对该任务状态进行轮询。
请求方式:
GET请求路径:
/tasks/{id}(例如/tasks/task_9876543210)响应格式 (Response):
- 生成中/排队中:
{ "status": "PROCESSING", "progress": 45 } - 执行失败:
{ "status": "FAILED", "error_message": "生图接口调用超时" } - 执行成功:
{ "status": "SUCCESS", "completed_at": "2026-05-29T10:15:30Z", "prompt": "提示词内容", "model": "sdxl-turbo", "params": { "quality": "standard", "aspect_ratio": "16:9" }, "result": { "videos": [ { "video_url": "/uploads/video_output.mp4", "cover_url": "/uploads/video_cover.jpg" } ], "url": "/uploads/image_output.jpg" } }
- 生成中/排队中:
状态值及关键字段规范:
status可选值为:QUEUED|PROCESSING(进行中),FAILED|FAILURE(失败),SUCCESS(成功)。- 若执行成功且返回的
video_url、cover_url或url为以/开头的相对路径,组件会利用配置的cdnUrl自动补全为公网绝对路径,例如https://cdn.yourdomain.com/uploads/image_output.jpg。
6. 保存画布 (POST /save)
当用户在页面上点击“保存”按钮或使用快捷键时触发。如果在 apiConfig 中配置了 baseUrl,组件内部将优先调用该接口。
- 请求方式:
POST - 请求体 (Request Body):
{
"id": 123,
"title": "我的工作流画布",
"canvas_data": {
"nodes": [],
"edges": [],
"viewport": { "x": 0, "y": 0, "zoom": 1 }
}
}- 响应格式 (Response):
{
"success": true,
"id": 12345
}- 字段说明:
id:当前画布的 ID(可选),如果不传则由后端创建并返回新的历史画布 ID。
7. 获取已保存画布的历史记录列表 (GET /history_list)
当打开历史画布面板时调用,用于展现可恢复的快照列表。
- 请求方式:
GET - 响应格式 (Response):
{
"records": [
{
"id": 123,
"title": "版本 1",
"updatedAt": "2026-05-29 10:00:23",
"canvas_data": {
"nodes": [],
"edges": [],
"viewport": { "x": 0, "y": 0, "zoom": 1 }
}
}
]
}- 说明:
- 组件会自动兼容平铺结构(如
nodes,edges,viewport直接位于元素最外层)或嵌套结构(嵌套在canvas_data属性内)。
- 组件会自动兼容平铺结构(如
8. 删除已保存画布的历史记录 (DELETE /delete_history)
当用户在历史版本弹窗中点击删除快照时触发。
- 请求方式:
DELETE - 请求路径:
/delete_history?id={id} - 响应格式 (Response):
{
"success": true
}