引言
"你家的猫,也能打冰球?"
不是玩笑------这是一次前端与 AI 工作流的完美邂逅。
在当今 AI 应用爆发的时代,开发者不再满足于调用单一模型 API,而是通过 工作流(Workflow) 编排多个能力节点,实现复杂业务逻辑。而前端作为用户交互的第一线,如何优雅地集成这些 AI 能力,成为现代 Web 开发的重要课题。
本文将带你深入剖析一个真实项目:使用 Vue3 前端调用 Coze 平台的工作流 API,上传一张宠物照片,生成穿着定制队服、手持冰球杆的运动员形象图 。我们将逐行解读 App.vue 源码,解释每一个 API 调用、每一段逻辑设计,并结合完整的 Coze 工作流图解,还原整个数据流转过程。文章内容严格引用原始代码(一字不变),确保技术细节 100% 准确。
一、项目背景与目标
AI 应用之冰球前端应用 vue3:冰球协会,上传宠物照片,生成运动员的形象照片。
这个应用的核心功能非常明确:
- 用户上传一张宠物(或人物)照片;
- 选择冰球队服编号、颜色、场上位置、持杆手、艺术风格等参数;
- 点击"生成",系统调用 AI 工作流;
- 返回一张合成后的"冰球运动员"图像。
而这一切的实现,完全依赖于 Coze 平台提供的工作流 API。前端负责收集输入、上传文件、发起请求、展示结果------典型的"轻前端 + 重 AI 后端"架构。
二、App.vue 整体结构概览
App.vue 是一个标准的 Vue3 单文件组件(SFC),采用 <script setup> 语法糖,结合 Composition API 实现响应式逻辑。整体分为三部分:
<template>:用户界面(UI)<script setup>:业务逻辑(JS)<style scoped>:样式(CSS)
我们先从模板入手,理解用户看到什么、能做什么。
三、模板(Template)详解:用户交互层
3.1 文件上传与预览
ini
<div class="file-input">
<input
type="file"
ref="uploadImage"
accept="image/*"
@change="updateImageData" required />
</div>
<img :src="imgPreview" alt="" v-if="imgPreview"/>
<input type="file">:原生文件选择器,限制只接受图片(accept="image/*")。ref="uploadImage":通过ref获取该 DOM 元素,便于 JS 中读取文件。@change="updateImageData":当用户选择文件后,立即触发updateImageData方法,生成本地预览。imgPreview是一个响应式变量,用于显示 Data URL 格式的预览图,无需上传即可看到效果。
✅ 用户体验亮点:即使图片很大、上传很慢,用户也能立刻确认自己选对了图。
3.2 表单参数设置
接下来是两组设置项,全部使用 v-model 双向绑定:
第一组:队服信息
xml
<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>
<option value="蓝">蓝</option>
<option value="绿">绿</option>
<option value="白">白</option>
<option value="黑">黑</option>
</select>
</div>
</div>
uniform_number:默认值为10(见 script 部分),支持任意数字。uniform_color:限定五种颜色,值为中文字符串(如"红")。
第二组:角色与风格
xml
<div class="settings">
<div class="selection">
<label>位置:</label>
<select v-model="position">
<option value="0">守门员</option>
<option value="1">前锋</option>
<option value="2">后卫</option>
</select>
</div>
<div class="selection">
<label>持杆:</label>
<select v-model="shooting_hand">
<option value="0">左手</option>
<option value="1">右手</option>
</select>
</div>
<div class="selection">
<label>风格:</label>
<select v-model="style">
<option value="写实">写实</option>
<option value="乐高">乐高</option>
<option value="国漫">国漫</option>
<option value="日漫">日漫</option>
<option value="油画">油画</option>
<option value="涂鸦">涂鸦</option>
<option value="素描">素描</option>
</select>
</div>
</div>
position和shooting_hand的值虽然是数字字符串("0"/"1"/"2"),但前端显示为中文,兼顾可读性与后端兼容性。style提供 7 种艺术风格,极大增强趣味性和分享欲。
3.3 生成按钮与输出区域
ini
<div class="generate">
<button @click="generate">生成</button>
</div>
点击后触发 generate() 函数,启动整个 AI 生成流程。
输出区域:
ini
<div class="output">
<div class="generated">
<img :src="imgUrl" alt="" v-if="imgUrl"/>
<div v-if="status">{{ status }}</div>
</div>
</div>
imgUrl:存储 Coze 返回的生成图 URL。status:动态显示当前状态(如"上传中..."、"生成失败"等),避免用户焦虑。
💡 设计哲学:状态反馈是良好 UX 的核心。没有反馈的"生成"按钮,等于黑盒。
四、脚本逻辑(Script Setup)深度解析
现在进入最核心的部分------JavaScript 逻辑。
4.1 环境配置与常量定义
ini
import { ref, onMounted } from 'vue'
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 = '7584046136391630898';
-
import.meta.env.VITE_PAT_TOKEN:Vite 提供的环境变量注入机制。.env文件中应包含:iniVITE_PAT_TOKEN=cztei_lvNwngHgch9rxNlx4KiXuky3UjfW9iqCZRe17KDXjh22RLL8sPLsb8Vl10R3IHJsW -
uploadUrl:Coze 官方文件上传接口(文档)。 -
workflowUrl:触发工作流的入口(文档)。 -
workflow_id:在 Coze 控制台创建的工作流唯一 ID,内部已配置好图像生成逻辑(如调用文生图模型、叠加队服等)。
⚠️ 安全警告:将 PAT Token 放在前端仅适用于演示或内部工具。生产环境应通过后端代理 API,避免 Token 泄露。
4.2 响应式状态声明
ini
const uniform_number = ref(10);
const uniform_color = ref('红');
const position = ref(0);
const shooting_hand = ref(0);
const style = ref('写实');
const status = ref('');
const imageUrl = ref('');
- 所有表单字段均为
ref响应式对象,确保视图自动更新。 status初始为空,后续将显示:"图片上传中..." → "图片上传成功, 正在生成..." → 成功清空 或 错误信息。imageUrl初始为空,生成成功后赋值为图片 URL。
4.3 核心函数 1:图片预览(updateImageData)
ini
const uploadImage = ref(null);
const imgPreview = ref('');
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);
reader.onload = (e) => {
imgPreview.value = e.target.result;
};
}
uploadImage是对<input>元素的引用。- 使用
FileReader的readAsDataURL方法,将文件转为 Base64 编码的 Data URL。 onload回调中,将结果赋给imgPreview,触发<img>标签渲染。
✅ 优势:纯前端实现,零网络请求,秒级响应。
4.4 核心函数 2:文件上传(uploadFile)
ini
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}`
},
body: formData
});
const ret = await res.json();
console.log(ret);
if (ret.code !== 0) {
status.value = ret.msg;
return;
}
return ret.data.id;
}
逐行解析:
-
构造 FormData:
new FormData()是浏览器原生 API,用于构建 multipart/form-data 请求体,专为文件上传设计。formData.append('file', file):Coze 要求字段名为file。
-
发送 POST 请求:
-
URL:
https://api.coze.cn/v1/files/upload -
Headers:
Authorization: Bearer <token>:Coze 使用 Bearer Token 认证。
-
Body:
formData自动设置正确 Content-Type(含 boundary)。
-
-
处理响应:
-
成功时返回:
css{ "code": 0, "msg": "success", "data": { "id": "file_xxx", ... } } -
失败时
code !== 0,msg包含错误原因(如 Token 无效、文件过大等)。 -
函数返回
file_id(如"file_abc123"),供下一步使用。
-
关键点 :Coze 的文件上传是独立步骤,必须先上传获取
file_id,才能在工作流中引用。
五、核心函数 3:调用工作流(generate)
这是整个应用的"大脑"。我们结合 Coze 工作流图,深入分析其逻辑与数据流。
ini
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,
};
try {
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();
console.log("Workflow API response:", ret);
if (ret.code !== 0) {
status.value = ret.msg;
return;
}
// 检查返回数据结构
console.log("Return data:", ret.data);
console.log("Return data type:", typeof ret.data);
// 尝试解析数据
let data;
if (typeof ret.data === 'string') {
try {
data = JSON.parse(ret.data);
console.log("Parsed data:", data);
} catch (e) {
console.error("JSON parse error:", e);
status.value = "数据解析错误";
return;
}
} else {
data = ret.data;
}
// 检查data.data是否存在
if (data && data.data) {
console.log("Generated image URL:", data.data);
status.value = '';
imageUrl.value = data.data;
} else {
console.error("Invalid data structure, missing 'data' field:", data);
status.value = "返回数据结构错误";
}
} catch (error) {
console.error("Generate error:", error);
status.value = "生成失败,请检查网络连接";
}
}
逻辑拆解(结合 Coze 工作流图)
Coze 工作流结构(图解说明)

图注:
- 开始节点 :接收
picture,style,uniform_number,position,shooting_hand,uniform_color等参数。- 分支一 :
imgUnderstand_1(图像理解)→ 分析上传图片内容(如动物种类、姿态)。- 分支二 :
代码节点 → 根据position,shooting_hand,style等生成描述文本(如"一只狗,右手持杆,身穿红色10号队服,站在冰球场上")。- 大模型节点:将图像理解结果与描述文本合并,生成最终提示词(prompt)。
- 图像生成节点:调用文生图模型(如豆包·1.5·Pro·32k),生成新图像。
- 结束节点:输出生成图的 URL。
前端代码的对应关系
| 前端参数 | Coze 输入字段 | 用途 |
|---|---|---|
picture |
picture |
图片文件 ID,传入 imgUnderstand_1 和 图像生成 节点 |
style |
style |
传递给 代码 节点,决定艺术风格 |
uniform_number |
uniform_number |
用于生成描述 |
position |
position |
决定角色动作(如守门员蹲姿) |
shooting_hand |
shooting_hand |
决定持杆手 |
uniform_color |
uniform_color |
用于生成队服颜色 |
💡 关键点:前端只需提供原始参数,Coze 工作流内部完成所有逻辑编排。
数据流全过程
-
前端上传文件 → 得到
file_id -
前端组装参数 → 发送至
/workflow/run -
Coze 工作流执行:
imgUnderstand_1:分析图片内容 → 输出text,url,content代码节点:根据参数生成描述 → 如"一只猫,身穿蓝色10号队服,右手持杆,站在冰球场上,风格为乐高"大模型节点:合并图像理解结果与描述 → 生成最终 prompt图像生成节点:调用模型生成图像 → 返回data字段(URL)
-
前端接收响应:
- 若
ret.data是字符串 → 尝试JSON.parse - 若是对象 → 直接取
data.data - 最终赋值给
imageUrl
- 若
✅ 为什么需要双重解析?
因为 Coze 的"图像生成"节点可能直接返回 URL 字符串,也可能返回
{ data: "url" }结构。前端必须兼容两种情况。
六、样式(Style)简析
xml
<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;
display: flex;
justify-content: center;
align-items: center;
}
.output img {
width: 100%;
}
</style>
- 使用 Flex 布局,左右分栏(输入区固定宽度,输出区自适应)。
.generated容器固定 400x400,图片居中显示,无论原始比例如何都不变形。scoped确保样式仅作用于当前组件,避免污染全局。
七、项目运行
在项目终端运行命令 :npm run dev
运行界面如下:

选择图片及风格等内容后,点击开始生成,运行结果如图:

总结:为什么这个项目值得学习?
-
真实场景:不是 Hello World,而是完整产品逻辑。
-
技术全面:
- Vue3 Composition API
- 文件上传与预览
- Fetch API 与错误处理
- 环境变量管理
- 响应式状态驱动 UI
-
AI 集成范式:展示了如何将复杂 AI 能力封装为简单 API,前端只需"填参数 + 拿结果"。
-
用户体验优先:状态提示、本地预览、错误反馈一应俱全。
安全与部署建议:
-
后端代理所有 Coze API 调用:
- 前端 → 自己的后端(/api/generate)
- 后端 → Coze(携带安全存储的 Token)
-
限制工作流权限:Coze 的 PAT Token 应仅授予必要权限。
-
添加速率限制:防止滥用。
最终,技术的意义在于创造快乐。
当你上传一张狗子的照片,看到它穿上红色10号球衣、右手持杆、以"乐高"风格站在冰场上------
你会笑,会分享,会说:"AI 真酷!"
而这,正是我们写代码的初心。