Vue3 调用 Coze 工作流:从上传宠物照到生成冰球明星的完整技术解析

引言

"你家的猫,也能打冰球?"

不是玩笑------这是一次前端与 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 实现响应式逻辑。整体分为三部分:

  1. <template> :用户界面(UI)
  2. <script setup> :业务逻辑(JS)
  3. <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>
  • positionshooting_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 文件中应包含:

    ini 复制代码
    VITE_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> 元素的引用。
  • 使用 FileReaderreadAsDataURL 方法,将文件转为 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;
}

逐行解析:

  1. 构造 FormData

    • new FormData() 是浏览器原生 API,用于构建 multipart/form-data 请求体,专为文件上传设计。
    • formData.append('file', file):Coze 要求字段名为 file
  2. 发送 POST 请求

    • URL:https://api.coze.cn/v1/files/upload

    • Headers:

      • Authorization: Bearer <token>:Coze 使用 Bearer Token 认证。
    • Body:formData 自动设置正确 Content-Type(含 boundary)。

  3. 处理响应

    • 成功时返回:

      css 复制代码
      { "code": 0, "msg": "success", "data": { "id": "file_xxx", ... } }
    • 失败时 code !== 0msg 包含错误原因(如 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 工作流结构(图解说明)

图注

  1. 开始节点 :接收 picture, style, uniform_number, position, shooting_hand, uniform_color 等参数。
  2. 分支一imgUnderstand_1(图像理解)→ 分析上传图片内容(如动物种类、姿态)。
  3. 分支二代码 节点 → 根据 position, shooting_hand, style 等生成描述文本(如"一只狗,右手持杆,身穿红色10号队服,站在冰球场上")。
  4. 大模型节点:将图像理解结果与描述文本合并,生成最终提示词(prompt)。
  5. 图像生成节点:调用文生图模型(如豆包·1.5·Pro·32k),生成新图像。
  6. 结束节点:输出生成图的 URL。

前端代码的对应关系

前端参数 Coze 输入字段 用途
picture picture 图片文件 ID,传入 imgUnderstand_1图像生成 节点
style style 传递给 代码 节点,决定艺术风格
uniform_number uniform_number 用于生成描述
position position 决定角色动作(如守门员蹲姿)
shooting_hand shooting_hand 决定持杆手
uniform_color uniform_color 用于生成队服颜色

💡 关键点:前端只需提供原始参数,Coze 工作流内部完成所有逻辑编排。


数据流全过程

  1. 前端上传文件 → 得到 file_id

  2. 前端组装参数 → 发送至 /workflow/run

  3. Coze 工作流执行

    • imgUnderstand_1:分析图片内容 → 输出 text, url, content
    • 代码 节点:根据参数生成描述 → 如 "一只猫,身穿蓝色10号队服,右手持杆,站在冰球场上,风格为乐高"
    • 大模型 节点:合并图像理解结果与描述 → 生成最终 prompt
    • 图像生成 节点:调用模型生成图像 → 返回 data 字段(URL)
  4. 前端接收响应

    • 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

运行界面如下:

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


总结:为什么这个项目值得学习?

  1. 真实场景:不是 Hello World,而是完整产品逻辑。

  2. 技术全面

    • Vue3 Composition API
    • 文件上传与预览
    • Fetch API 与错误处理
    • 环境变量管理
    • 响应式状态驱动 UI
  3. AI 集成范式:展示了如何将复杂 AI 能力封装为简单 API,前端只需"填参数 + 拿结果"。

  4. 用户体验优先:状态提示、本地预览、错误反馈一应俱全。

安全与部署建议

  • 后端代理所有 Coze API 调用

    • 前端 → 自己的后端(/api/generate)
    • 后端 → Coze(携带安全存储的 Token)
  • 限制工作流权限:Coze 的 PAT Token 应仅授予必要权限。

  • 添加速率限制:防止滥用。

最终,技术的意义在于创造快乐。

当你上传一张狗子的照片,看到它穿上红色10号球衣、右手持杆、以"乐高"风格站在冰场上------

你会笑,会分享,会说:"AI 真酷!"

而这,正是我们写代码的初心。

完整项目源码:lesson_zp/ai/app/iceball: AI + 全栈学习仓库 - Gitee.com

相关推荐
异界蜉蝣2 小时前
React Fiber架构:Diff算法的演进
前端·react.js·前端框架
追梦_life2 小时前
localStorage使用不止于getItem、setItem、removeItem
前端·javascript
全栈陈序员2 小时前
请描述下你对 Vue 生命周期的理解?在 `created` 和 `mounted` 中请求数据有什么区别?
前端·javascript·vue.js·学习·前端框架
无限大62 小时前
用三行代码实现圣诞树?别逗了!让我们来真的
前端·javascript
init_23612 小时前
label-route-capability
服务器·前端·网络
拉姆哥的小屋2 小时前
深度剖析SentiWordNet情感词典:155,287单词的情感世界
前端·javascript·easyui
T___T2 小时前
从 0 搭建 React 待办应用:状态管理、副作用与双向绑定模拟
前端·react.js·面试
林太白2 小时前
vue3这些常见指令你封装了吗
前端·javascript
傻啦嘿哟2 小时前
Python在Excel中创建与优化数据透视表的完整指南
java·前端·spring