从 0 到 1 打造 AI 冰球运动员:Coze 工作流与 Vue3 的深度实战

在这个 AI 爆发的时代,如何快速将一个创意转化为可落地的应用?本文将带你通过字节跳动 旗下的 AI 开发平台 Coze (扣子) 和前端框架 Vue3,一步步实现一个有趣的"冰球宠物拟人化"项目。即使你是零基础的小白,跟着这篇文章也能独立完成你的第一个 AI 应用。

一、 项目背景:让宠物变身冰球明星

本项目源于一个节日活动场景:用户只需上传一张宠物的照片,选择心仪的队服编号、颜色、比赛位置和艺术风格,系统就会通过 Coze AI 工作流 生成一张该宠物"拟人化"后的冰球运动员海报。

核心技术栈

  • Coze (扣子) :负责构建 AI 图片编辑工作流,处理逻辑判断与图像生成。
  • Vue3 (Composition API) :负责前端界面搭建,收集用户输入并展示结果。
  • Coze API:连接前端与 AI 后端,实现数据传输。

二、 后端核心:在 Coze 上搭建 AI 工作流

Coze 的工作流(Workflow)就像一个"加工厂",你只需要把节点(Node)像积木一样连接起来,就能实现复杂的逻辑。

1. 工作流架构解析

根据你提供的截图,我们的工作流由以下节点组成:

  1. 开始节点 (Start) :定义输入参数,包括 picture(用户上传的图片)、style(风格)、uniform_number(编号)等。
  2. 代码节点 (Code)这是提升严谨性的关键。 由于用户输入的可能是数字(如位置 0 代表守门员),代码节点负责将这些原始数据转化为 AI 能听懂的自然语言描述。
  3. imgUnderstand (图片理解) :分析用户上传的宠物图片,提取出宠物的品种、毛色、特征。
  4. 特征提取 (LLM) :结合图片描述和用户选定的参数,生成一段详细的绘图提示词(Prompt)。
  5. 图像生成 (Image Generation) :调用模型,根据提示词生成最终的冰球运动员照片。
  6. 结束节点 (End) :将生成的图片 URL 返回给前端。

2. 代码节点的逻辑 (1.ts)

在工作流中,我们写了一段简单的 TypeScript 代码来处理数据映射:

TypeScript 复制代码
const position = params.position == 0 ? '守门员': (params.position == 1 ? '前锋': '后卫');
const shooting_hand = params.shooting_hand == 0 ? '左手': '右手';

这段代码的作用是:如果用户在前端选了"0",它会自动变成"守门员"字样传给 AI,大大提高了生成图片的准确度。

三、 前端实战:Vue3 业务逻辑详解

前端部分是我们与用户交互的窗口。我们将深入讲解 App.vue 中的每一行核心逻辑。

1. 响应式状态管理

在 Vue3 中,我们使用 ref 来定义需要随用户操作而改变的数据:

JavaScript 复制代码
const uniform_number = ref(10); // 队服编号,默认10
const status = ref('');         // 状态提示:上传中 -> 生成中 -> 成功
const imgUrl = ref('');         // AI 生成后的图片地址
const imgPreview = ref('');      // 用户本地上传后的预览图

2. 图片预览:本地加载而非上传

为了提升用户体验,用户选完图片应立即看到预览,而不是等上传成功。

JavaScript 复制代码
const updateImageData = () => {
  const input = uploadImage.value; // 获取 DOM 元素
  const file = input.files[0];     // 获取选中的文件对象
  
  const reader = new FileReader(); // HTML5 文件读取器
  reader.readAsDataURL(file);      // 将图片转为 Base64 编码字符串
  reader.onload = (e) => {
    imgPreview.value = e.target.result; // 将 Base64 赋给预览图标签
  }
}
  • 核心逻辑 :利用 FileReader 在浏览器本地完成读取,不消耗网络流量,响应极快。

3. 调用 Coze API:两步走战略

由于 Coze 的工作流不能直接接收原始文件流,我们需要先将文件上传到 Coze 服务器换取 file_id

第一步:上传文件
JavaScript 复制代码
const uploadFile = async () => {
  const formData = new FormData(); // 准备表单数据
  formData.append('file', uploadImage.value.files[0]);

  const res = await fetch(uploadUrl, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${patToken}` }, // 使用个人访问令牌鉴权
    body: formData
  });
  const ret = await res.json();
  return ret.data.id; // 返回 file_id
};
第二步:运行工作流

拿到 file_id 后,连同用户选择的样式、位置等参数一起发给工作流:

JavaScript 复制代码
const generate = async () => {
  status.value = '图片上传中...';
  const file_id = await uploadFile(); // 先拿 ID
  
  status.value = "正在生成...";
  const parameters = {
    picture: JSON.stringify({ file_id: file_id }), // 传入图片 ID
    style: style.value,
    position: position.value,
    // ...其他参数
  }

  const res = await fetch(workflowUrl, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${patToken}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ workflow_id, parameters }) // 指定工作流 ID 运行
  });
  
  const ret = await res.json();
  const data = JSON.parse(ret.data); // 解析返回的 JSON 结果
  imgUrl.value = data.data; // 这里的 data.data 是最终生成的图片 URL
};

四、 关键点与避坑指南

  1. 权限与 Token :必须在 Coze 个人设置中申请 PAT_TOKEN(个人访问令牌),并确保该 Token 有权限调用你创建的工作流。
  2. 安全建议 :代码中的 patToken 不应直接写在前端,正式项目中应通过后端转发,防止 Token 泄露。
  3. Vite 环境变量 :在 Vue 项目中,敏感信息(如 Token)建议放在 .env.local 文件中,通过 import.meta.env.VITE_xxx 调用。

五、拓展:Vue3小细节

1. Vue3 数据处理:v-model VS ref

App.vue 中,处理用户输入主要有两种手段:

  • v-model(双向绑定)

    • 适用场景 :队服号码(uniform_number)、颜色选择(uniform_color)等普通表单。
    • 原理:它建立了 HTML 表单元素与 JavaScript 变量之间的"实时同步"。当你在页面输入数字,JS 里的变量会自动更新;如果你在 JS 里修改了变量,页面上的显示也会随之改变。
  • ref(DOM 引用)

    • 适用场景 :文件上传框 <input type="file">
    • 原理 :浏览器出于安全考虑,禁止 JavaScript 脚本直接设置或修改文件上传框的 value。因此,v-model 在这里无法实现"写入",失去了意义。我们必须通过 ref 直接拿到这个 HTML 元素(DOM)本身,才能读取到用户选中的原始文件对象。

2. 属性与事件绑定::src, @change, @click

这是实现界面交互的"三剑客":

  • :src(属性绑定)

    • 在 Vue 中,普通的 src="xxx" 只能指向静态路径。
    • 加上冒号的 :src="imgUrl" 表示这是一个动态变量 。当 AI 生成图片后,JS 将新的地址赋值给 imgUrl.value,Vue 会自动侦测到变化并更新页面图片。
  • @change(改变监听)

    • 用法<input type="file" @change="updateImageData">
    • 作用 :当用户在文件选择器中选中了一张图片,这个事件就会被触发。在项目中,我们利用它来立即读取图片内容并实现本地预览,让用户在点击"生成"前就能看到自己上传的照片。
  • @click(点击监听)

    • 用法<button @click="generate">生成</button>
    • 作用:这是用户发起 AI 生成请求的开关。点击后,程序会依次执行"上传图片到服务器"和"调用 Coze 工作流"这两大步骤。

3. FormData:文件传输的集装箱

uploadFile 函数中,你会看到 new FormData() 的操作。

  • 作用:在进行网络请求时,普通的 JSON 格式(文本流)无法承载二进制的文件数据(图片)。
  • 形象理解:FormData 就像一个特殊的快递盒,专门用来封装文件,以便通过 fetch 安全地发送到服务器。

六、 总结

通过"Coze 工作流 + 代码节点处理 + Vue3 异步请求",我们成功把复杂的 AI 绘图逻辑封装成了一个简单的网页应用。这种"低代码 AI 后端 + 灵活前端"的模式,是目前快速开发 AI 应用的主流选择。

七、App.vue 源码

js 复制代码
<template>
  <div class="container">
    <!-- 左侧输入区域 -->
    <div class="input">
      <!-- 文件上传区域 -->
      <div class="file-input">
        <!-- 
          ref="uploadImage":给这个 input 元素起个名字,方便 JS 操作它
          accept="image/*":只允许选择图片文件
          required:表单提交时必须选文件(但这里我们手动处理,所以主要是语义)
          @change="updateImageData":当用户选择了文件,就调用 updateImageData 函数
        -->
        <input type="file" ref="uploadImage" accept="image/*" required @change="updateImageData" />
      </div>

      <!-- 图片预览:只有 imgPreview 有值时才显示 -->
      <img :src="imgPreview" alt="预览图片" v-if="imgPreview" />

      <!-- 表单设置区域 -->
      <div class="settings">
        <!-- 队服编号 -->
        <div class="selection">
          <label>队服编号: </label>
          <!-- v-model 双向绑定:输入框内容 ↔ uniform_number 变量 -->
          <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 class="selection">
          <label>位置: </label>
          <!-- 注意:value 是数字,但 HTML 中写成字符串也没问题,Vue 会自动转 -->
          <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>

      <!-- 生成按钮 -->
      <div class="generate">
        <button @click="generate">生成</button>
      </div>
    </div>

    <!-- 右侧输出区域 -->
    <div class="output">
      <div class="generated">
        <!-- 显示 AI 生成的图片 -->
        <img :src="imgUrl" alt="AI生成图片" v-if="imgUrl" />
        <!-- 显示状态信息(如"上传中...") -->
        <div v-if="status">{{ status }}</div>
      </div>
    </div>
  </div>
</template>

<script setup>
// 引入 Vue 3 的组合式 API
import { ref, onMounted } from 'vue'

// ======================
// 🔑 响应式数据定义(相当于 Vue 2 的 data)
// ======================

// 表单数据(用 ref 包裹,变成响应式)
const uniform_number = ref(10)     // 默认编号 10
const uniform_color = ref('红')    // 默认红色
const position = ref(0)            // 0=前锋, 1=后卫, 2=守门员
const shooting_hand = ref('0')     // '0'=左手, '1'=右手(注意:这里保持字符串,和 option 一致)
const style = ref('写实')          // 默认风格

// 状态管理
const status = ref('')             // 当前操作状态提示(空 / 上传中 / 生成中...)
const imgUrl = ref('')             // AI 生成的图片 URL

// DOM 引用(用于操作文件输入框)
const uploadImage = ref(null)      // 初始为 null,挂载后指向 input 元素
const imgPreview = ref('')         // 本地预览图的 Base64 URL

// ======================
// ⚙️ 环境变量 & 接口配置
// ======================
const patToken = import.meta.env.VITE_PAT_TOKEN; // 从 .env 文件读取令牌
const uploadUrl = 'https://api.coze.cn/v1/files/upload';
const workflowUrl = 'https://api.coze.cn/v1/workflow/run';
const workflow_id = '7584046122328555530'; // 你的 Workflow ID

// ======================
// 📤 上传文件到 Coze 服务器
// ======================
const uploadFile = async () => {
  const input = uploadImage.value;
  // 检查是否选择了文件
  if (!input?.files || input.files.length === 0) {
    alert('请先选择一张图片!');
    return null;
  }

  // 创建 FormData 对象(用于上传文件)
  const formData = new FormData();
  formData.append('file', input.files[0]); // 把第一个文件加进去

  try {
    // 发送 POST 请求上传文件
    const res = await fetch(uploadUrl, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${patToken}` // 携带认证令牌
      },
      body: formData
    });

    const ret = await res.json();
    console.log('上传响应:', ret);

    // 检查是否上传成功(Coze 返回 code=0 表示成功)
    if (ret.code !== 0) {
      status.value = `上传失败: ${ret.msg}`;
      return null;
    }

    // 返回文件 ID(用于后续 Workflow 调用)
    return ret.data.id;
  } catch (error) {
    status.value = '网络错误,请检查网络或 PAT 令牌';
    console.error('上传出错:', error);
    return null;
  }
};

// ======================
// 🧠 调用 AI Workflow 生成图片
// ======================
const generate = async () => {
  // 重置状态
  imgUrl.value = '';
  status.value = '图片上传中...';

  // 第一步:上传图片,获取 file_id
  const file_id = await uploadFile();
  if (!file_id) return;

  status.value = '图片上传成功,正在生成...';

  // 第二步:准备参数,调用 Workflow
  const parameters = {
    // ⚠️ 注意:Coze 的 picture 参数要求是 JSON 字符串!
    picture: JSON.stringify({ file_id: 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' // 发送 JSON 数据
      },
      body: JSON.stringify({
        workflow_id,
        parameters
      })
    });

    const ret = await res.json();
    console.log('Workflow 响应:', ret);

    if (ret.code !== 0) {
      status.value = `生成失败: ${ret.msg}`;
      return;
    }

    // Coze 返回的是字符串化的 JSON,需要再解析一次
    const data = JSON.parse(ret.data);
    imgUrl.value = data.data; // 获取生成的图片 URL
    status.value = ''; // 清空状态
  } catch (error) {
    status.value = '生成请求失败,请重试';
    console.error('生成出错:', error);
  }
};

// ======================
// 👁️ 图片预览功能
// ======================
const updateImageData = () => {
  const input = uploadImage.value;
  if (!input?.files || input.files.length === 0) return;

  const file = input.files[0];
  console.log('选中的文件:', file);

  // 使用 FileReader 读取文件为 Base64
  const reader = new FileReader();
  reader.readAsDataURL(file); // 异步读取

  // 读取完成后触发
  reader.onload = (e) => {
    imgPreview.value = e.target.result; // 赋值给预览变量
  };
};

</script>

<style scoped>
.container {
  display: flex;
  flex-direction: row;
  align-items: start;
  justify-content: start;
  height: 100vh;
  font-size: 0.85rem;
}

.input {
  display: flex;
  flex-direction: column;
  min-width: 330px;
  padding: 20px;
}

.file-input {
  margin-bottom: 16px;
}

.settings {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 1rem;
}

.selection {
  display: flex;
  align-items: center;
  gap: 8px;
}

.selection input,
.selection select {
  padding: 4px;
}

.generate {
  margin-top: 20px;
}

button {
  padding: 10px 20px;
  border: 1px solid #333;
  background: #f5f5f5;
  cursor: pointer;
}

.output {
  padding: 20px;
  width: 100%;
}

.generated {
  width: 400px;
  height: 400px;
  border: 1px solid #ccc;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #fafafa;
}

.generated img {
  max-width: 100%;
  max-height: 100%;
}
</style>
相关推荐
xiangzhihong84 小时前
Visual Studio 2026 正式发布,带来 AI 原生 IDE 和提升性能
前端
安_4 小时前
为什么 Vue 要用 npm run dev 启动
前端·vue.js·npm
LYFlied4 小时前
【每日算法】LeetCode 437. 路径总和 III
前端·算法·leetcode·面试·职场和发展
爱好读书4 小时前
AI生成流程图
人工智能·流程图
六便士的理想4 小时前
el-table实现滑窗列
前端·vue.js
阿蓝灬4 小时前
Chrome Lighthouse优化
前端·chrome
一水鉴天4 小时前
整体设计 定稿 之 32 增强型领域六边形架构 设计(codebuddy)
开发语言·人工智能·架构
武昌库里写JAVA4 小时前
Java设计模式-(创建型)抽象工厂模式
java·vue.js·spring boot·后端·sql
极限实验室4 小时前
INFINI Labs 产品更新 - Coco AI v0.10 × Easysearch v2.0 联袂上线:UI 全面重构,体验焕然一新
数据库·人工智能·产品