造相-Z-Image-Turbo与Vue.js构建AI绘图平台:前端工程化实践

最近在做一个挺有意思的项目,想给团队内部搞一个AI画图工具。后端用的是已经部署好的造相-Z-Image-Turbo模型,效果挺不错。但问题来了,总不能让大家每次都去调API或者用命令行吧?得有个像模像样的网页界面才行。

这时候,Vue.js就成了我的首选。它上手快、生态好,组件化的思路也特别适合做这种交互复杂、状态繁多的应用。今天就来聊聊,怎么用Vue 3搭起一个功能完整的AI绘图平台前端,把那些看似复杂的参数调整、历史管理、图片加载优化,都变成用户点点按钮就能完成的事。

1. 项目规划与核心功能设计

在动手写代码之前,得先想清楚这个平台到底要做什么。我们的核心目标很简单:让用户能方便地使用造相-Z-Image-Turbo模型生成图片。围绕这个目标,我拆解出了几个关键功能模块。

1.1 核心用户流程与功能模块

用户来到这个平台,最典型的路径是这样的:输入一段描述文字 -> 选择喜欢的画风(LoRA)-> 调整几个关键参数 -> 点击生成 -> 查看结果并保存。基于这个流程,我规划了以下几个前端模块:

  • 提示词输入与管理:这是创作的起点。需要一个好用的文本输入框,支持多行,最好还能有一些预设提示词模板,或者历史记录,方便用户快速复用。
  • 参数控制面板:这是控制生成效果的核心。需要把模型API的关键参数,比如采样步数(steps)、引导强度(CFG scale)、图片尺寸等,做成直观的滑块或输入框,让用户实时调整。
  • LoRA风格选择器:造相-Z-Image-Turbo支持加载不同的LoRA模型来实现特定风格(如动漫风、写实风、油画感)。前端需要提供一个清晰的列表或网格,让用户一眼就能看到所有可选风格,并能轻松切换。
  • 生成任务控制与状态反馈:点击“生成”后,用户需要明确知道任务已提交、正在处理中。这里需要一个加载状态提示,比如旋转的图标或进度条,同时最好能显示预估的等待时间。
  • 生成结果画廊:生成的图片不能只看一次就没了。需要一个画廊来展示所有历史生成结果,支持缩略图预览、大图查看,并且要能流畅地加载和展示可能数量很多的图片。
  • 图片操作功能:用户看到满意的作品,自然想要保存或分享。所以下载(原图)、复制链接、一键分享到社交平台(如果集成)这些功能必不可少。

1.2 技术栈选型:为什么是Vue 3?

面对这些需求,我选择了Vue 3的组合式API生态,主要基于以下几点考虑:

  • 开发体验与效率:Vue的单文件组件(.vue文件)把模板、逻辑和样式放在一起,非常直观。组合式API(setup + ref/reactive)让逻辑关注点更容易组织和复用,这对于我们这种有复杂交互状态(多个参数、生成队列)的应用来说,代码会清晰很多。
  • 响应式系统的优势:Vue的响应式系统是自动的。这意味着当我将生成参数(如steps)绑定到一个滑块组件时,参数值的变化会自动同步到对应的响应式变量上,无需手动监听事件去更新状态,简化了开发。
  • 丰富的生态系统:Vue周边有大量成熟的高质量组件库,如Element Plus、Naive UI、Ant Design Vue等。我们可以直接使用它们提供的表单、按钮、滑块、弹窗等组件,快速搭建出美观且交互一致的界面,把精力集中在业务逻辑上。
  • 状态管理(Pinia)的简洁性:对于跨多个组件共享的状态(比如当前用户选择的LoRA风格、生成历史列表),我们需要一个状态管理方案。Vue官方推荐的Pinia,相比之前的Vuex,API更简洁,对TypeScript的支持也更好,学习成本低,足够应对我们这个项目的复杂度。
  • 性能与优化友好:Vue 3在性能上有显著提升。同时,其设计理念(如Composition API)让我们能更自然地运用代码分割、异步组件等现代前端优化手段,这对于需要加载大量图片的画廊页面至关重要。

确定了“做什么”和“用什么做”,接下来就可以进入具体的搭建环节了。

2. 前端工程化搭建与核心实现

项目从零开始,我习惯先搭好骨架,再填充血肉。这里我使用Vite作为构建工具,因为它启动快、热更新灵敏,开发体验非常好。

# 初始化项目
npm create vue@latest ai-drawing-platform
# 按照提示选择需要的特性:TypeScript, Pinia, Vue Router等
cd ai-drawing-platform
npm install

安装完成后,我会根据之前规划的功能模块,开始组织项目结构。

2.1 项目结构与状态管理设计

一个清晰的项目结构能让后续开发和维护事半功倍。我的目录结构大致如下:

src/
├── assets/          # 静态资源
├── components/      # 可复用组件
│   ├── common/      # 通用组件(按钮、卡片等)
│   ├── drawing/     # 绘图相关业务组件
│   │   ├── PromptInput.vue     # 提示词输入框
│   │   ├── ParamPanel.vue      # 参数控制面板
│   │   ├── LoraSelector.vue    # LoRA风格选择器
│   │   ├── GenerateButton.vue  # 生成按钮与状态
│   │   └── ImageGallery.vue    # 图片画廊
│   └── layout/      # 布局组件
├── composables/     # 组合式函数(逻辑复用)
│   ├── useImageGenerator.ts    # 封装生成图片的API调用
│   └── useGalleryManager.ts    # 封装画廊图片加载逻辑
├── stores/          # Pinia状态仓库
│   └── drawing.ts   # 绘图相关的全局状态
├── views/           # 页面组件
│   └── HomeView.vue # 主绘图页面
├── router/          # 路由配置
├── utils/           # 工具函数
└── App.vue

核心的状态管理集中在Pinia Store (stores/drawing.ts) 中。这里定义了整个应用共享的数据和逻辑。

// stores/drawing.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { GeneratedImage } from '@/types/drawing'

export const useDrawingStore = defineStore('drawing', () => {
  // 状态
  const prompt = ref('') // 当前提示词
  const negativePrompt = ref('') // 反向提示词
  const steps = ref(20) // 采样步数
  const cfgScale = ref(7.5) // CFG引导强度
  const selectedLora = ref<string | null>(null) // 当前选中的LoRA
  const availableLoras = ref([ // 可用的LoRA列表,可从后端获取
    { id: 'anime', name: '动漫风格', thumbnail: '/loras/anime.jpg' },
    { id: 'realistic', name: '写实风格', thumbnail: '/loras/realistic.jpg' },
    // ... 更多风格
  ])
  const isGenerating = ref(false) // 是否正在生成
  const generationHistory = ref<GeneratedImage[]>([]) // 生成历史

  // 计算属性
  const currentParams = computed(() => ({
    prompt: prompt.value,
    negative_prompt: negativePrompt.value,
    steps: steps.value,
    cfg_scale: cfgScale.value,
    lora: selectedLora.value,
    width: 512,
    height: 512,
  }))

  // 动作
  async function generateImage() {
    if (isGenerating.value) return
    isGenerating.value = true
    try {
      // 调用封装好的API函数
      const imageData = await generateImageAPI(currentParams.value)
      // 将生成结果添加到历史记录
      generationHistory.value.unshift({
        id: Date.now(),
        url: imageData.url,
        params: { ...currentParams.value },
        createdAt: new Date().toISOString(),
      })
    } catch (error) {
      console.error('生成失败:', error)
      // 这里可以添加错误提示
    } finally {
      isGenerating.value = false
    }
  }

  function clearHistory() {
    generationHistory.value = []
  }

  return {
    // 状态
    prompt,
    negativePrompt,
    steps,
    cfgScale,
    selectedLora,
    availableLoras,
    isGenerating,
    generationHistory,
    // 计算属性
    currentParams,
    // 动作
    generateImage,
    clearHistory,
  }
})

这个Store成了整个应用的“中央指挥部”,所有组件都通过它来读取和修改共享状态。

2.2 核心组件开发与API集成

状态管理搭好了,接下来就是开发一个个具体的Vue组件。我以参数控制面板和生成按钮为例,展示如何将它们与状态和API连接起来。

首先,在 composables/useImageGenerator.ts 中封装与后端造相-Z-Image-Turbo API的交互。

// composables/useImageGenerator.ts
import { ref } from 'vue'
import axios from 'axios'

// 假设后端API地址
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:7860'

export function useImageGenerator() {
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  async function generate(params: any): Promise<{ url: string }> {
    isLoading.value = true
    error.value = null
    try {
      // 调用造相-Z-Image-Turbo的生成接口
      const response = await axios.post(`${API_BASE_URL}/sdapi/v1/txt2img`, params, {
        responseType: 'json',
        timeout: 300000, // 生成图片可能较久,设置长超时
      })
      // 假设API返回base64编码的图片
      const imageBase64 = response.data.images[0]
      // 转换为可用的Blob URL
      const blob = base64ToBlob(imageBase64, 'image/png')
      const imageUrl = URL.createObjectURL(blob)
      return { url: imageUrl }
    } catch (err: any) {
      error.value = `生成失败: ${err.message}`
      throw err
    } finally {
      isLoading.value = false
    }
  }

  function base64ToBlob(base64: string, mimeType: string): Blob {
    const byteCharacters = atob(base64)
    const byteArrays = []
    for (let offset = 0; offset < byteCharacters.length; offset += 512) {
      const slice = byteCharacters.slice(offset, offset + 512)
      const byteNumbers = new Array(slice.length)
      for (let i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i)
      }
      const byteArray = new Uint8Array(byteNumbers)
      byteArrays.push(byteArray)
    }
    return new Blob(byteArrays, { type: mimeType })
  }

  return {
    isLoading,
    error,
    generate,
  }
}

然后,在 components/drawing/ParamPanel.vue 组件中,我们使用UI组件库(这里以Element Plus为例)来构建直观的控制面板,并绑定到Store中的状态。

<!-- components/drawing/ParamPanel.vue -->
<template>
  <div class="param-panel">
    <h3>生成参数</h3>
    <div class="param-item">
      <label>采样步数 (Steps): {{ steps }}</label>
      <el-slider v-model="steps" :min="10" :max="50" :step="1" show-input />
      <p class="param-hint">步数越高,细节越丰富,但生成越慢。</p>
    </div>
    <div class="param-item">
      <label>引导强度 (CFG Scale): {{ cfgScale }}</label>
      <el-slider v-model="cfgScale" :min="1" :max="20" :step="0.5" show-input />
      <p class="param-hint">值越高,越贴近你的描述,但可能降低多样性。</p>
    </div>
    <!-- 更多参数,如尺寸选择器、种子输入等 -->
  </div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useDrawingStore } from '@/stores/drawing'

const drawingStore = useDrawingStore()
// 使用 storeToRefs 保持响应式
const { steps, cfgScale } = storeToRefs(drawingStore)
</script>

<style scoped>
.param-panel {
  padding: 20px;
  background: #f9f9f9;
  border-radius: 8px;
}
.param-item {
  margin-bottom: 20px;
}
.param-hint {
  font-size: 0.85em;
  color: #666;
  margin-top: 4px;
}
</style>

最后,GenerateButton.vue 组件负责触发生成动作,并显示状态。

<!-- components/drawing/GenerateButton.vue -->
<template>
  <div class="generate-area">
    <el-button
      type="primary"
      :loading="isGenerating"
      :disabled="!prompt"
      @click="handleGenerate"
      size="large"
    >
      {{ buttonText }}
    </el-button>
    <p v-if="!prompt" class="tip">请输入提示词以开始生成</p>
  </div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useDrawingStore } from '@/stores/drawing'

const drawingStore = useDrawingStore()
const { prompt, isGenerating, generateImage } = drawingStore

const buttonText = computed(() => {
  return isGenerating ? '生成中...' : '开始生成'
})

async function handleGenerate() {
  if (!prompt) return
  await generateImage()
}
</script>

通过这样的方式,各个组件各司其职,通过Pinia Store进行通信,逻辑清晰,耦合度低。用户在前端的每一次操作,都通过Store最终转化为对后端造相-Z-Image-Turbo API的一次规范调用。

3. 性能优化与用户体验提升

功能跑通只是第一步。当生成的历史图片越来越多,画廊里可能有几十上百张图时,性能问题就凸显出来了。同时,生成过程中的等待体验也很重要。这部分我们聊聊如何优化。

3.1 图片懒加载与虚拟滚动

ImageGallery.vue 组件如果一次性渲染所有历史图片的DOM元素,在图片数量多时会非常卡顿。解决方案是“懒加载”和“虚拟滚动”。

  • 图片懒加载:只加载当前视口内及附近的图片。我们可以使用 Intersection Observer API 或者现成的Vue指令库(如 vue3-lazyload)来实现。当图片滚动进入视口时,才将其 src 属性设置为真实的图片URL。
  • 虚拟滚动:对于超长列表,只渲染可视区域内的DOM元素。当滚动时,动态计算并更新渲染的内容。这可以借助 vue-virtual-scroller 这类库轻松实现。
<!-- 使用 vue3-lazyload 的简单示例 -->
<template>
  <div class="gallery">
    <div v-for="img in visibleImages" :key="img.id" class="gallery-item">
      <!-- v-lazy 指令会在图片进入视口时加载 -->
      <img v-lazy="img.url" :alt="`生成图片-${img.id}`" @click="preview(img)" />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useDrawingStore } from '@/stores/drawing'
import { storeToRefs } from 'pinia'

const drawingStore = useDrawingStore()
const { generationHistory } = storeToRefs(drawingStore)

// 在实际项目中,这里可以结合虚拟滚动计算 visibleImages
const visibleImages = generationHistory
</script>

3.2 生成状态管理与用户体验

AI生成图片不是瞬间完成的,可能需要十几秒甚至更久。良好的状态反馈至关重要。

  1. 禁用与加载状态:如 GenerateButton.vue 所示,在生成期间,按钮应变为加载状态并禁用,防止重复提交。
  2. 进度提示:如果后端API能提供生成进度(例如通过WebSocket或轮询),前端可以展示一个进度条,让用户有明确的预期。
  3. 任务队列:高级一点的需求是支持任务队列。用户可以连续提交多个生成任务,前端将其加入队列,依次执行,并在界面上展示排队状态、当前任务进度等。这需要前端维护一个更复杂的状态,并与后端进行协同。
  4. 错误处理与重试:网络波动或后端服务暂时不可用时有发生。需要在API调用层做好错误捕获,给用户友好的提示(如“生成失败,请检查网络或稍后重试”),并提供一键重试的按钮。

3.3 本地存储与离线体验

为了提升用户体验,我们可以利用浏览器的本地存储(LocalStorage)来保存一些数据。

  • 保存提示词模板:用户常用的、效果好的提示词组合可以保存下来,方便下次快速选择。
  • 缓存生成历史:将生成历史(至少是元数据,如图片ID、参数、缩略图URL)存储在本地。这样即使刷新页面,用户也能看到之前生成的作品。注意,Blob URL是临时的,刷新后会失效,所以需要将图片数据也以Base64形式存储,或上传到图床获得永久链接。
  • 记住用户偏好:用户习惯的默认参数(如图片尺寸、常用LoRA)可以保存,下次访问时自动应用。
// 在Pinia Store中集成本地存储
export const useDrawingStore = defineStore('drawing', () => {
  // ... 其他状态

  // 从LocalStorage初始化状态
  const savedHistory = localStorage.getItem('drawingHistory')
  const generationHistory = ref<GeneratedImage[]>(
    savedHistory ? JSON.parse(savedHistory) : []
  )

  // 监听历史记录变化,自动保存
  watch(
    generationHistory,
    (newHistory) => {
      localStorage.setItem('drawingHistory', JSON.stringify(newHistory))
    },
    { deep: true }
  )

  // ... 其余逻辑
})

4. 总结

用Vue.js来构建这样一个AI绘图平台的前端,整个过程更像是在搭积木。Pinia负责管理所有活动的“积木块”(状态),各个Vue组件则是不同形状和功能的积木,通过清晰的接口组合在一起。从规划功能、设计状态结构,到实现组件、集成API,再到优化图片加载和用户体验,每一步都需要考虑前后端的协作以及最终用户的使用感受。

回过头看,这个项目的关键不在于某个炫酷的交互效果,而在于如何将复杂的AI模型参数,通过直观的界面呈现出来,让没有技术背景的用户也能轻松创作。Vue的响应式和组件化特性在这里发挥了巨大优势。当然,这里展示的只是一个基础框架,实际项目中还可以加入更多功能,比如图片后期处理(裁剪、滤镜)、社区分享、风格融合等等。

平台搭好后,团队里的设计师和产品同学已经用起来了,反馈还不错。最让我有成就感的是,看到他们通过简单的滑块和下拉菜单,就能不断调整、探索出令人惊喜的AI画作,这大概就是前端工程的价值所在——在复杂的系统与简单的体验之间,架起一座桥梁。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐