最近在玩工作流, 从n8n到coze,发现coze更符合现在的我学,发现它真的能把“上传照片 → 选择风格 → 生成一张新图片”这件事变得特别轻量。只用前端一个单文件组件,就能把整个链路跑通。我把它做成一个生成冰球队员形象的小工具,本文就从这个项目的 App.vue 出发,逐行拆解源码,然后配合 Coze 工作流图解,带你还原「图片上传 → 工作流调用 → 生成结果返回」的全过程。
完整项目链接:gitee.com/hong-strong…
提示:文中的 token 已脱敏,请替换为你自己的扣子 PAT(个人访问令牌)。
一、项目总览:我们做了什么?
页面上半部分是参数区:选择照片、队服编号、颜色、位置、持杆手、生成风格。
下半部分是结果区:先出预览图,再展示生成后的图片。
整个交互流程只有三步:
- 选照片,页面上立刻出现本地预览
- 点击“生成”按钮,先把图片上传到 Coze 的文件服务,拿到
file_id - 用
file_id和其他参数调 Coze 工作流,等它跑完返回图片 URL 并渲染
下面我们就来看app.vue代码,一行一行啃。
二、模板骨架:两个大面板 + 一个按钮
<template>
<div class="container">
<!-- 左侧输入面板 -->
<div class="input">
<!-- 文件选择 + 预览 -->
<div class="file-input">
<input
type="file"
ref="uploadImage"
accept="image/*"
@change="updateImageData"
required
/>
</div>
<img :src="imgPreview" alt="" v-if="imgPreview"/>
<!-- 设置区:队服编号/颜色 -->
<div class="settings">
<div class="selection">
<label>队服编号:</label>
<input type="number" v-model="uniform_number"/>
</div>
<div class="selection">
<label>队服颜色:</label>
<select v-model="uniform_color">
<option value="红">红</option>
...
</select>
</div>
</div>
<!-- 设置区:位置/持杆/风格 -->
<div class="settings">
<div class="selection">
<label>位置:</label>
<select v-model="position">
<option value="0">守门员</option>
...
</select>
</div>
<div class="selection">
<label>持杆:</label>
<select v-model="shooting_hand">
<option value="0">左手</option>
...
</select>
</div>
<div class="selection">
<label>风格:</label>
<select v-model="style">
<option value="写实">写实</option>
...
</select>
</div>
</div>
<!-- 生成按钮 -->
<div class="generate">
<button @click="generate">生成</button>
</div>
</div>
<!-- 右侧输出面板 -->
<div class="output">
<div class="generated">
<img :src="imgUrl" alt="" v-if="imgUrl"/>
<div v-if="status">{{ status }}</div>
</div>
</div>
</div>
</template>
逐行解读:
ref="uploadImage":把这个 input 的 DOM 节点挂到响应式变量uploadImage上,等会儿上传文件时要拿input.files。@change="updateImageData":一旦选了图片,触发本地预览函数。v-model那坨:所有参数(队服编号、颜色、位置、持杆、风格)都双向绑定到ref变量上。v-if="imgPreview/imgUrl":没数据前不占位,有图再显示。@click="generate":唯一主流程入口,点了就开干。
模板非常“扁平”,就是把所有输入/输出堆在 flex 容器里。接下来看灵魂——script。
三、逻辑核心:Vue 3 Composition API 如何串联生成全流程
<script setup>
import { ref, onMounted } from 'vue'
// 1. 常量 & 配置
const patToken = import.meta.env.VITE_PAT_TOKEN;
const uploadUrl = 'https://api.coze.cn/v1/files/upload';
const workflowUrl = 'https://api.coze.cn/v1/workflow/run';
const workflow_id = '7628583629199687718';
console.log(patToken); // 仅调试用,生产删掉
逐行解读:
patToken:用import.meta.env从 Vite 环境变量里拿 Coze 的 Personal Access Token,绝不硬编码。uploadUrl/workflowUrl:Coze 开放 API 地址,这里用国内版api.coze.cn。workflow_id:你在 Coze 里发布工作流后会拿到一个 ID,这里直接写死,也可以抽成环境变量。
// 2. 用户选项的响应式数据
const uniform_number = ref(10);
const uniform_color = ref('红');
const position = ref(0);
const shooting_hand = ref(0);
const style = ref('写实');
逐行解读:
- 全部用
ref包一层,这是 Vue 3 的响应式基础。类型上,enum 类型的值(如位置、持杆)用字符串"0"/"1",方便对接 Coze 工作流里的参数类型。
// 3. 生成过程状态
const status = ref(''); // '' → 上传中 ... → 生成中 ... → 生成成功
const imgUrl = ref(''); // 最终生成的图片链接
逐行解读:
status:充当“进度文案”,控制生成按钮和提示文字。空字符串表示无操作。imgUrl:从工作流返回后赋值,直接驱动<img :src="imgUrl">。
3.1 图片预览模块:FileReader 的老派浪漫
const uploadImage = ref(null);
const imgPreview = ref('');
onMounted(() => {
console.log(uploadImage.value); // 看一下 DOM 挂载成功没
})
const updateImageData = () => {
const input = uploadImage.value;
if (!input.files || input.files.length === 0) return;
const file = input.files[0]; // 拿文件对象
const reader = new FileReader();
reader.readAsDataURL(file); // 异步读成 Base64 dataURL
reader.onload = (e) => {
imgPreview.value = e.target.result; // 赋值给 imgPreview,触发预览渲染
}
}
逐行解读:
uploadImage与模板里的ref="uploadImage"对应,onMounted后拿到原生 input 节点。updateImageData:用FileReader把文件转成 base64,赋给imgPreview。这里没做文件大小/类型校验,生产环境可以加。- 为什么不用 URL.createObjectURL? 用
readAsDataURL更适合小图预览,且不需要手动 revoke;而且 Coze 上传接口走 FormData 原文件流,预览和上传两条线互不干扰。
3.2 生成主流程 generate:状态机的灵魂
const generate = async () => {
status.value = "图片上传中..."
const file_id = await uploadFile();
if (!file_id) return; // 上传失败则中断
status.value = "图片上传成功, 正在生成...";
// 组装工作流参数
const parameters = {
picture: JSON.stringify({ file_id }),
style: style.value,
uniform_color: uniform_color.value,
uniform_number: uniform_number.value,
position: position.value,
shooting_hand: shooting_hand.value,
}
// 调用 Coze 工作流
const res = await fetch(workflowUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${patToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
workflow_id,
parameters
})
});
const ret = await res.json();
if (ret.code !== 0) {
status.value = ret.msg; // 显示错误原因
return;
}
const data = JSON.parse(ret.data);
console.log(data);
status.value = ''; // 恢复正常态
imgUrl.value = data.data; // 赋值生成图片链接
}
逐行解读:
status驱动 UI 变化,三步文案:“图片上传中...”、“图片上传成功, 正在生成...”、最后清空显示图片。picture参数故意用JSON.stringify({ file_id })包一层,因为 Coze 工作流那边接收的是 JSON 字符串,不是对象——这是跟后端约定好的格式。- 请求头带
Authorization: Bearer ${patToken}和Content-Type: application/json,这是 Coze 开放 API 的标准签名方式。 - 错误处理:
ret.code !== 0说明业务逻辑失败,直接将ret.msg抛给 status 展示。 ret.data是一个 JSON 字符串,需要再JSON.parse才能拿到真实地址,赋值给图片src。
3.3 文件上传模块:用 FormData 送走图片
const uploadFile = async () => {
const formData = new FormData();
const input = uploadImage.value;
if (!input.files || input.files.length <= 0) return;
formData.append('file', input.files[0]); // 塞入原始文件
const res = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${patToken}`,
// ⚠️ 千万别设置 Content-Type,让浏览器自动带 boundary
},
body: formData
})
const ret = await res.json();
if (ret.code !== 0) {
status.value = ret.msg;
return
}
return ret.data.id; // 返回 Coze 服务端的 file_id
}
逐行解读:
FormData是上传文件的标准姿势,append('file', ...)中的'file'字段名要和 Coze 文件上传 API 要求一致(官方文档为file)。- Headers 中绝不要手动设置 Content-Type,因为 fetch 会自动添加
multipart/form-data和boundary参数。 - 返回值
ret.data.id就是 Coze 文件的唯一 ID,之后工作流可以用它去下载加工。
四、Coze 工作流内部长啥样?(图解)
前端只是皮囊,真正干活的是 Coze 里的工作流。下图展示了一个典型配置:
graph TD
A[开始节点] --> B[图片输入: picture]
B --> C[代码节点: 解析 file_id]
C --> D[下载图片节点]
D --> E[AI 换装/风格迁移处理]
E --> F[上传生成结果]
F --> G[结束: 返回图片 URL]
H[输入: uniform_number] --> E
I[输入: uniform_color] --> E
J[输入: position] --> E
K[输入: shooting_hand] --> E
L[输入: style] --> E
流程节点详解:
- 开始节点:接收我们 fetch 传入的 6 个参数。
- 图片输入 (picture):前端传的是
{"file_id":"xxx"}的 JSON 字符串,所以需要第一个代码节点用JSON.parse取出file_id。 - 下载图片节点:Coze 插件能力,根据
file_id从 Coze 文件服务拉原图二进制。 - AI 处理节点(换装/风格迁移):将下载的原图和 uniform_number、color、position 等参数喂给大模型(或图像服务),这一步通常是一个“图像生成”插件。
- 上传生成结果节点:AI 产出新图片后,存回 Coze 文件服务,得到一个新的
file_id。 - 结束节点:返回最终图片的可访问 URL,格式就是
{"data":"https://xxx"}。
因此前端 JSON.parse(ret.data) 后的 data.data 就是那个 URL。
如果你想复刻这个工作流,可以在 Coze 里这样搭:
① 创建开始节点,定义与前端一致的参数名。
② 添加「代码节点」解析picture,输出file_id。
③ 使用「Coze 文件系统」插件下载图片。
④ 接一个「图像处理(换装/风格化)」插件(或自定义模型)。
⑤ 再用「Coze 文件系统」上传生成图。
⑥ 返回结果。发布后记下workflow_id填入前端。
五、样式:简单的弹性布局
<style scoped>
.container {
display: flex;
flex-direction: row;
align-items: start;
justify-content: start;
height: 100vh;
font-size: .85rem;
}
/* ... */
.generated {
width: 400px;
height: 400px;
border: solid 1px black;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.output img {
width: 100%;
}
</style>
没有用第三方组件库,纯手写 flex,让左右两栏自然排列。生成窗口固定 400x400,图片 width:100% 自适应。很朴素,但够用。
六、回顾完整数据流
一次完整的“生成”操作,数据是怎么跑的?
- 用户选择文件 →
updateImageData转 base64 →imgPreview显示预览。 - 点击“生成” →
status更新为“上传中”。 uploadFile()→FormData带文件请求api.coze.cn/v1/files/upload→ 获得file_id。status更新为“生成中”,组装参数{ picture: '{"file_id":"xxx"}', style, ... }。- 请求
api.coze.cn/v1/workflow/run→ Coze 工作流消费参数。 - 工作流内部下载原图 → AI 融合参数生成新图 → 上传并返回 URL。
- 前端解析返回 JSON →
imgUrl赋值,页面展示最终图片。 status清空,整个流程结束。
七、可以优化的点 & 生产建议
想把这个玩具变成产品,可以考虑:
- 环境变量强化:
workflow_id也放进.env,方便切换测试/生产环境。 - 上传进度条:用
XMLHttpRequest的upload.onprogress或 fetch +ReadableStream做真实进度。 - 并发控制:防止用户连点多次“生成”,加
loading锁。 - 错误重试:上传/工作流调用失败时,提供重试按钮。
- 生成历史:保存
imgUrl到localStorage,让用户能看之前生成的图。 - 更丰富的状态管理:把生成状态抽象成枚举
IDLE / UPLOADING / RUNNING / SUCCESS / ERROR,而不是裸字符串。
写在最后
整篇文章带着大家从 Vue 3 的单文件组件出发,一行一行看懂了图像生成应用的完整前端逻辑,并且配合 Coze 工作流图解理解了后端处理链路。你会发现:
前端不再只是调一个简单的“生成接口”,而是完整参与了文件上传、参数编排、状态流转和结果消费的全过程。
这个模式完全可以复用到 证件照生成、头像风格化、短视频素材处理 等场景。把 AI 能力封装成 Coze 工作流,前端只需做个轻量编排层,开发效率直接起飞。
项目完整代码可直接粘贴到 Vite + Vue 3 项目中使用。