引言
现如今 AI 应用开发变得越来越"平民化",即使你不懂任何深度学习,也能通过"玩"一些 AI 开发平台来做出一些有趣又智能的小工具。而这其中不得不提的就是**✨字节跳动推出的一站式AI应用开发平台 Coze 了**。
那为什么要选择 Coze 呢?那是因为其强大的低代码开发能力,即使是小白也可通过可视化界面"搭积木"一样编排工作流------比如上传图片、调用大模型、生成新图像等等,并且全程无需写复杂的后端或算法代码,极大降低了 AI 集成门槛💡。
拥有了工作流后,我们就可以使用优秀的现代前端框架 Vue3 ,通过其简洁的语法、响应式数据和组件化思想,让页面交互开发变得高效又直观。不仅向用户展示我们的开发成果,也要"接住"用户操作,最后再把结果展示出来。
本文就以"宠物变冰球运动员"为例,联合 Coze 的"AI 能力"与 Vue3 的"交互表现力" 来手把手教你如何使用这两项技术,从零搭建一个完整的 AI 应用。
项目简介
本项目是一个基于 Vue3 + Coze AI 工作流 的轻量级 Web 应用,实现用户上传一张宠物照片,选择队服编号、颜色、担任的位置、持杆手以及艺术风格,即可生成该宠物化身"冰球运动员"的图像。
核心功能:
- 文件上传并预览
- 用户配置参数(队服、风格等)
- 调用 Coze 工作流进行 AI 处理
- 返回生成的图像并展示
📂 项目结构说明
arduino
project-root/
├── src/
│ ├── components/
│ └── App.vue # 主页面组件
├── public/
│ └── index.html
├── vite.config.js
└── package.json
我们只用了一个主文件 App.vue 来完成全部功能,适合快速原型开发。
🤖 Coze 平台开发详解(低代码)
整体流程概述:
- 开始节点:接收所有输入数据。
- 图像理解节点:分析照片内容,提取宠物图片的关键特征。
- 特征提取节点:将特征转化为结构化的描述。
- 代码节点:处理用户提供的参数或生成默认值来描述一只宠物作为"冰球运动员"的外观和特性
- 图像生成节点:结合用户选择和图片特征来生成需要展现给用户的图片
- 结束节点:将生成的新图像展示给用户。
从输入图片开始,经过图像理解、特征提取、代码处理与图像生成,最终输出结果的完整流程

一、开始节点:
功能:流程入口,接收用户的原始输入数据,启动后续流程。
操作演示:

输入参数:
| 参数名 | 类型 | 是否必填 | 默认值 | 描述 |
|---|---|---|---|---|
picture |
Image | 是 | --- | --- |
style |
String | 否 | 写实 | 生成照片的艺术风格 |
uniform_number |
Number | 否 | 10 | 运动员的号码 |
uniform_color |
String | 否 | 红 | 球衣颜色 |
position |
Number | 否 | --- | 整数枚举,0、1、2,分别为守门员、前锋、后卫,默认随机 |
shooting_hand |
Number | 否 | --- | 持杆手,整数枚举0、1,分别为左手、右手,默认随机 |

二、图像理解节点:imgUnderstand
功能:使用AI模型分析图像内容。
操作演示:

输入:
text:这应该是一张宠物图片,请详细描述宠物的外貌特征。url:选择从开始取到的图片

输出:通过"查看示例"可以查看输出效果
author:作者信息content:图像描述文本msg:返回消息

三、特征提取节点:
功能:对文本进行语义分析,提取关键词或结构化信息。
操作演示:

模型:豆包·1.5 · Pro · 32k(默认即可)
技能:无需配置技能
输入 :连接图像理解节点 会自动配置 imgUnderstand 的 content 字段为 input 中的变量值
系统提示词:为对话提供系统级指导,在此之中可以使用{{变量名}}等方式引用变量(此处我们导入文字提示词即可)
你是动物学家,负责从动物描述中提取出该动物(主要是外表)里最有独特性的特征,例如特征的肤色、表情、神态、动作等等。
用户提示词 :向模型提供用户指令(此处我们导入输入变量input)
css
{{input}}
输出 :output(默认即可)
四、代码节点:
功能:执行自定义代码逻辑,这里我们需要处理用户提供的参数或生成默认值
操作演示:

输入 :从开始节点 里面导入shooting_hand、style、uniform_number、uniform_color、position

代码逻辑:
ts
const random = (start: number, end: number) => {
// 生成一个 [0, 1) 之间的随机数
const p = Math.random();
// 线性插值 + 向下取整
// start * (1 - p) + end * p 只会让数据居于[start, end)之间
// 再向下取整使数据只为整数并且小于end
return Math.floor(start * (1 - p) + end * p);
}
//对用户传入的参数进行标准化处理和默认值填充,以便后续用于图像生成 prompt 的构建
async function main({ params }: Args): Promise<Output> {
if (params.position == null) params.position = random(0, 3);
if (params.shooting_hand == null) params.shooting_hand = random(0, 2);
const style = params.style || '写实';
const uniform_number:string = (params.uniform_number || 10).toString();
const uniform_color = params.uniform_color || '红';
const position = params.position == 0 ? '守门员': (params.position == 1 ? '前锋': '后卫');
const shooting_hand = params.shooting_hand == 0 ? '左手': '右手';
const empty_hand = params.shooting_hand ? '左手': '右手';
// 构建输出对象
const ret = {
style,
uniform_number,
uniform_color,
position,
shooting_hand,
};
return ret;
}
输出:

五、图像生成节点:
功能:根据描述生成新图像。
操作演示:

模型设置 :选择通用即可,比例按照自身喜好调节
参考图 :添加参考图,模型选择"形象一致",参考图选择开始节点中的 picture,程度调为 0.7
程度:决定了参考图对最终生成图像的影响程度。数值范围通常是从0到1,其中0表示完全不考虑参考图,而1表示尽可能地模仿参考图。
输入:
description:图片描述,导入imgUnderstand节点中的content- 从代码节点 里面导入处理后的
shooting_hand、style、uniform_number、uniform_color、position details:详细信息,导入特征提取节点 中的output。

提示词:生成内容的提示词,分为两类
- 正向提示词:引导生成内容
bash
用动物的形象和特征,将该动物**拟人**为一名宠物儿童冰球员,生成{{style}}风格的冰球球员照片,球员身穿
{{uniform_color}}色队服,佩戴同色的冰球头盔,队服号码为{{uniform_number}}号,球员位置是{{position}},
用{{shooting_hand}}握着球杆,另一只手空着。该照片图像风格为{{style}}。
# 动物形象描述
{{description}}
# 独特外貌特征
{{details}}
# 注意
- 照片中应强化动物独特的外貌特征,以增加辨识度
- 如果球员位置是守门员,画面中应该有冰球球门
-
反向提示词:排除不良或无关元素
球员双手各握一根球杆
球员未佩戴头盔
球员吃东西
画面中出现除了冰球之外的其他球类
地点不在冰球赛场
球员四足站立
输出:
data:生成图像的二进制数据或URLmsg:状态信息
六、结束节点:
功能:工作流的最终节点,返回工作流运行后的结果信息。
操作演示:

输出变量 :连接图像生成节点
output:最终结果,变量值为图像生成节点 中的data
回答内容:编辑智能体回复的内容,我们这里不修改直接输出图片(不需要流式输出)
lua
{{output}}
流式输出:回复内容中的大语言模型的生成内容将会逐字流式输出;关闭后,回复内容将全部生成后一次性输出
测试:
在连接好工作流后,我们当然需要进行测试,以保证节点中没有发生错误
操作演示:

后话:
当然,在你测试无误之后,可以点击右上角的发布来向别人分享你第一次创作的工作流。
发布之后,在你资源库中的工作流中就能找到你所发布的资源了。
💻 前端实现详解(Vue3 + JavaScript)
一、 <template> ------ 视觉层布局
首先我们确定组件的主要功能为---用户可以上传一张图片,并根据不同的参数(如队服编号、颜色等)进行个性化设置。
所以我们的UI布局,包括两块区域:输入区域(用于上传图片和选择参数)和 输出区域(展示生成的结果)。
html
<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>
<option value="蓝">蓝</option>
<option value="绿">绿</option>
<option value="白">白</option>
<option value="黑">黑</option>
</select>
</div>
</div>
<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>
<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 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>
1、输入部分(input):
html
<input type="file" ref="uploadImage" accept="image/*" @change="updateImageData" required />
这段代码实现了 让用户选择本地文件并且触发预览更新
type="file":声明这是一个文件上传控件,点击后会弹出系统文件选择对话框。accept="image/*":限制用户只能选择图片文件,防止无效输入。@change="updateImageData":当用户选中一张图片后,会立即触发 updateImageData 方法(用于将图片文件转换为可以直接预览的 URL)ref="uploadImage":为这个 input 元素注册一个 引用标识 ,方便在<script setup>中通过声明一个相同名称的 ref 变量 来直接访问该 DOM 元素。required: 表示该字段是必填项。
对比
ref和v-model:
v-model:同步"数据值",并且可以自动更新(关心的是"值"本身)ref:获取"DOM元素",而且需要手动更新(需要访问 DOM 的原生能力)⚠️并且在
<input type="file">中 无法使用v-model!因为浏览器出于安全考虑,不允许 JS 设置 file input 的值,只能读取 。所以必须用
ref来读取用户选中的文件。
html
<img :src="imgPreview" alt="" v-if="imgPreview" />
在用户上传图片后,需要及时给予用户反馈(让用户知道自己行为驱动了页面,而不是毫无作用),所以需要展示预览图,让用户知道上传图片成功了。
:src="imgPreview":动态绑定响应式变量 imgPreview,用于自动更新图片。v-if="imgPreview":条件渲染,只有当用户成功上传后才能渲染<img>元素,避免未上传时显示空白占位图。
2、用户选择参数部分(settings):
html
<div class="selection">
<label>队服颜色:</label>
<select v-model="uniform_color">
<option value="红">红</option>
...
</select>
</div>
响应式的选择器,以便于用户选择参数
<label>队服颜色:</label>:提供文字说明,告诉用户这个下拉框的作用<select>:HTML 的下拉选择控件v-model="uniform_color":双向绑定 uniform_color 用于同步用户选择数据<option value="红">红</option>:每个<option>代表一个可选项,该选项被选中时,赋给 uniform_color 的实际值就为红
3、输出部分(output):
html
<img :src="imgUrl" alt="" v-if="imgUrl">
同上,绑定的响应式变量 imgUrl 用于返还 Coze 工作流生成后的图片
html
<div v-if="status">{{ status }}</div>
显示当前操作的状态提示信息,为用户提供及时的反馈
v-if="status":只有当 status 有内容时才渲染{{ status }}:将 status 的文本内容插入到页面中
二、<script setup> ------ 业务逻辑核心
基于业务需求,我们的开发流程总体为:定义响应式状态(数据层) --> 实现图片预览(本地交互) --> 实现文件上传到 Coze(前置依赖) --> 调用工作流生成图像(核心业务) --> 环境变量与 API 配置(支撑信息)
html
<script setup>
import { ref, onMounted } from 'vue'
// --------------- 定义响应式状态 -------------------
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 imgUrl = ref(''); // 用于存储最终生成的图片 URL
const imgPreview = ref(''); // 用于本地预览用户上传的原图
// --------------- 图片预览模块 -------------------
const uploadImage = ref(null);
onMounted(() => {
console.log(uploadImage.value)
})
const updateImageData = () => {
const input = uploadImage.value;
if (!input.files || input.files.length === 0) {
return;
}
const file = input.files[0];
console.log(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
imgPreview.value = e.target.result;
}
}
// --------------- 大厂常用业务请求 ----------------
const patToken = import.meta.env.VITE_PAT_TOKEN;
const uploadUrl = 'https://api.coze.cn/v1/files/upload';
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;
}
// --------------- 生成图片模块 -------------------
const workflowUrl = 'https://api.coze.cn/v1/workflow/run';
const workflow_id = 'XXXXXXX';
const generate = async () => {
status.value = "图片上传中..."
const file_id = await uploadFile();
if (!file_id) return;
status.value = "图片上传成功,正在生成...";
// workflow 调用
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,
}
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;
}
</script>
1、定义响应式状态(数据层):
js
// 用户选择参数,并且设定初始值
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 imgUrl = ref(''); // 生成图片url
2、图片预览功能(本地交互):
js
const uploadImage = ref(null);
onMounted(() => {
console.log(uploadImage.value)
})
const updateImageData = () => {
const input = uploadImage.value;
if (!input.files || input.files.length === 0) {
return;
}
const file = input.files[0];
// console.log(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
imgPreview.value = e.target.result;
}
}
定义响应式引用
js
const uploadImage = ref(null);
标记模板中的 <input type="file">元素,未挂载前为 null,便于访问该DOM元素
生命周期钩子:组件挂载时执行的操作
js
// null -> DOM对象 (变化)
onMounted(() => {
console.log(uploadImage.value)
})
输出 uploadImage.value 到控制台,便于检查此时是否已经正确绑定了 <input> 元素。如果绑定成功,uploadImage.value 就指向上面的 <input> 元素。
更新图片数据方法
js
// 用户选择文件后调用
const updateImageData = () => {
const input = uploadImage.value;
// console.log(uploadImage.value.files);
if (!input.files || input.files.length === 0) {
return;
}
const file = input.files[0];
// console.log(file);
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
imgPreview.value = e.target.result;
}
}
if (!input.files || input.files.length === 0) return;:判断用户上传的文件中是否至少包含一个文件const file = input.files[0];:取出用户上传的第一个文件(因为不知道用户会上传多少文件,但是我们单次只能处理一个)reader.readAsDataURL(file);:用新创建的 FileReader 实例上的 readAsDataURL 方法将文件转换为 Base64 编码的字符串(这种格式可以直接作为图像的URL使用)reader.onload = (e) => { imgPreview.value = e.target.result; }:onload 是一个事件处理器(回调函数),会自动触发 load 事件,这里也就是事件对象 e,而e.target指的是触发该事件的对象(也就是reader),e.target.result就是转换为 Base64 编码的字符串,并且赋值给本地预览的响应式数据 imgPreview,这样页面上就会自动更新
⚠️ 注意:文件读取是异步的 ,不能直接写
let result = reader.readAsDataURL(file),必须用回调或 Promise。

3、文件上传至 Coze 云服务:
js
const patToken = import.meta.env.VITE_PAT_TOKEN;
const uploadUrl = 'https://api.coze.cn/v1/files/upload';
// 先上传到coze服务器
const uploadFile = async () => {
// 创建一个空表单用于提交数据
const formData = new FormData(); // FormData专门用于构建form-data格式数据的对象,常用于传输文件
const input = uploadImage.value;
if (!input.files || input.files.length <= 0) return;
formData.append('file', input.files[0]);
// 向 coze 发送http请求 上传
const res = await fetch(uploadUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${patToken}`
},
body: formData
})
// 等待返还内容(异步),并转换为json格式
const ret = await res.json();
// console.log(ret);
// 错误判断
if( ret.code !== 0) {
status.value = ret.msg;
return;
}
return ret.data.id;
}
const patToken = import.meta.env.VITE_PAT_TOKEN;:获取认证令牌,在项目根目录中创建.env文件,在其中放置你的私有访问令牌。

const uploadUrl = 'https://api.coze.cn/v1/files/upload';:Coze 官方提供的文件上传 API 地址。formData.append('file', input.files[0]);:将用户选中的第一个文件附加到表单字段名为file的字段上
⚠️ 注意:Coze API 要求字段名必须是
'file',否则会报错
const res = await fetch(uploadUrl, {...}):向 Coze 上传文件
响应结构(Coze API 规范):
js
const ret = await res.json();
// 错误判断
if( ret.code !== 0) {
status.value = ret.msg;
return;
}
return ret.data.id;
Coze 的 API 通常返回如下 JSON 结构:
json
{
"code": 0,
"msg": "success",
"data": {
"id": "file-abc123xyz", // ← 我们需要的 file_id
"name": "cat.jpg",
"size": 123456
}
}
ret.code !== 0:判断是否出错(code === 0表示成功)。status.value = ret.msg:将错误信息显示给用户。return ret.data.id:成功时返回file_id(字符串),供后续工作流调用使用。
注: 这个
file_id是 Coze 系统内部对文件的唯一引用,后续在调用工作流时,只需传递这个 ID,无需再传整个文件。
4、触发 Coze 工作流生成图像(核心业务):
js
const workflowUrl = 'https://api.coze.cn/v1/workflow/run';
const workflow_id = 'XXXXXXX';
const generate = async () => {
status.value = "图片上传中..."
const file_id = await uploadFile();
if (!file_id) return;
status.value = "图片上传成功,正在生成...";
// workflow 调用
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,
}
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;
}
const workflowUrl = 'https://api.coze.cn/v1/workflow/run';:Coze 官方提供的 工作流执行 API 地址const workflow_id = 'XXXXXXX';:你在 Coze 控制台创建的工作流的唯一 ID(在 Coze Bot 或 Workflow 编排界面可找到)const file_id = await uploadFile();:取到返回的 file_id
为什么先上传?
Coze 的工作流无法直接处理前端的本地文件或 Base64,必须使用之前通过接口上传后返回的
file_id。
解析最终图片 URL
js
const data = JSON.parse(ret.data);
console.log(data);
status.value = '';
imgUrl.value = data.data;
❗ 为什么需要两次 JSON.parse?
这是 Coze 工作流 API 的特殊设计:
-
第一层
ret.data是一个 JSON 字符串 (不是对象!)jsret.data === '{"data":"https://cdn.coze.com/generated.jpg"}' -
所以必须用
JSON.parse(ret.data)得到真正的对象:jsconst data = { data: "https://cdn.coze.com/generated.jpg" };
最终图片 URL 在 data.data 中。
📌 这是因为 Coze 工作流的输出通常被封装为字符串,确保兼容性
三、CSS 样式优化
css
.container {
display: flex;
flex-direction: row;
align-items: start;
justify-content: start;
height: 100vh;
font-size: .85rem;
}
.input {
display: flex;
flex-direction: column;
min-width: 330px;
}
.output {
margin-top: 10px;
min-height: 300px;
width: 100%;
text-align: left;
}
.generated {
width: 400px;
height: 400px;
border: solid 1px black;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.output img {
width: 100%;
}
效果演示:

📚 学习资源推荐
| 类别 | 推荐内容 |
|---|---|
| Vue3 | Vue 官方文档 |
| Coze | Coze 开发者中心 |
| REST API | MDN Fetch API 文档 |
| 前端工程化 | Vite、Webpack、JavaScript |
📝 最后
这个项目不仅是一个简单的"AI图像生成器",更是现代 Web 开发的一个缩影:前端负责交互与体验,AI 提供智能能力,API 连接两者 😊,这也是当下的趋势:无代码/低代码 + 可视化 AI 编排 + 前端集成
AI 原生应用」的崛起
-
传统 AI 开发:需要算法工程师 + 后端 + 前端,周期长、成本高。
-
新范式(Coze 代表):
- 业务人员/前端开发者 通过拖拽工作流,组合大模型、插件、知识库;
- 前端直接调用工作流 API,像调用普通接口一样获取 AI 结果;
- 无需维护后端服务,Coze 托管执行环境。
这就是 "AI as a Service"(AI 即服务)。