宠物冰球大作战!手把手教你用 Vue 3 + Coze 工作流打造 AI 生图应用

57 阅读8分钟

嘿,各位正在 Vue 进阶之路上的小伙伴们!👋 想象一下:你家那只只会拆家的哈士奇,或者是傲娇的小橘猫,如果穿上冰球服、手持球杆在冰场上驰骋,那是多么酷炫的画面?

今天我们就来整点好玩的!利用 Vue 3 (Composition API) 结合 Coze (扣子) 工作流,带大家从零实现一个“宠物冰球选手 AI 生成器”。这不仅是一个有趣的项目,更是一次 Vue 核心知识点的深度实战。


🚀 项目概览

我们将开发一个单页面应用:

  1. 输入端:用户上传宠物照片,并选择队服颜色、编号、球杆手向、场上位置以及艺术风格。
  2. 处理端:通过 API 先将图片上传至 Coze 服务器,再触发配置好的 Coze 工作流进行 AI 绘图。
  3. 输出端:实时显示处理状态,并最终展示 AI 生成的精美大图。

🛠 第一步:搭建 UI 骨架与“即时预览”魔法

写 Vue 项目,我们通常遵循“先静态后动态”的原则。首先,我们要解决最基础的需求:如何让用户上传图片,并且在还没发给服务器前就能在页面上看到它?

1. 模板结构 (Template)

<template> 中,我们使用原生的 <input type="file">。为了方便在 JS 中操作这个 DOM,我们要给它打上一个“标签”——ref="uploadImage"

HTML

<div class="file-input">
    <input 
      type="file" 
      ref="uploadImage" 
      accept="image/*" 
      @change="updateImageData"
    >
</div>
<img :src="imgPreview" alt="预览图" v-if="imgPreview" />

2. 响应式与预览逻辑 (Script Setup)

这里是 Vue 3 的精华所在。我们引入 ref 来定义响应式数据。

JavaScript

import { ref } from 'vue';

// 1. 定义引用 DOM 的 ref,初始为 null
const uploadImage = ref(null); 
// 2. 定义存储预览图 URL 的响应式变量
const imgPreview = ref('');

const updateImageData = () => {
    // 获取 input 节点中的文件对象
    const input = uploadImage.value;
    if(!input.files || input.files.length === 0) return;
    
    const file = input.files[0]; // 获取第一个文件

    /**
     * 💡 关键点:FileReader
     * 浏览器是不能直接渲染本地二进制文件的。
     * 我们需要 FileReader 对象将文件读取为 DataURL (Base64 编码的字符串)。
     */
    const reader = new FileReader();
    
    // 开始读取
    reader.readAsDataURL(file);
    
    // 异步加载完成后触发
    reader.onload = (e) => { 
        // e.target.result 就是转换后的 URL
        imgPreview.value = e.target.result; 
    };
}

技术点拨:

  • ref(null) :在 Vue 3 中,如果你在模板里用了 ref="xxx",那么在脚本里声明一个同名的ref(null),Vue 会在组件挂载后自动把真实的 DOM 节点赋值给它,在这里用于获取dom元素中用户上传的图片文件。
  • FileReader:这是原生 JS 的 API。对于新手来说,理解“异步”很重要——读取图片需要时间,所以我们必须在 onload 回调里去更新数据。在里面有一个叫做readAsDataURL的api方法,通过这个方法可以把一张二进制的图片转化成浏览器可以直接读取的URL格式,就像一个链接一样点击就可以打开图片。

🎨 第二步:完善表单,给宠物定制装备

有了图片,我们还需要一些“参数”来告诉 AI 怎么画。我们在 HTML 中添加 v-model 双向绑定。

HTML

<div class="settings">
            <div class="selection">
                <lable>队服编号:</lable>
                <input type="number" v-model="uniform_number"/>
            </div>
            <div class="selection">
                <lable>队服颜色:</lable>
                <select v-model="uniform_color">
                    <option value="红">红色</option>
                    <option value="蓝">蓝色</option>
                    <option value="绿">绿色</option>
                    <option value="黄">黄色</option>
                    <option value="紫">紫色</option>
                </select>
            </div>
        <div class="setting">
            <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>

<script setup> 中初始化这些状态: JavaScript

const uniform_number = ref(10);
const uniform_color = ref('红');
const position = ref(0); // 0-守门员, 1-前锋...
const style = ref('国漫');

使用 v-model 后,用户在界面上选了什么,对应的变量就会自动同步。这就是数据驱动视图的魅力!和js的dom编程相比真的是方便的没话说。


⚡ 第三步:核心逻辑——与 Coze 的深度对话

这是整个项目最“硬核”的部分。点击“生成”按钮后,我们需要经历:上传文件 -> 获取 file_id -> 启动工作流 -> 获取结果

1. 配置 API 信息

在实际开发中,出于安全考虑,敏感信息(如 Token)通常放在 .env 环境变量文件中。

JavaScript

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 workflowId = '7584046124295405608'; // 你在 Coze 创建的工作流 ID

meta代表当前的环境变量

2. 状态追踪 (Status)

为了让用户不焦虑(vue聚焦于用户体验),我们要实时反馈当前在干嘛:

JavaScript

const status = ref(''); // 空 -> 上传中 -> 生成中 -> 生成成功

3. 第一阶段:上传文件到 Coze

Coze 的工作流不能直接接收你的本地二进制图,它需要一个服务器上的 file_id

image.png

JavaScript

const uploadFile = async () => {
    const formData = new FormData(); // 创建表单数据对象
    const input = uploadImage.value;
    
    if(!input.files || input.files.length <= 0) return;
    
    // 将文件塞入 FormData,键名通常为 'file'
    formData.append('file', input.files[0]);
    
    const res = await fetch(uploadUrl, {
        method: 'POST',
        headers: {
            // 注意:上传 FormData 时不需要手动设置 Content-Type,浏览器会自动处理
            'Authorization': `Bearer ${patToken}`
        },
        body: formData
    });
    
    const ret = await res.json();
    if(ret.code !== 0) {
        status.value = ret.msg; // 报错提示
        return null;
    }
    return ret.data.id; // 返回上传成功后的唯一标识符
}

这里使用了 FormData 对象——这是 JavaScript 提供的一个用于构造表单数据的原生 Web API。它非常方便:只需通过 .append() 方法添加需要上传的字段或文件,即可在 fetch 请求中直接作为请求体(body)使用。浏览器会自动设置正确的 Content-Type(如 multipart/form-data 并包含 boundary),无需手动配置,让表单提交(尤其是文件上传)更加简洁清晰。

以下是返回结果图:

fade622978064d14bb760f14d6d48580.png

4. 第二阶段:调用工作流

拿到 file_id 后,我们就可以带着所有参数去轰炸 Coze 的服务器了。

JavaScript

const generate = async () => {
    status.value = '图片上传中...';
    const file_id = await uploadFile();
    
    if(!file_id) return;
    status.value = '图片上传成功,正在生成中...';

    // 组装工作流参数
    const parameters = {
        // picture 需要按照 Coze 要求的 JSON 格式传入
        picture: JSON.stringify({ file_id }), 
        style: style.value,
        uniform_number: uniform_number.value,
        uniform_color: uniform_color.value,
        position: position.value,
        shooting_hand: shooting_hand.value,
    };

    // 发起 Post 请求
    const res = await fetch(workflowUrl, {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${patToken}`,
            'Content-Type': 'application/json', // 告诉服务器我们发的是 JSON
        },
        body: JSON.stringify({
            workflow_id: workflowId,
            parameters
        })
    });

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

    /**
     * 💡 关键点:解析响应
     * Coze 的工作流返回数据通常在 ret.data 中,且可能是一个字符串化的 JSON。
     * 我们需要解析后拿取图片 URL。
     */
    const data = JSON.parse(ret.data);
    imgUrl.value = data.data; // 假设工作流最后输出的字段叫 data
    status.value = '图片生成成功!';
};

📺 第四步:展示成果

在模板的右侧或下方,我们预留一个区域展示 AI 的杰作。

HTML

<div class="output">
    <div class="generated">
        <img :src="imgUrl" alt="AI 生成图" v-if="imgUrl" />
        
        <div v-if="status" class="status-bar">{{ status }}</div>
    </div>
</div>

🐖这里大概看看我的coze工作流以及补充一些coze使用说明

image.png

节点说明

  • 代码节点筛选用户输入的数据是否符合标准
  • 图片理解节点让ai把宠物图片转换成详细的文字描述
  • 特征提取节点提取宠物关键特征(你是动物学家,负责从动物描述中提取出该动物(主要是外表)最有独特性的特征,例如特征的肤色,表情,神态,动作。)
  • 最后在图片生成节点生成图片

prompt:

用动物的形象和特征,将该动物拟人为一名宠物儿童冰球员,生成{{style}}风格的冰球球员照片,球员身穿{{uniform_color}}色队服,佩戴同色的冰球头盔,队服号码为{{uniform_number}}号,球员位置是{{position}},用{{shooting_hand}}握着球杆,另一只手空着。该照片图像风格为{{style}}。 动物形象描述 {{description}} 独特外貌特征 {{details}}

注意

  • 照片中应强化动物独特的外貌特征,以增加辨识度
  • 如果球员位置是守门员,画面中应该有冰球球门

coze的一些使用说明:

image.png 在coze界面点击资源库 -> 点击要使用的工作流 -> 上面的url链接中就包含了工作流id

image.png 在首页点击API&SDK里面有许多coze的使用说明智能体,工作流都有调用说明,这里的token就是我项目中使用的VITE_PAT_TOKEN但是它会固定时间刷新,怎么在项目中实时更新我还不会。

image.png 在这里还可以查看调用后的返回值等等·····

🐖我的简陋样式,如果想实战试试不想写样式可以复制过去

<style  scoped>
.container {
    display: flex;
    flex-direction: row;
    align-items: start;
    justify-content: start;
    height: 100vh;
    font-size: .85rem;
  }
  
  .preview {
    max-width: 300px;
    margin-bottom: 20px;
  }
  
  .settings {
    display: flex;
    flex-direction: row;
    align-items: start;
    justify-content: start;
    margin-top: 1rem;
  }
  
  .selection {
    width: 100%;
    text-align: left;
  }
  
  .selection input {
    width: 50px;
  }
  
  .input {
    display: flex;
    flex-direction: column;
    min-width: 330px;
  }
  
  .file-input {
    display: flex;
    margin-bottom: 16px;
  }
  
  .output {
    margin-top: 10px;
    min-height: 300px;
    width: 100%;
    text-align: left;
  }
  
  button {
    padding: 10px;
    min-width: 200px;
    margin-left: 6px;
    border: solid 1px black;
  }
  
  .generate {
    width: 100%;
    margin-top: 16px;
  }
  
  .generated {
    width: 400px;
    height: 400px;
    border: solid 1px black;
    position: relative;
    display: flex;
    justify-content: center;
    /* 水平居中 */
    align-items: center;
    /* 垂直居中 */
  }
  
  .output img {
    width: 100%;
  }
</style>

💡 深度总结:新手必看避坑指南

1. 为什么用 ref 而不是 let?

在 Vue 3 中,普通变量的改变不会触发页面更新。ref 会把数据包装成一个“响应式对象”,当 imgUrl.value 改变时,Vue 会察觉到并自动帮你重绘页面上的 <img> 标签。

2. fetch 请求的细节

  • Headers: 调用 Coze API 必须带上 Authorization
  • JSON.stringify: 我们在 JS 中操作的是对象,但网络传输需要字符串,所以发送前要 stringify,接收后要 parse。

3. 生命周期 onMounted

在示例代码中,我们看到了 onMounted。这是因为在 setup 刚开始执行时,HTML 还没渲染好,uploadImage.valuenull。只有在 onMounted(挂载完成)阶段,我们才能安全地操作 DOM 节点。


🎁 结语与后续

通过这个项目,你不仅掌握了 Vue 3 的基本语法、响应式系统,还学会了如何处理文件上传、如何对接强大的 AI 工作流。这种“前端 UI + 后端 AI 工作流”的模式,是目前 AI 应用开发的主流趋势。