前端也能玩多模态 AI?看 Vue 3 如何调用 Coze 工作流生成萌宠冰球运动员!

290 阅读16分钟

从零构建一个基于 Coze 工作流 + Vue 的 AI 冰球运动员生成器

让毛孩子打进 NHL!用 AI 把宠物变成冰球明星
想象一下:你家那只慵懒的橘猫,头戴护盔、身披红袍、手持球杆,在冰面上凌厉滑行;或是你忠诚的金毛,作为守门员稳如泰山,挡住一记时速 100 公里的射门……这不再是幻想!
在本项目中,我们将借助 Coze 的多模态工作流Vue 3 响应式前端,从零构建一个趣味十足的 AI 应用——「萌宠冰球运动员生成器」。
用户只需上传一张宠物照片,再选择风格、号码、位置等偏好,AI 便会基于真实外貌特征,生成一张拟人化、高辨识度的“冰球明星”图像。
本文将完整拆解整个实现过程:从 Coze 工作流的节点编排、默认值兜底逻辑,到 Vue 前端的文件预览、安全 API 调用与状态管理,手把手带你打通 低代码 AI + 现代前端 的全链路开发。

一、搭建专属的coze工作流

工作流初步搭建

image.png coze平台让我们能够使用低代码的方式搭建一个工作流,我们只需要添加需要的节点完成一些必要的设定即可,让我们继续往下完成它!

完成工作流的节点设计

  1. 开始节点:初步确定需要用到的信息,作为变量输入

image.png
我们需要用户上传一张图片,这是必要的,另外需要用户选择生成图片的风格、队服编号、颜色,希望的位置担当,持杆的手等等

  1. 代码节点:对初步输入的除照片外的数据进行筛选处理

image.png 我们说coze是一个低代码开发平台,但是同样的我们可以使用代码来完成更严谨更准确的数据提取。
为什么需要这个节点?因为大部分的用户可能不想一个个主动的选择每一个需要的数据,所以我们会在选择之前通过一个代码节点为所有数据提供一个默认值,这样做不仅严谨了应用逻辑也优化了用户的体验 对应的代码如下

const random = (start: number, end: number) => {
    const p = Math.random();
    return Math.floor(start * (1 - p) + end * p);
}

async function main({ params }: Args): Promise<Output> {
    if (params.position == null) params.position = random(0, 3);
    if (params.shooting_hand == null) params.shooting_hand = random(0, 2);

    const style = params.style || '写实';
    const uniform_number:string = (params.uniform_number || 10).toString();
    const uniform_color = params.uniform_color || '红';
    const position = params.position  == 0 ? '守门员': (params.position == 1 ? '前锋': '后卫');
    const shooting_hand = params.shooting_hand == 0 ? '左手': '右手';
    const empty_hand = params.shooting_hand ? '左手': '右手';

    // 构建输出对象
    const ret = {
        style,
        uniform_number,
        uniform_color,
        position,
        shooting_hand,
    };

    return ret;
}
  1. 图片理解:处理开始输入的图片,分析图片内容

image.png text变量代表我们希望节点理解图片的哪些内容
提供这样一段prompt:这应该是一张宠物图片,请详细描述宠物的外貌特征

  1. 大模型:根据prompt提取出照片中的萌宠特征

image.png

prompt:“你是动物学家,负责从动物描述中,提取出该动物(主要是外表)里最有独特性的特征,例如特征的肤色、表情、神态、动作等等。”

  1. 图片生成:根据最后处理完的提示词(prompt)生成图片

image.png 这里我们分别添加正向和负向的提示词,如果仅仅添加正向的提示词它会完成你的要求完成图片生成,但是同样的你并没有约束它不能额外的添加其他动作、神态等,使用一段负向的提示词来约束它能够更加精确的生成图片

正向提示词:

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

# 动物形象描述
{{description}}

# 独特外貌特征
{{details}}

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

负向提示词:

球员双手各握一根球杆
球员未佩戴头盔
球员吃东西
画面中出现除了冰球之外的其他球类
地点不在冰球赛场
球员四足站立

6. 结束节点:输出最终结果

image.png

测试工作流

1.链接各个节点 image.png 最终效果:

image.png

好了,如果你也拿到了结果,恭喜你已经成功搭建了一个属于自己的工作流!让我们继续做出一个可以交互的界面吧!

使用Vue搭建前端应用

在现代前端开发中,Vue 3 的响应式系统让我们能够真正聚焦于“数据与业务逻辑”本身,而非繁琐的 DOM 操作。通过声明式模板与组合式 API(Composition API),我们可以将用户交互、状态变化和异步请求清晰地组织在一起,让代码更易读、更易维护。

本项目正是这一理念的实践:我们不需要手动控制图片何时显示、按钮是否禁用、加载提示如何切换——只需定义好数据状态(如 imgPreviewstatusimgUrl),Vue 会自动驱动 UI 同步更新。这种“数据驱动视图”的方式,极大提升了开发效率和用户体验的一致性。

接下来,我们将从零开始,分模块递进式地构建这个 AI 萌宠冰球运动员生成器的前端界面……

第一步:使用 Vite 初始化项目

首先,通过官方脚手架创建一个干净的 Vue 3 项目:

npm init vite

在交互提示中:

  • 输入项目名称
  • 选择Javascript
  • 选择vue
  • 其余跟随默认选项即可

完成后进入目录并安装依赖:

npm i

此时项目结构如下(关键文件):

src/
├── main.js       ← 入口文件
├── App.vue       ← 根组件
└── style.css     ← 全局样式

这与你提供的代码结构完全一致。

验证:运行 npm run dev,应看到默认 Vue 欢迎页。


第二步:实现图片上传与预览(优化初始体验)

用户上传图片后若无反馈,会感到“卡住”。因此我们优先实现本地预览,让用户立刻看到自己选了什么。

1. 简单搭建一个界面-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>
  </div>
</template>


<script setup>
//业务逻辑....
</script>


<style scoped>
* { margin: 0; padding: 0; }
.container {
  display: flex;
  height: 100vh;
}
.input {
  min-width: 330px;
  display: flex;
  flex-direction: column;
}
.file-input {
  margin-bottom: 16px;
}
input[type="file"] {
  font-size: 0.85rem;
}
img {
  max-width: 100%;
  margin-top: 10px;
}
</style>
2. 实现预览逻辑(基于 FileReader)

<script setup> 中:

import { ref, onMounted } from 'vue'

const uploadImage = ref(null)
const imgPreview = ref('') // 响应式预览 URL

const updateImageData = () => {
  const input = uploadImage.value
  if (!input?.files?.length) return

  const file = input.files[0]
  const reader = new FileReader()
  reader.readAsDataURL(file)
  reader.onload = (e) => {
    imgPreview.value = e.target.result // 赋值给响应式变量,自动更新视图
  }
}

关键点解析:

  1. 使用 ref() 声明响应式状态
const uploadImage = ref(null)
const imgPreview = ref('')
  • uploadImage:用于获取 <input type="file"> 的 DOM 引用(通过 ref="uploadImage" 绑定)。
  • imgPreview:存储图片的 Base64 数据 URL,作为 <img :src="imgPreview"> 的数据源。
  • 关键机制ref() 创建的是一个响应式引用对象。当 imgPreview.value 被赋值时,Vue 会自动触发视图更新,无需手动操作 DOM。
  1. 使用ref绑定表单获取DOM元素
const uploadImage = ref(null)
const input = uploadImage.value//获取表单内容

<input>元素上为其添加 ref="uploadImage",这样我们就通过ref创建的响应式对象uploadImage来获取到这个表单元素,虽然我们说vue让我们不用再聚焦于DOM操作,但是凡是都有万一,在我们需要操作DOM元素时就能够通过ref实现
4. 监听文件选择事件

const updateImageData = () => {
  const input = uploadImage.value
  if (!input?.files?.length) return
  // ...
}
  • 当用户选择文件时,<input> 的 change 事件触发 updateImageData
  • input.files 是一个 FileList 对象,包含用户选中的文件。
  • 使用可选链 ?. 安全地检查是否存在有效文件,避免空值错误。
  1. 用 FileReader 读取本地文件
const file = input.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
  • FileReader 是浏览器提供的 Web API,用于异步读取用户计算机上的文件内容。
  • readAsDataURL(file)  方法将文件读取为 Base64 编码的 Data URL(格式如 data:image/jpeg;base64,/9j/4AAQ...)。
  • 这种 URL 可直接作为 <img> 的 src 属性值,在页面中渲染图片,无需上传到服务器

🔒 安全提示:FileReader 只能读取用户主动选择的文件,无法访问任意本地路径,符合浏览器安全策略。


  1. 响应式更新预览图
reader.onload = (e) => {
  imgPreview.value = e.target.result
}
  • 当文件读取完成,FileReader 触发 load 事件,onload 回调执行。
  • e.target.result 即为生成的 Data URL。
  • 将其赋值给 imgPreview.value 后,由于 imgPreview 是响应式变量,Vue 自动更新模板中 <img :src="imgPreview"> 的 src 属性,图片立即显示

效果:用户选择图片后,下方立即显示缩略图,无需等待上传或 AI 处理。

image.png


第三步:引入响应式状态管理(统一控制 UI 反馈)

为了让用户清楚知道“系统正在做什么”,我们需要管理几个关键状态:

  • status:空 / “图片上传中...” / “图片生成中...” / 错误信息
  • imgUrl:最终生成的图片 URL

这些状态将驱动按钮文案、加载提示和结果展示。

在 <script setup> 中定义状态
const status = ref('')      // 当前操作状态
const imgUrl = ref('')      // 生成结果图片 URL
更新模板以反映状态
<template>
  <div class="input">
    <!-- ...图片上传和预览... -->

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

    <!-- 状态提示 -->
    <div v-if="status">{{ status }}</div>
  </div>

  <!-- 输出区域 -->
  <div class="output">
    <div class="generated">
      <img :src="imgUrl" alt="" v-if="imgUrl" />
    </div>
  </div>
</template>

💡 此时 generate 函数尚未实现,但 UI 结构已为状态驱动做好准备。

image.png


第四步:实现图片生成模块(严格遵循 Coze API 规范)

要让前端与我们搭建好的 Coze 工作流打通,必须严格按照 Coze 官方开放平台的 API 调用规范进行请求。执行工作流 - 文档 - 扣子
调用工作流需分两步完成:

  1. 先将用户上传的图片文件上传至 Coze 文件服务,获取 file_id
  2. 再以该 file_id 作为参数,调用工作流执行接口,触发 AI 生成流程。

我们将这两个步骤封装为两个函数,确保请求格式、认证方式和参数结构完全符合 API 要求。


1. 配置安全凭证(PAT Token)

Coze 的 API 要求通过 Personal Access Token (PAT) 进行身份认证。为避免硬编码敏感信息,我们在项目根目录创建 .env.local 文件:

VITE_PAT_TOKEN=your_coze_pat_token_here

⚠️ 注意:

  • 该文件不应提交到 Git(请加入 .gitignore);
  • Vite 会自动将 VITE_ 开头的环境变量注入客户端代码。

在coze中拿到自己的token

image.png 在代码中通过 import.meta.env 安全读取:

const patToken = import.meta.env.VITE_PAT_TOKEN

2. 第一步:上传图片到 Coze 文件服务

根据官方文档上传文件 - 文档 - 扣子,确定正确的格式和配置

实现如下:

const uploadUrl = 'https://api.coze.cn/v1/files/upload'

const uploadFile = async () => {
  status.value = '图片上传中...'
  const input = uploadImage.value
  if (!input?.files?.length) return null

  const formData = new FormData()
  formData.append('file', input.files[0]) // 必须使用 file 字段名

  try {
    const res = await fetch(uploadUrl, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${patToken}` // Bearer 认证
      },
      body: formData
    })

    const ret = await res.json()
    // Coze 成功响应 code 为 0
    if (ret.code !== 0) throw new Error(ret.msg || '文件上传失败')
    
    return ret.data.id // 返回 Coze 分配的 file_id
  } catch (err) {
    status.value = err.message
    return null
  }
}

关键点解析:如何安全、规范地将图片上传至 Coze 文件服务

该函数是前端与 Coze AI 能力对接的第一步——将用户本地图片上传到 Coze 服务器并获取唯一文件标识(file_id 。以下是逐层拆解的关键技术点:

1. 使用 FormData 构造 multipart/form-data 请求体
FormData浏览器原生提供的 JavaScript 对象,用于构造一组键值对(key-value pairs) ,通常用来模拟 HTML 表单的提交数据,特别适用于通过 XMLHttpRequestfetch 向服务器发送 包含文件上传的表单数据

const formData = new FormData()
formData.append('file', input.files[0])
  • 为什么用 FormData
    Coze 的 /v1/files/upload 接口要求以 multipart/form-data 格式上传文件(这是 HTTP 上传二进制文件的标准方式)。
  • 字段名必须为 'file'
    根据 上传文件 - 文档 - 扣子,请求体中必须包含名为 file 的字段,否则会返回错误(如可能遇到的 cannot get access token from Authorization header 实际上常因请求格式错误导致认证失败)。

💡 FormData 会自动设置正确的 Content-Type(含 boundary),无需手动指定。


2. 正确设置 Bearer Token 认证头

headers: {
  Authorization: `Bearer ${patToken}`
}
  • Coze API 使用 OAuth 2.0 Bearer Token 进行身份验证。
  • patToken 来自 .env.local 中的 VITE_PAT_TOKEN,确保敏感信息不硬编码。
  • 若未提供或格式错误(如漏掉 Bearer  前缀),API 会拒绝请求,并可能返回模糊错误(如你上传的 JSON 错误:"cannot get access token from Authorization header")。

⚠️ 注意:Authorization 首字母大写,且 Bearer 后有一个空格,这是标准格式。


3. 使用 fetch 发起异步网络请求

const res = await fetch(uploadUrl, { method: 'POST', headers, body: formData })
  • fetch 是现代浏览器原生支持的 Promise-based HTTP 客户端,无需引入第三方库。
  • 采用 async/await 写法,使异步逻辑更清晰、易读、易调试。

4. 解析响应并处理 Coze 特有的返回结构

const ret = await res.json()
if (ret.code !== 0) throw new Error(ret.msg || '文件上传失败')
return ret.data.id
  • Coze 所有 API 响应均遵循统一格式:

245ece74472acbf555f0355d9724fae8.png

  • code === 0 表示成功;

  • data.id 即为上传后分配的 file_id,后续工作流调用需用此 ID 引用图片。

  • 主动检查 code 并抛出错误,便于统一错误处理(如更新 status.value 显示给用户)。


5. 健壮的输入校验与错误兜底

if(!input.files||input.files?.length===0)
      return;
 if(ret.code !== 0){//如果出错了
      status.value = ret.msg;//msg 错误消息
      return;
    }
}
  • 使用可选链 ?. 安全访问 input.files,避免因 DOM 未挂载或用户未选文件导致运行时错误。
  • 捕获网络异常(如断网、超时)和业务错误(如 token 失效),并通过 status 反馈给用户。

3. 第二步:调用工作流执行生成任务

根据 执行工作流 - 文档 - 扣子,调用需满足:

  • URL 为 POST https://api.coze.cn/v1/workflow/run
  • 请求头包含:
    • Authorization: Bearer <token>
    • Content-Type: application/json
  • 请求体为 JSON,包含 workflow_id 和 parameters

📌 注意:parameters 中的字段名必须与你在 Coze 工作流「开始节点」中定义的输入变量名完全一致

实现主生成函数:

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

    // workflow 调用
    const parameters ={
      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,
    }
    

    const res = await fetch(workflowUrl,{
      method:'POST',
      headers:{
        //请求头
        'Authorization':`Bearer ${patToken}`,// 授权信息 Bearer 令牌
        'Content-Type':'application/json',// 内容类型 json 字符串
      },
      body:JSON.stringify({
        workflow_id,
        parameters
      })
      
    });
    const ret =await res.json();
    if(ret.code !== 0){//如果出错了
      status.value = ret.msg;//msg 错误消息
      return;
    }
   const data =JSON.parse(ret.data);
   console.log(data);
   status.value='';
   imgUrl.value=data.data;
   
  }

关键点解析: 1. 状态驱动用户体验(UX)

status.value = '图片上传中...';
// ...
status.value = '图片上传成功,正在生成图片...';
// ...
status.value = ''; // 成功后清空
  • 使用 status 响应式变量向用户实时反馈系统状态
  • 避免“点击按钮后无反应”的黑盒体验;
  • 错误时也通过 status.value = ret.msg 显示具体原因(如 token 失效、参数错误等)。

💡 这正是 Vue “数据驱动视图”的优势:你只需改数据,UI 自动同步。


2. 安全地引用上传后的文件 ID

picture: JSON.stringify({ file_id })
  • 为什么必须 JSON.stringify
    Coze 要求图片输入字段的值是一个 字符串化的 JSON 对象,格式为 '{"file_id":"xxx"}'
  • 如果直接传 { file_id }(JS 对象),Coze 会因类型不匹配而忽略或报错;
  • 如果只传 file_id 字符串,Coze 无法识别为文件引用。

⚠️ 安全提示:file_id 是 Coze 分配的临时标识,不包含用户原始文件路径或敏感信息,可安全传输。


3. 严格对齐工作流输入参数

const parameters ={
      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,
    }
  • 所有 key(如 styleposition必须与 Coze 工作流「开始节点」中定义的变量名完全一致(包括大小写);
  • 值来自 Vue 的 ref() 响应式变量,确保用户选择能实时传递;
  • 若字段名不匹配,Coze 会使用默认值(如你在代码节点中设置的随机值),可能导致结果不符合预期。

4. 符合 Coze API 规范的请求构造

fetch(workflowUrl, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${patToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ workflow_id, parameters })
})
要素说明
URLhttps://api.coze.cn/v1/workflow/run(官方端点)
Method必须为 POST
AuthorizationBearer Token 认证,patToken 来自 .env.local,避免硬编码
Content-Type必须为 application/json,因为 body 是 JSON 字符串
Body 结构必须包含 workflow_id 和 parameters 两个顶层字段

📌 任何一项不符合规范,Coze 都会返回 code !== 0 的错误。


5. 双重 JSON 解析:理解 Coze 的响应结构

const ret = await res.json();       // 第一层:{ code, msg, data: "字符串" }
const data = JSON.parse(ret.data);  // 第二层:解析字符串得到 { data: "图片URL" }
imgUrl.value = data.data;

image.png

  • 第一层解析res.json() 得到 Coze 标准响应:

    
    { "code": 0, "msg": "", "data": "{"data":"https://..."}" }
    
  • 第二层解析ret.data 是一个 JSON 字符串,需再次 JSON.parse() 才能得到实际结果对象;

  • 最终图片 URL 存在于 data.data 中(由你的工作流“结束节点”输出决定)。

🔍 为什么这样设计?
Coze 将工作流的任意输出(文本、URL、结构化数据)统一序列化为字符串存入 data 字段,以保持 API 响应结构稳定。


6. 健壮的错误处理机制

if (ret.code !== 0) {
  status.value = ret.msg;
  return;
}
  • Coze 所有 API 成功时 code === 0,否则为错误码;
  • ret.msg 包含人类可读的错误信息(如 “Invalid workflow_id”);
  • 提前 return 阻止后续逻辑执行,避免无效操作。

🔍 关键细节说明

  • picture 字段必须是字符串化的 JSON 对象 {"file_id": "..."},这是 Coze 图片输入的标准格式;
  • ret.data 是一个字符串,需用 JSON.parse() 解析后才能获取内部数据;
  • 所有用户选项(如 styleposition 等)都通过 ref() 响应式变量管理,确保实时同步。

4. 补全用户选项状态

<script setup> 中声明所有可配置项:

const uniform_number = ref(10)
const uniform_color = ref('红')
const position = ref(0)        // 0:守门员, 1:前锋, 2:后卫
const shooting_hand = ref(0)   // 0:左手, 1:右手
const style = ref('写实')       // 默认风格

并在模板中绑定对应的 <select><input> 元素(参考你原始 App.vue),确保用户选择能正确传递给 parameters


通过以上步骤,我们严格遵循 Coze 官方 API 规范,完成了从前端上传到 AI 生成的完整链路。整个过程清晰、可靠,且具备良好的错误处理能力,为用户提供流畅的生成体验。


🧱 小结:从零到一的模块化演进路径

本项目采用 “分步聚焦、逐层叠加” 的开发策略,将一个看似复杂的 AI 应用拆解为四个清晰可执行的阶段。每个阶段都解决一个核心问题,同时为下一阶段打下坚实基础:

阶段核心目标关键技术与理念
1. 项目初始化搭建可运行的现代前端骨架使用 Vite + Vue 3 快速创建工程化项目,拥抱 Composition API 与响应式编程范式
2. 即时反馈体验让用户“所见即所得”利用 FileReader 实现本地图片预览,消除上传等待焦虑,提升交互信心
3. 状态驱动 UI统一管理应用生命周期状态通过 ref() 声明 statusimgUrl 等响应式状态,实现“数据变 → 视图自动更新”的声明式 UI
4. 安全对接 Coze AI完成端到端 AI 能力集成严格遵循 Coze API 规范:FormData 上传文件 → 获取 file_id → 调用工作流 → 双重解析响应,确保每一步都可靠、可调试

💡 这种 “先体验,再能力;先本地,再云端” 的演进方式,不仅降低了开发复杂度,也让用户始终处于“可控、可知、可感”的交互环境中——这正是优秀 AI 应用的核心体验准则。

通过这四步,我们不仅完成了一个功能完整的“萌宠冰球运动员生成器”,更实践了一套可复用于其他 Coze + Vue AI 项目的标准化开发流程。

完整代码

<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>
            <option value="蓝">蓝色</option>
            <option value="绿">绿色</option>
            <option value="黄">黄色</option>
            <option value="白">白色</option>
            <option value="黑">黑色</option>

          </select>
        </div>
      </div>
      <div class="settings">
        <div class="selection">
          <label >位置:
            <select v-model="position">
              <option value="0">守门员</option>
              <option value="1">前锋</option>
              <option value="2">后卫</option>
            </select>
          </label>
        </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">
        <img :src="imgUrl" alt="" v-if="imgUrl">
        <div v-if="status">{{ status }}</div>
      </div>
    </div>
  </div>
</template>

<script setup>
  import { ref ,onMounted} from 'vue'
  // script+setuo 是vue3 最好的代码组织方式
  //composition api 组合
  // 直接在 script setup 中定义函数,就可以在模板中直接调用
  //用于标记一个DOM 对象,如果要做就用ref
  //未挂载前null,
  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 =''
  console.log(patToken);
  
  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 imgUrl = ref('');// 生成的图片url
  // 生成图片模块
  const generate =async ()=>{
    status.value = '图片上传中...';
    const file_id =await uploadFile();
    if(!file_id){
      return;
    }
    status.value = '图片上传成功,正在生成图片...';

    // workflow 调用
    const parameters ={
      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,
    }
    

    const res = await fetch(workflowUrl,{
      method:'POST',
      headers:{
        //请求头
        'Authorization':`Bearer ${patToken}`,// 授权信息 Bearer 令牌
        'Content-Type':'application/json',// 内容类型 json 字符串
      },
      body:JSON.stringify({
        workflow_id,
        parameters
      })
      
    });


    const ret =await res.json();
    if(ret.code !== 0){//如果出错了
      status.value = ret.msg;//msg 错误消息
      return;
    }
   const data =JSON.parse(ret.data);
   console.log(data);
   status.value='';
   imgUrl.value=data.data;
   
    
  }
  //先上传到coze 服务器
  const uploadFile =async ()=>{
    // post 请求体 http 协议
    const formData =new FormData();// 表单提交数据
    const input =uploadImage.value;
    if(!input.files||input.files.length===0)
      return;
    formData.append('file',input.files[0]);// 向请求体里加上文件

    //向coze 发送http 请求 上传
    const res =await fetch(uploadUrl,{
      method:'POST',
      headers:{
        //请求头
        'Authorization':`Bearer ${patToken}`,// 授权信息 Bearer 令牌
        
      },
      body:formData
    })
    const ret =await res.json();
    console.log(ret);
    
    if(ret.code !== 0){//如果出错了
      status.value = ret.msg;//msg 错误消息
      return;
    }
    return ret.data.id;
  }

  // 图片预览模块
  const uploadImage = ref(null);
  const imgPreview = ref('');//声明了响应式对象
  // 挂载了 null -> dom 对象 变化
  onMounted(()=>{
    console.log(uploadImage.value);
  })
const updateImageData = ()=>{
  //html5 文件对象
  //console.log(uploadImage.value.files[0]);
  const input =uploadImage.value;
  if(!input.files||input.files.length===0){
    return;
  }
  const file =input.files[0];//文件对象 html5新特性
  console.log(file);
  // FileReader 文件阅读对象
  const reader =new FileReader();
  reader.readAsDataURL(file);//url 异步
  reader.onload =(e)=>{//读完之后
    // console.log(e.target.result);
    imgPreview.value = e.target.result;
    
  }
  

  
  
}
</script>

<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>