用 Vue 3 + Coze API 快速打造一个爆款 AI 趣味应用:把宠物照变成冰球运动员!

79 阅读8分钟

用 Vue 3 + Coze API 快速打造一个爆款 AI 趣味应用:把宠物照变成冰球运动员!

大家好,今天和大家聊的是基于 Vue 3 + Composition API + Coze Workflow 做了一个小而美的 AI 趣味应用——“AI 冰球运动员生成器”

核心玩法非常简单:用户上传一张宠物的照片,选择队服颜色、号码、位置、持杆手、艺术风格后,一键生成一张“穿冰球装备”的酷炫形象图。生成结果特别适合分享到朋友圈或小红书,传播性极强。

项目整体架构

前端(Vue 3 + Vite)          →   Coze 文件上传 API       →   Coze Workflow 执行
      ↑                           (获取 file_id)             (传入 file_id + 参数)
      └─────────────── fetch ───────────────────────────────┘
                              ↓
                       返回最终生成的图片 URL

核心流程只有两步:

  1. 先把用户上传的图片通过 Coze 的 /v1/files/upload 接口上传,拿到 file_id
  2. file_id 和其他参数(队服颜色、号码、位置等)一起传给事先配置好的 Workflow,Workflow 内部调用大模型 + 图像生成模型完成图生图任务,最终返回生成图片的 URL。

整个前端只需要维护几个响应式状态和两个异步函数,就能实现完整的交互体验。

项目初始化与环境准备

npm init vite
cd ai-hockey
npm install
npm run dev

在项目根目录创建 .env 文件(Vite 默认支持):

VITE_PAT_TOKEN=pat_xxxxxxxxxxxxxx  # Coze 个人访问令牌(Personal Access Token)

提醒:PAT 令牌在 Coze 控制台 → 个人中心 → API 访问中生成,记得勾选「Workflow 执行」和「文件上传」权限。千万不要把 .env 文件提交到 GitHub

核心组件:App.vue 完整解析

03998dfb2be956b19c909a672ec27e78.jpg 下面我们逐段拆解优化后的 App.vue 代码,我会把每一段的关键逻辑、为什么要这样写、容易踩的坑都讲清楚。

1. 模板结构优化

<template>
  <div class="container">
    <!-- 左侧输入区 -->
    <div class="input">
      <div class="file-input">
        <input
          type="file"
          ref="uploadImage"
          accept="image/*"
          @change="updateImagePreview"
          required
        />
      </div>

      <!-- 上传后立即预览 -->
      <img :src="imgPreview" alt="预览图" class="preview" v-if="imgPreview" />

      <!-- 参数设置区 -->
      <div class="settings">
        <div class="selection">
          <label>队服编号:</label>
          <input type="number" min="0" max="99" v-model.number="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>
          </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 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" :disabled="isGenerating">
          {{ isGenerating ? '生成中...' : '生成' }}
        </button>
      </div>
    </div>

    <!-- 右侧输出区 -->
    <div class="output">
      <div class="generated">
        <img :src="imgUrl" alt="生成结果" v-if="imgUrl" />
        <div class="status" v-if="status">{{ status }}</div>
      </div>
    </div>
  </div>
</template>

优化点说明:

  • 给数字输入加了 min/max,防止用户输入 999 这种不合理号码。
  • 按钮在生成过程中禁用并显示“生成中...”,避免重复点击。
  • 预览图加了 class="preview" 方便控制大小。
  • 状态提示单独用 .status 类,便于后续加 loading 动画。

2. Script Setup 核心逻辑

<script setup>
import { ref, computed, 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 = '7584046150103547958'  // 替换成你自己的 workflow id

// 参数状态
const uniform_number = ref(10)
const uniform_color = ref('红')
const position = ref('0')          // 注意:这里用字符串更安全
const shooting_hand = ref('0')
const style = ref('写实')

// UI 状态
const status = ref('')
const imgUrl = ref('')
const imgPreview = ref('')
const uploadImage = ref(null)
const isGenerating = computed(() => !!status)  // 有 status 文字就视为生成中

// 图片本地预览(关键!提升用户体验)
const updateImagePreview = () => {
  const input = uploadImage.value
  if (!input?.files?.length) {
    imgPreview.value = ''
    return
  }

  const file = input.files[0]
  const reader = new FileReader()
  reader.onload = (e) => {
    imgPreview.value = e.target.result
  }
  reader.readAsDataURL(file)
}

// 第一步:上传图片到 Coze 获取 file_id
const uploadFile = async () => {
  const input = uploadImage.value
  if (!input?.files?.length) {
    status.value = '请先选择一张图片'
    return null
  }

  const formData = new FormData()
  formData.append('file', input.files[0])

  try {
    const res = await fetch(uploadUrl, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${patToken}`
        // 注意:FormData 时不要手动设置 Content-Type,浏览器会自动加上 boundary
      },
      body: formData
    })

    const ret = await res.json()
    if (ret.code !== 0) {
      status.value = `上传失败:${ret.msg}`
      return null
    }
    return ret.data.id
  } catch (err) {
    status.value = '网络错误,请检查网络后重试'
    console.error(err)
    return null
  }
}

// 第二步:调用 Workflow 生成图片
const generate = async () => {
  if (!imgPreview.value) {
    status.value = '请先上传一张图片'
    return
  }

  status.value = '图片上传中...'
  const file_id = await uploadFile()
  if (!file_id) return

  status.value = '上传成功,正在生成图片(可能需要 20-40 秒)...'

  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()
    if (ret.code !== 0) {
      status.value = `生成失败:${ret.msg}`
      return
    }

    // Coze Workflow 返回的 data 是字符串,需要解析
    const data = JSON.parse(ret.data)
    imgUrl.value = data.data  // 假设 workflow 最后输出的是图片 URL
    status.value = '生成成功!'
    setTimeout(() => { status.value = '' }, 3000)  // 3秒后自动隐藏成功提示
  } catch (err) {
    status.value = '调用失败,请检查网络或稍后重试'
    console.error(err)
  }
}

onMounted(() => {
  console.log('组件已挂载,可访问 DOM')
})
</script>

关键知识点

1. FileReader + readAsDataURL 的妙用

这是提升用户体验的黄金技巧!很多新手直接等上传完成再显示图片,用户会觉得“卡住了”。而 FileReader 能在文件还未上传时就把图片转成 base64 显示预览,几乎是即时响应。

易错提醒:

  • 一定要在 reader.onload 里赋值,不要在 readAsDataURL 后面同步取值(它是异步的!)
  • 大图片 base64 会很长,但现代浏览器都能轻松处理 10MB 以内的照片,不用担心。
2. FormData 上传时不要手动设置 Content-Type

很多人会习惯性地写:

headers: {
  'Content-Type': 'multipart/form-data'
}

这是错误的!

为什么?

因为真正的 Content-Type 应该是这样的:

text

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

那个 boundary 是一串随机字符串,用来分隔不同字段。只有浏览器自动生成才正确

如果你手动写了,boundary 就没了,服务器根本拆不开你的“包裹”,直接上传失败。

正确做法:只传 Authorization,Content-Type 完全不写

3. Coze Workflow 参数传递技巧

Coze 要求 parameters 里的值必须是字符串。如果你直接传对象:

picture: { file_id: 'xxx' }

会报错“参数格式不合法”。所以必须:

picture: JSON.stringify({ file_id })

在 Workflow 里再用 JSON 解析节点还原成对象。

4. 返回数据双重 JSON.parse 的坑

Coze 的 /workflow/run 接口返回的结构是:

{
  "code": 0,
  "msg": "success",
  "data": "{\"data\":\"https://xxx.jpg\"}"
}

注意 ret.data 是一个字符串化的 JSON!所以必须:

const data = JSON.parse(ret.data)
imgUrl.value = data.data

很多人只 parse 一次就直接取 ret.data.data,结果是 undefined。

5. 状态管理与用户体验

我用了 status 一个 ref 就搞定了所有提示:

  • “图片上传中...”
  • “正在生成(可能需要 20-40 秒)...”
  • 错误信息
  • “生成成功!”

配合 computedisGenerating 禁用按钮,避免重复提交。

6.核心思维

这个 AI 冰球运动员生成器项目看起来功能挺酷,有文件上传、有 API 调用、有图片预览、有生成按钮……但如果剥开所有表象,最核心的思维其实就是 Vue 3 的响应式数据状态管理

整个应用的所有交互、反馈、加载状态、显示隐藏,全都围绕着几个 ref 定义的响应式变量在转。

核心响应式状态一览(就是项目的“大脑”)
const imgPreview = ref('')        // 本地预览图(base64)
const status = ref('')           // 当前提示文字(上传中/生成中/错误/成功)
const imgUrl = ref('')           // 最终生成的图片 URL
const uniform_number = ref(10)   // 队服号码
const uniform_color = ref('红')  // 队服颜色
const position = ref('0')        // 位置
const shooting_hand = ref('0')   // 持杆手
const style = ref('写实')        // 艺术风格

这 8 个 ref,就是整个应用的全部状态。没有 Redux、没有 Pinia、没有 Vuex,就靠这几个小 ref,驱动了所有 UI 变化。

响应式状态如何驱动 UI(模板直接绑定)
<img :src="imgPreview" v-if="imgPreview" />          <!-- 状态变 → 预览图出现/消失 -->
<div v-if="status">{{ status }}</div>               <!-- 状态变 → 提示文字显示 -->
<img :src="imgUrl" v-if="imgUrl" />                  <!-- 状态变 → 生成结果显示 -->
<input v-model="uniform_number" />                  <!-- 双向绑定,用户改 → 状态变 -->
<select v-model="style">...</select>                <!-- 同上 -->
<button :disabled="!!status">生成</button>           <!-- 状态有值 → 按钮禁用 -->

你看,模板里完全没有 if-else 逻辑判断、没有手动操作 DOM,全是声明式地直接绑定到状态上。

只要状态变了,Vue 自动帮你更新对应的 UI。这就是响应式的真正威力!

用户操作 → 状态变化 → UI 自动更新 的完整链路

举几个典型例子:

  1. 用户选择图片

    • 触发 @change="updateImagePreview"
    • 函数里用 FileReader 读取文件 → imgPreview.value = base64字符串
    • imgPreview 变 → <img :src="imgPreview"> 自动显示预览图
    • 完全不需要你手动创建 img 元素、设置 src
  2. 用户点击“生成”按钮

    • 调用 generate() 异步函数
    • 第一步:status.value = '图片上传中...' → UI 立即显示提示 + 按钮禁用
    • 第二步:上传成功 → status.value = '正在生成中...' → 提示文字实时更新
    • 第三步:生成成功 → imgUrl.value = 生成的图片URL,同时 status.value = ''
    • imgUrl 变 → 生成结果图自动出现;status 清空 → 提示消失、按钮恢复
  3. 用户修改队服号码

    • 输入框 v-model 直接改 uniform_number.value
    • 下次点击生成时,parameters.uniform_number 自动取最新值
    • 不需要额外保存、不需要事件传参
为什么这种方式这么牛?

因为它“把什么状态下该显示什么” 这个复杂逻辑,彻底简化成了:

“我只管维护状态,UI 怎么变 Vue 帮我搞定”

你不再需要写:

  • document.getElementById('preview').src = ...
  • button.disabled = true
  • statusDiv.innerText = '上传中...'
  • if (success) showResult() else showError()

这些命令式操作全都没了,取而代之的是声明式的模板绑定。

核心思维总结

Vue 3 Composition API 的终极开发思维就是:

  1. 先思考需要哪些状态(ref)
  2. 模板直接绑定这些状态(v-model / :src / v-if / {{}})
  3. 所有操作(用户交互、API 调用)只做一件事:改变这些状态
  4. UI 自动跟随状态变化更新

状态是单向数据流的源头,一切皆由状态驱动。

这个项目虽然小,但完美体现了这个思维:

  • 没有复杂的组件拆分
  • 没有状态管理库
  • 没有手动 DOM 操作
  • 代码清晰、可维护性极高

以后做更大更复杂的项目,这个思维依然适用——只是状态会从几个 ref 变成 Pinia store,但本质没变:状态驱动视图

项目总结

这个小项目虽然代码量不到 200 行,却完整展示了现代前端 AI 应用的典型开发模式:

  1. 本地即时预览 → 提升体验
  2. 分步异步调用第三方 API → 文件上传 + Workflow 执行
  3. 优雅的状态管理 → 一个 status 搞定所有提示
  4. 细致错误处理 → 网络错误、API 错误都要友好提示