用 Vue 3 搓一个 AI 冰球形象生成器:从源码到 Coze 工作流全解析

0 阅读8分钟

最近在玩工作流, 从n8n到coze,发现coze更符合现在的我学,发现它真的能把“上传照片 → 选择风格 → 生成一张新图片”这件事变得特别轻量。只用前端一个单文件组件,就能把整个链路跑通。我把它做成一个生成冰球队员形象的小工具,本文就从这个项目的 App.vue 出发,逐行拆解源码,然后配合 Coze 工作流图解,带你还原「图片上传 → 工作流调用 → 生成结果返回」的全过程。

完整项目链接:gitee.com/hong-strong…

提示:文中的 token 已脱敏,请替换为你自己的扣子 PAT(个人访问令牌)。


一、项目总览:我们做了什么?

页面上半部分是参数区:选择照片、队服编号、颜色、位置、持杆手、生成风格。
下半部分是结果区:先出预览图,再展示生成后的图片。

整个交互流程只有三步:

  1. 选照片,页面上立刻出现本地预览
  2. 点击“生成”按钮,先把图片上传到 Coze 的文件服务,拿到 file_id
  3. 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-databoundary 参数。
  • 返回值 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

流程节点详解:

  1. 开始节点:接收我们 fetch 传入的 6 个参数。
  2. 图片输入 (picture):前端传的是 {"file_id":"xxx"} 的 JSON 字符串,所以需要第一个代码节点JSON.parse 取出 file_id
  3. 下载图片节点:Coze 插件能力,根据 file_id 从 Coze 文件服务拉原图二进制。
  4. AI 处理节点(换装/风格迁移):将下载的原图和 uniform_number、color、position 等参数喂给大模型(或图像服务),这一步通常是一个“图像生成”插件。
  5. 上传生成结果节点:AI 产出新图片后,存回 Coze 文件服务,得到一个新的 file_id
  6. 结束节点:返回最终图片的可访问 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% 自适应。很朴素,但够用。


六、回顾完整数据流

一次完整的“生成”操作,数据是怎么跑的?

  1. 用户选择文件updateImageData 转 base64 → imgPreview 显示预览。
  2. 点击“生成”status 更新为“上传中”。
  3. uploadFile()FormData 带文件请求 api.coze.cn/v1/files/upload → 获得 file_id
  4. status 更新为“生成中”,组装参数 { picture: '{"file_id":"xxx"}', style, ... }
  5. 请求 api.coze.cn/v1/workflow/run → Coze 工作流消费参数。
  6. 工作流内部下载原图 → AI 融合参数生成新图 → 上传并返回 URL。
  7. 前端解析返回 JSON → imgUrl 赋值,页面展示最终图片。
  8. status 清空,整个流程结束。

七、可以优化的点 & 生产建议

想把这个玩具变成产品,可以考虑:

  • 环境变量强化workflow_id 也放进 .env,方便切换测试/生产环境。
  • 上传进度条:用 XMLHttpRequestupload.onprogress 或 fetch + ReadableStream 做真实进度。
  • 并发控制:防止用户连点多次“生成”,加 loading 锁。
  • 错误重试:上传/工作流调用失败时,提供重试按钮。
  • 生成历史:保存 imgUrllocalStorage,让用户能看之前生成的图。
  • 更丰富的状态管理:把生成状态抽象成枚举 IDLE / UPLOADING / RUNNING / SUCCESS / ERROR,而不是裸字符串。

写在最后

整篇文章带着大家从 Vue 3 的单文件组件出发,一行一行看懂了图像生成应用的完整前端逻辑,并且配合 Coze 工作流图解理解了后端处理链路。你会发现:

前端不再只是调一个简单的“生成接口”,而是完整参与了文件上传、参数编排、状态流转和结果消费的全过程。

这个模式完全可以复用到 证件照生成、头像风格化、短视频素材处理 等场景。把 AI 能力封装成 Coze 工作流,前端只需做个轻量编排层,开发效率直接起飞。

项目完整代码可直接粘贴到 Vite + Vue 3 项目中使用。