GLM-Image前端集成方案:Vue3实现实时图像预览
GLM-Image前端集成方案:Vue3实现实时图像预览
最近在做一个创意工具项目,需要让用户输入文字描述就能快速生成图片,并且能实时看到生成效果。试了几个方案后,发现GLM-Image在文字理解和图像质量上表现不错,特别是对中文描述的理解很到位。
不过官方提供的体验界面比较简单,如果要在自己的产品里集成,就需要自己动手做前端界面。今天就来分享一下,怎么用Vue3把GLM-Image的图像生成能力集成到你的项目里,实现一个用户友好、能实时预览的界面。
1. 为什么选择Vue3 + GLM-Image组合
先说说为什么这么搭配。Vue3现在的生态很成熟,响应式系统用起来特别顺手,尤其是做这种需要实时更新界面的应用。GLM-Image这边,它最大的优势是能准确理解文字指令,特别是中文描述,不会出现那种“画得漂亮但内容不对”的情况。
我试过几个场景,比如让生成“一个穿着汉服的程序员在写代码”,GLM-Image能准确理解“汉服”这个元素,生成的人物穿着确实是汉服风格,而不是随便套个古装。这种对语义的精准把握,对于实际应用来说很重要。
从技术角度看,Vue3的Composition API让代码组织更清晰,特别是处理异步请求和状态管理时,比Options API要灵活不少。GLM-Image的API接口也比较规范,返回的是标准的图片URL,前端处理起来不复杂。
2. 项目环境搭建与基础配置
开始动手前,先确保你的开发环境准备好了。我用的是Vue3 + TypeScript + Vite的组合,这个配置现在算是主流选择,开发体验和构建速度都不错。
2.1 创建Vue3项目
如果你还没有项目,可以用下面这个命令快速创建一个:
npm create vue@latest my-glm-image-app
创建过程中,记得选择TypeScript、Pinia(状态管理)、以及必要的工具链。创建完成后,进入项目目录安装依赖:
cd my-glm-image-app
npm install
2.2 安装必要的依赖
除了基础依赖,我们还需要安装几个专门用到的包:
npm install axios
npm install @vueuse/core
npm install element-plus
axios用来处理API请求,@vueuse/core提供了一些好用的组合式函数,element-plus是UI组件库,能让界面开发更快一些。如果你喜欢其他UI库,比如Ant Design Vue或者Naive UI,也可以按需选择。
2.3 配置GLM-Image API访问
GLM-Image需要通过API Key来访问,这个Key可以在智谱AI的开放平台申请。拿到Key后,不要在代码里硬编码,最好放在环境变量里。
在项目根目录创建.env.local文件:
VITE_GLM_API_KEY=你的API_Key
VITE_GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4
然后在代码里通过import.meta.env来读取这些配置。这样做的安全性更好,也方便不同环境切换配置。
3. 核心组件设计与实现
界面设计上,我打算做一个比较简洁但功能完整的图像生成工具。主要包含几个部分:文字输入区、参数设置区、生成按钮、以及图片预览区。
3.1 创建图像生成组件
先创建一个ImageGenerator.vue组件,这是整个功能的核心:
<template>
<div class="image-generator">
<div class="generator-container">
<!-- 左侧输入区域 -->
<div class="input-section">
<h3>描述你想要生成的图像</h3>
<el-input
v-model="prompt"
type="textarea"
:rows="4"
placeholder="例如:一只可爱的橘猫在阳光下睡觉,背景是温馨的客厅..."
:maxlength="500"
show-word-limit
/>
<div class="params-section">
<h4>生成参数</h4>
<div class="param-row">
<span class="param-label">图片尺寸:</span>
<el-select v-model="selectedSize" size="small">
<el-option
v-for="size in sizeOptions"
:key="size.value"
:label="size.label"
:value="size.value"
/>
</el-select>
</div>
<div class="param-row">
<span class="label">生成数量:</span>
<el-slider
v-model="imageCount"
:min="1"
:max="4"
:step="1"
show-stops
style="width: 200px"
/>
<span class="value">{{ imageCount }}张</span>
</div>
<div class="param-row">
<span class="label">风格强度:</span>
<el-slider
v-model="styleStrength"
:min="0.1"
:max="1.0"
:step="0.1"
style="width: 200px"
/>
<span class="value">{{ styleStrength.toFixed(1) }}</span>
</div>
</div>
<div class="action-buttons">
<el-button
type="primary"
:loading="isGenerating"
:disabled="!prompt.trim()"
@click="generateImages"
>
{{ isGenerating ? '生成中...' : '开始生成' }}
</el-button>
<el-button @click="clearAll">
清空
</el-button>
</div>
</div>
<!-- 右侧预览区域 -->
<div class="preview-section">
<div v-if="generatedImages.length === 0" class="empty-preview">
<div class="empty-icon">
<svg width="64" height="64" viewBox="0 0 64 64">
<!-- 简化的图片图标 -->
<rect x="8" y="8" width="48" height="40" rx="4" fill="#f0f0f0"/>
<circle cx="24" cy="24" r="6" fill="#d9d9d9"/>
<polyline points="40,36 48,44 56,36" fill="none" stroke="#d9d9d9" stroke-width="2"/>
</svg>
</div>
<p>输入描述并点击生成,图片将在这里显示</p>
</div>
<div v-else class="images-grid">
<div
v-for="(image, index) in generatedImages"
:key="index"
class="image-item"
>
<div class="image-wrapper">
<img
:src="image.url"
:alt="`生成的图片 ${index + 1}`"
@load="onImageLoad(index)"
/>
<div v-if="image.loading" class="image-loading">
<div class="loading-spinner"></div>
</div>
</div>
<div class="image-info">
<span>图片 {{ index + 1 }}</span>
<el-button
size="small"
@click="downloadImage(image.url, index)"
>
下载
</el-button>
</div>
</div>
</div>
<!-- 生成状态提示 -->
<div v-if="isGenerating" class="generation-status">
<div class="status-content">
<div class="loading-spinner small"></div>
<p>正在生成图像,请稍候...</p>
<p class="status-tip">生成时间通常需要10-30秒</p>
</div>
</div>
</div>
</div>
<!-- 历史记录 -->
<div v-if="history.length > 0" class="history-section">
<h3>生成历史</h3>
<div class="history-list">
<div
v-for="(item, index) in history"
:key="index"
class="history-item"
@click="loadFromHistory(item)"
>
<div class="history-prompt">{{ item.prompt }}</div>
<div class="history-time">{{ formatTime(item.timestamp) }}</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import axios from 'axios'
// 类型定义
interface GeneratedImage {
url: string
loading: boolean
}
interface HistoryItem {
prompt: string
images: GeneratedImage[]
timestamp: number
}
// 响应式数据
const prompt = ref('')
const selectedSize = ref('1024x1024')
const imageCount = ref(2)
const styleStrength = ref(0.7)
const isGenerating = ref(false)
const generatedImages = ref<GeneratedImage[]>([])
const history = ref<HistoryItem[]>([])
// 可选的图片尺寸
const sizeOptions = [
{ label: '1024x1024', value: '1024x1024' },
{ label: '768x768', value: '768x768' },
{ label: '512x512', value: '512x512' }
]
// 从localStorage加载历史记录
onMounted(() => {
const savedHistory = localStorage.getItem('glmImageHistory')
if (savedHistory) {
try {
history.value = JSON.parse(savedHistory)
} catch (e) {
console.error('加载历史记录失败:', e)
}
}
})
// 生成图片的主函数
const generateImages = async () => {
if (!prompt.value.trim()) {
ElMessage.warning('请输入图片描述')
return
}
isGenerating.value = true
generatedImages.value = []
try {
// 调用GLM-Image API
const response = await axios.post(
`${import.meta.env.VITE_GLM_BASE_URL}/images/generations`,
{
model: 'glm-image',
prompt: prompt.value,
n: imageCount.value,
size: selectedSize.value,
style: styleStrength.value
},
{
headers: {
'Authorization': `Bearer ${import.meta.env.VITE_GLM_API_KEY}`,
'Content-Type': 'application/json'
}
}
)
// 处理返回的图片数据
if (response.data.data && response.data.data.length > 0) {
generatedImages.value = response.data.data.map((item: any) => ({
url: item.url,
loading: true
}))
// 保存到历史记录
const historyItem: HistoryItem = {
prompt: prompt.value,
images: [...generatedImages.value],
timestamp: Date.now()
}
history.value.unshift(historyItem)
if (history.value.length > 10) {
history.value = history.value.slice(0, 10)
}
// 保存到localStorage
localStorage.setItem('glmImageHistory', JSON.stringify(history.value))
ElMessage.success(`成功生成${generatedImages.value.length}张图片`)
} else {
ElMessage.warning('未生成图片,请重试')
}
} catch (error: any) {
console.error('生成图片失败:', error)
ElMessage.error(`生成失败: ${error.response?.data?.message || error.message}`)
} finally {
isGenerating.value = false
}
}
// 图片加载完成时的处理
const onImageLoad = (index: number) => {
generatedImages.value[index].loading = false
}
// 下载图片
const downloadImage = (url: string, index: number) => {
const link = document.createElement('a')
link.href = url
link.download = `generated-image-${index + 1}-${Date.now()}.png`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
ElMessage.success('开始下载图片')
}
// 清空所有内容
const clearAll = () => {
prompt.value = ''
generatedImages.value = []
}
// 从历史记录加载
const loadFromHistory = (item: HistoryItem) => {
prompt.value = item.prompt
generatedImages.value = [...item.images]
}
// 格式化时间显示
const formatTime = (timestamp: number) => {
const date = new Date(timestamp)
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
</script>
<style scoped>
.image-generator {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.generator-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
margin-bottom: 32px;
}
@media (max-width: 768px) {
.generator-container {
grid-template-columns: 1fr;
}
}
.input-section h3 {
margin-bottom: 16px;
color: #333;
}
.params-section {
margin: 24px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.params-section h4 {
margin-bottom: 16px;
color: #555;
}
.param-row {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.param-row:last-child {
margin-bottom: 0;
}
.param-label {
min-width: 80px;
color: #666;
}
.param-row .label {
min-width: 80px;
color: #666;
}
.param-row .value {
margin-left: 12px;
color: #333;
font-weight: 500;
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 24px;
}
.preview-section {
min-height: 400px;
border: 2px dashed #e0e0e0;
border-radius: 12px;
padding: 24px;
background: #fff;
}
.empty-preview {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #999;
}
.empty-icon {
margin-bottom: 16px;
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 20px;
}
.image-item {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
transition: transform 0.2s;
}
.image-item:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.image-wrapper {
position: relative;
aspect-ratio: 1;
background: #f5f5f5;
}
.image-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.image-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.loading-spinner.small {
width: 20px;
height: 20px;
border-width: 2px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.image-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
background: #fff;
}
.generation-status {
margin-top: 24px;
padding: 20px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #bae0ff;
}
.status-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.status-tip {
font-size: 14px;
color: #666;
}
.history-section {
margin-top: 32px;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
}
.history-section h3 {
margin-bottom: 16px;
color: #333;
}
.history-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.history-item {
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
}
.history-item:hover {
background: #e9ecef;
}
.history-prompt {
color: #333;
margin-bottom: 8px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.history-time {
font-size: 14px;
color: #999;
}
</style>
3.2 创建API服务封装
为了更好的代码组织和复用,我们把API调用逻辑单独封装成一个服务。创建src/services/glmImageService.ts:
import axios from 'axios'
export interface ImageGenerationParams {
prompt: string
n?: number
size?: string
style?: number
negative_prompt?: string
}
export interface GeneratedImage {
url: string
revised_prompt?: string
}
export interface GenerationResponse {
data: GeneratedImage[]
created: number
}
class GLMImageService {
private baseURL: string
private apiKey: string
constructor() {
this.baseURL = import.meta.env.VITE_GLM_BASE_URL || 'https://open.bigmodel.cn/api/paas/v4'
this.apiKey = import.meta.env.VITE_GLM_API_KEY || ''
if (!this.apiKey) {
console.warn('GLM-Image API Key未配置,请检查环境变量')
}
}
/**
* 生成图片
*/
async generateImages(params: ImageGenerationParams): Promise<GenerationResponse> {
try {
const response = await axios.post(
`${this.baseURL}/images/generations`,
{
model: 'glm-image',
prompt: params.prompt,
n: params.n || 2,
size: params.size || '1024x1024',
style: params.style || 0.7,
negative_prompt: params.negative_prompt || '',
response_format: 'url'
},
{
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
timeout: 60000 // 60秒超时,图片生成可能需要较长时间
}
)
return response.data
} catch (error: any) {
console.error('GLM-Image API调用失败:', error)
// 提供更友好的错误信息
if (error.response) {
const status = error.response.status
const data = error.response.data
switch (status) {
case 401:
throw new Error('API Key无效或已过期,请检查配置')
case 429:
throw new Error('请求过于频繁,请稍后重试')
case 500:
throw new Error('服务器内部错误,请稍后重试')
default:
throw new Error(data?.message || `请求失败: ${status}`)
}
} else if (error.request) {
throw new Error('网络请求失败,请检查网络连接')
} else {
throw new Error(`请求配置错误: ${error.message}`)
}
}
}
/**
* 批量生成图片(分批次处理,避免单次请求太大)
*/
async batchGenerateImages(
params: ImageGenerationParams,
batchSize: number = 4
): Promise<GeneratedImage[]> {
const totalImages = params.n || 2
const batches = Math.ceil(totalImages / batchSize)
const allImages: GeneratedImage[] = []
for (let i = 0; i < batches; i++) {
const currentBatchSize = Math.min(batchSize, totalImages - i * batchSize)
if (currentBatchSize <= 0) break
const batchParams = {
...params,
n: currentBatchSize
}
try {
const response = await this.generateImages(batchParams)
allImages.push(...response.data)
// 批次间稍微延迟,避免触发限流
if (i < batches - 1) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
} catch (error) {
console.error(`第${i + 1}批次生成失败:`, error)
// 可以选择继续生成剩余批次,或者直接抛出错误
throw error
}
}
return allImages
}
/**
* 验证API Key是否有效
*/
async validateApiKey(): Promise<boolean> {
try {
// 发送一个简单的测试请求
await axios.get(`${this.baseURL}/models`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
})
return true
} catch (error) {
return false
}
}
}
// 导出单例实例
export const glmImageService = new GLMImageService()
3.3 创建状态管理
对于这种有多个组件需要共享状态的应用,用Pinia来做状态管理比较合适。创建src/stores/imageStore.ts:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { glmImageService, type ImageGenerationParams, type GeneratedImage } from '@/services/glmImageService'
export interface GenerationHistory {
id: string
prompt: string
params: ImageGenerationParams
images: GeneratedImage[]
timestamp: number
favorite?: boolean
}
export const useImageStore = defineStore('image', () => {
// 状态
const currentPrompt = ref('')
const generationParams = ref<ImageGenerationParams>({
prompt: '',
n: 2,
size: '1024x1024',
style: 0.7
})
const isGenerating = ref(false)
const generatedImages = ref<GeneratedImage[]>([])
const generationHistory = ref<GenerationHistory[]>([])
const error = ref<string | null>(null)
// 计算属性
const hasImages = computed(() => generatedImages.value.length > 0)
const historyCount = computed(() => generationHistory.value.length)
const favoriteHistory = computed(() =>
generationHistory.value.filter(item => item.favorite)
)
// 生成图片
const generateImages = async () => {
if (!currentPrompt.value.trim()) {
error.value = '请输入图片描述'
return
}
isGenerating.value = true
error.value = null
try {
const params = {
...generationParams.value,
prompt: currentPrompt.value
}
const response = await glmImageService.generateImages(params)
generatedImages.value = response.data
// 保存到历史记录
const historyItem: GenerationHistory = {
id: Date.now().toString(),
prompt: currentPrompt.value,
params: { ...params },
images: [...response.data],
timestamp: Date.now()
}
generationHistory.value.unshift(historyItem)
// 限制历史记录数量
if (generationHistory.value.length > 50) {
generationHistory.value = generationHistory.value.slice(0, 50)
}
// 保存到localStorage
saveHistoryToStorage()
} catch (err: any) {
error.value = err.message || '生成图片失败'
generatedImages.value = []
} finally {
isGenerating.value = false
}
}
// 从历史记录加载
const loadFromHistory = (historyItem: GenerationHistory) => {
currentPrompt.value = historyItem.prompt
generationParams.value = { ...historyItem.params }
generatedImages.value = [...historyItem.images]
}
// 切换收藏状态
const toggleFavorite = (historyId: string) => {
const item = generationHistory.value.find(item => item.id === historyId)
if (item) {
item.favorite = !item.favorite
saveHistoryToStorage()
}
}
// 删除历史记录
const deleteHistory = (historyId: string) => {
generationHistory.value = generationHistory.value.filter(item => item.id !== historyId)
saveHistoryToStorage()
}
// 清空当前生成结果
const clearCurrent = () => {
generatedImages.value = []
currentPrompt.value = ''
error.value = null
}
// 清空所有历史记录
const clearAllHistory = () => {
generationHistory.value = []
localStorage.removeItem('imageGenerationHistory')
}
// 保存历史记录到localStorage
const saveHistoryToStorage = () => {
try {
localStorage.setItem('imageGenerationHistory', JSON.stringify(generationHistory.value))
} catch (e) {
console.error('保存历史记录失败:', e)
}
}
// 从localStorage加载历史记录
const loadHistoryFromStorage = () => {
try {
const saved = localStorage.getItem('imageGenerationHistory')
if (saved) {
generationHistory.value = JSON.parse(saved)
}
} catch (e) {
console.error('加载历史记录失败:', e)
}
}
// 初始化时加载历史记录
loadHistoryFromStorage()
return {
// 状态
currentPrompt,
generationParams,
isGenerating,
generatedImages,
generationHistory,
error,
// 计算属性
hasImages,
historyCount,
favoriteHistory,
// 方法
generateImages,
loadFromHistory,
toggleFavorite,
deleteHistory,
clearCurrent,
clearAllHistory
}
})
4. 高级功能实现
基础功能完成后,可以添加一些提升用户体验的高级功能。
4.1 实时预览优化
图片生成可能需要一段时间,这段时间里给用户一些反馈很重要。我们可以实现一个进度模拟和预览优化的功能。
创建一个useImagePreview组合式函数:
import { ref, computed, onUnmounted } from 'vue'
export function useImagePreview() {
const previewProgress = ref(0)
const estimatedTime = ref(30) // 预估30秒
const progressInterval = ref<NodeJS.Timeout | null>(null)
// 开始模拟进度
const startProgressSimulation = () => {
previewProgress.value = 0
estimatedTime.value = 30
progressInterval.value = setInterval(() => {
// 非线性进度,前期快后期慢
const increment = previewProgress.value < 70 ? 5 : 2
previewProgress.value = Math.min(previewProgress.value + increment, 95)
// 更新预估时间
if (previewProgress.value < 50) {
estimatedTime.value = Math.max(10, estimatedTime.value - 2)
}
}, 1000)
}
// 停止进度模拟
const stopProgressSimulation = () => {
if (progressInterval.value) {
clearInterval(progressInterval.value)
progressInterval.value = null
}
previewProgress.value = 100
setTimeout(() => {
previewProgress.value = 0
}, 1000)
}
// 计算剩余时间
const remainingTime = computed(() => {
if (previewProgress.value >= 95) return '即将完成'
const progressPerSecond = previewProgress.value / (30 - estimatedTime.value)
const remainingSeconds = Math.round((100 - previewProgress.value) / progressPerSecond)
return `${remainingSeconds}秒`
})
// 清理
onUnmounted(() => {
if (progressInterval.value) {
clearInterval(progressInterval.value)
}
})
return {
previewProgress,
estimatedTime,
remainingTime,
startProgressSimulation,
stopProgressSimulation
}
}
4.2 图片处理工具
生成图片后,用户可能还需要一些简单的处理功能。创建一个图片处理工具:
export class ImageProcessor {
/**
* 压缩图片
*/
static async compressImage(
imageUrl: string,
maxWidth: number = 1024,
quality: number = 0.8
): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) {
reject(new Error('无法创建canvas上下文'))
return
}
// 计算新尺寸
let width = img.width
let height = img.height
if (width > maxWidth) {
height = (height * maxWidth) / width
width = maxWidth
}
canvas.width = width
canvas.height = height
// 绘制并压缩
ctx.drawImage(img, 0, 0, width, height)
// 转换为Blob
canvas.toBlob(
(blob) => {
if (blob) {
const compressedUrl = URL.createObjectURL(blob)
resolve(compressedUrl)
} else {
reject(new Error('图片压缩失败'))
}
},
'image/jpeg',
quality
)
}
img.onerror = () => {
reject(new Error('图片加载失败'))
}
img.src = imageUrl
})
}
/**
* 转换为Base64
*/
static async toBase64(imageUrl: string): Promise<string> {
const response = await fetch(imageUrl)
const blob = await response.blob()
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result as string)
reader.onerror = reject
reader.readAsDataURL(blob)
})
}
/**
* 获取图片信息
*/
static async getImageInfo(imageUrl: string): Promise<{
width: number
height: number
size: number
type: string
}> {
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = async () => {
try {
const response = await fetch(imageUrl)
const blob = await response.blob()
resolve({
width: img.width,
height: img.height,
size: blob.size,
type: blob.type
})
} catch (error) {
reject(error)
}
}
img.onerror = () => {
reject(new Error('无法加载图片'))
}
img.src = imageUrl
})
}
}
4.3 批量处理功能
如果需要批量生成图片,可以添加批量处理功能:
<template>
<div class="batch-processor">
<h3>批量图片生成</h3>
<div class="batch-input">
<el-input
v-model="batchPrompts"
type="textarea"
:rows="6"
placeholder="每行一个描述,例如: 一只可爱的橘猫 美丽的日落风景 未来城市夜景"
/>
<div class="batch-info">
<span>共 {{ promptCount }} 个描述</span>
<el-button
size="small"
@click="generateBatch"
:loading="isBatchGenerating"
:disabled="promptCount === 0"
>
批量生成
</el-button>
</div>
</div>
<div v-if="batchResults.length > 0" class="batch-results">
<h4>生成结果</h4>
<div class="result-list">
<div
v-for="(result, index) in batchResults"
:key="index"
class="result-item"
:class="{ 'has-error': result.error }"
>
<div class="result-header">
<span class="prompt">{{ result.prompt }}</span>
<span class="status">
<span v-if="result.status === 'pending'">等待中</span>
<span v-else-if="result.status === 'generating'">生成中</span>
<span v-else-if="result.status === 'success'">成功</span>
<span v-else-if="result.status === 'error'" class="error">失败</span>
</span>
</div>
<div v-if="result.images && result.images.length > 0" class="result-images">
<img
v-for="(image, imgIndex) in result.images"
:key="imgIndex"
:src="image.url"
class="batch-image"
/>
</div>
<div v-if="result.error" class="error-message">
{{ result.error }}
</div>
</div>
</div>
<div class="batch-actions">
<el-button @click="downloadAll">下载全部</el-button>
<el-button @click="clearBatchResults">清空结果</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { glmImageService } from '@/services/glmImageService'
interface BatchResult {
prompt: string
status: 'pending' | 'generating' | 'success' | 'error'
images?: Array<{ url: string }>
error?: string
}
const batchPrompts = ref('')
const isBatchGenerating = ref(false)
const batchResults = ref<BatchResult[]>([])
const promptCount = computed(() => {
return batchPrompts.value
.split('\n')
.filter(prompt => prompt.trim().length > 0)
.length
})
const generateBatch = async () => {
const prompts = batchPrompts.value
.split('\n')
.filter(prompt => prompt.trim().length > 0)
if (prompts.length === 0) {
ElMessage.warning('请输入至少一个描述')
return
}
isBatchGenerating.value = true
batchResults.value = prompts.map(prompt => ({
prompt,
status: 'pending'
}))
// 限制并发数,避免触发API限流
const concurrencyLimit = 3
const results: BatchResult[] = []
for (let i = 0; i < prompts.length; i += concurrencyLimit) {
const batch = prompts.slice(i, i + concurrencyLimit)
const batchPromises = batch.map(async (prompt, index) => {
const resultIndex = i + index
batchResults.value[resultIndex].status = 'generating'
try {
const response = await glmImageService.generateImages({
prompt,
n: 1,
size: '1024x1024'
})
batchResults.value[resultIndex] = {
prompt,
status: 'success',
images: response.data
}
return batchResults.value[resultIndex]
} catch (error: any) {
batchResults.value[resultIndex] = {
prompt,
status: 'error',
error: error.message || '生成失败'
}
return batchResults.value[resultIndex]
}
})
const batchResultsData = await Promise.all(batchPromises)
results.push(...batchResultsData)
// 批次间延迟
if (i + concurrencyLimit < prompts.length) {
await new Promise(resolve => setTimeout(resolve, 2000))
}
}
isBatchGenerating.value = false
// 统计结果
const successCount = results.filter(r => r.status === 'success').length
const errorCount = results.filter(r => r.status === 'error').length
ElMessage.success(`批量生成完成:成功 ${successCount} 个,失败 ${errorCount} 个`)
}
const downloadAll = () => {
// 实现批量下载逻辑
ElMessage.info('批量下载功能开发中')
}
const clearBatchResults = () => {
batchResults.value = []
}
</script>
<style scoped>
.batch-processor {
margin-top: 32px;
padding: 24px;
background: #f8f9fa;
border-radius: 12px;
}
.batch-input {
margin-bottom: 24px;
}
.batch-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
color: #666;
}
.batch-results {
margin-top: 24px;
}
.result-list {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 500px;
overflow-y: auto;
padding-right: 8px;
}
.result-item {
padding: 16px;
background: white;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.result-item.has-error {
border-color: #f56c6c;
background: #fef0f0;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.result-header .prompt {
flex: 1;
color: #333;
font-weight: 500;
}
.result-header .status {
margin-left: 16px;
font-size: 14px;
}
.result-header .status .error {
color: #f56c6c;
}
.result-images {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.batch-image {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #e0e0e0;
}
.error-message {
margin-top: 8px;
padding: 8px;
background: #fef0f0;
border-radius: 4px;
color: #f56c6c;
font-size: 14px;
}
.batch-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
</style>
5. 性能优化与最佳实践
在实际使用中,还需要注意一些性能优化和最佳实践。
5.1 图片懒加载
当生成大量图片时,实现懒加载可以显著提升页面性能:
import { useIntersectionObserver } from '@vueuse/core'
export function useLazyImage() {
const imageRef = ref<HTMLImageElement>()
const isLoaded = ref(false)
const isLoading = ref(false)
const { stop } = useIntersectionObserver(
imageRef,
([{ isIntersecting }]) => {
if (isIntersecting && !isLoaded.value && !isLoading.value) {
loadImage()
}
},
{
rootMargin: '50px'
}
)
const loadImage = () => {
if (!imageRef.value) return
isLoading.value = true
const img = new Image()
img.onload = () => {
if (imageRef.value) {
imageRef.value.src = img.src
isLoaded.value = true
isLoading.value = false
}
}
img.onerror = () => {
isLoading.value = false
}
img.src = imageRef.value.dataset.src || ''
}
onUnmounted(() => {
stop()
})
return {
imageRef,
isLoaded,
isLoading
}
}
5.2 请求缓存
为了避免重复生成相同的图片,可以添加请求缓存:
class RequestCache {
private cache = new Map<string, {
data: any
timestamp: number
expiresIn: number
}>()
constructor(private defaultExpiresIn: number = 5 * 60 * 1000) {}
set(key: string, data: any, expiresIn?: number) {
this.cache.set(key, {
data,
timestamp: Date.now(),
expiresIn: expiresIn || this.defaultExpiresIn
})
}
get(key: string): any | null {
const item = this.cache.get(key)
if (!item) return null
// 检查是否过期
if (Date.now() - item.timestamp > item.expiresIn) {
this.cache.delete(key)
return null
}
return item.data
}
delete(key: string) {
this.cache.delete(key)
}
clear() {
this.cache.clear()
}
// 生成缓存键
static generateKey(params: any): string {
return JSON.stringify(params)
}
}
// 在GLMImageService中使用缓存
export class GLMImageServiceWithCache extends GLMImageService {
private cache = new RequestCache(10 * 60 * 1000) // 10分钟缓存
async generateImages(params: ImageGenerationParams): Promise<GenerationResponse> {
const cacheKey = RequestCache.generateKey(params)
const cached = this.cache.get(cacheKey)
if (cached) {
console.log('使用缓存结果')
return cached
}
const result = await super.generateImages(params)
this.cache.set(cacheKey, result)
return result
}
}
5.3 错误重试机制
网络请求可能会失败,添加重试机制可以提高成功率:
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
delay: number = 1000
): Promise<T> {
let lastError: Error
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (error) {
lastError = error as Error
// 如果不是最后一次重试,等待后继续
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)))
}
}
}
throw lastError
}
// 使用示例
const generateWithRetry = async (params: ImageGenerationParams) => {
return await withRetry(
() => glmImageService.generateImages(params),
3,
1000
)
}
6. 部署与配置
6.1 生产环境配置
在生产环境中,需要确保配置正确且安全:
# .env.production
VITE_GLM_API_KEY=你的生产环境API_Key
VITE_GLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4
VITE_APP_TITLE=AI图像生成工具
VITE_API_TIMEOUT=60000
6.2 构建优化
在vite.config.ts中添加构建优化配置:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'ui-library': ['element-plus'],
'utils': ['axios', '@vueuse/core']
}
}
},
chunkSizeWarningLimit: 1000
},
server: {
proxy: {
'/api': {
target: 'https://open.bigmodel.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api/paas/v4')
}
}
}
})
6.3 部署脚本
可以创建一个简单的部署脚本:
#!/bin/bash
# deploy.sh
echo "开始构建..."
npm run build
echo "构建完成,开始部署..."
# 这里可以根据你的部署方式添加具体命令
# 例如上传到CDN、部署到服务器等
echo "部署完成!"
7. 实际应用中的注意事项
在实际项目中使用这个方案时,有几个点需要注意:
API调用成本:GLM-Image是按调用次数计费的,如果用户量比较大,成本会相应增加。可以考虑添加使用限制,比如每天免费生成次数,或者对高质量参数设置付费。
内容安全:用户可能会输入不适当的内容,需要在前后端都添加内容审核。前端可以做一些关键词过滤,后端调用API时也可以设置安全策略。
用户体验:图片生成需要时间,要给用户明确的反馈。除了进度条,还可以考虑添加预估时间、排队状态显示等。
错误处理:网络可能不稳定,API可能暂时不可用。要有完善的错误处理机制,给用户友好的错误提示,并在可能的情况下自动重试。
移动端适配:如果用户可能在手机上使用,要确保界面在移动设备上也能正常显示和操作。Element Plus本身响应式做得不错,但一些自定义样式可能需要额外调整。
性能监控:在生产环境中,可以添加一些性能监控,比如记录生成成功率、平均生成时间、用户常用参数等,这些数据对优化产品很有帮助。
8. 总结
整体用下来,Vue3 + GLM-Image的组合还是挺顺手的。Vue3的响应式系统和组合式API让前端开发效率很高,GLM-Image的API也比较稳定,图像质量在中文场景下表现不错。
这个方案比较适合需要快速集成AI图像生成能力的项目,比如内容创作工具、电商素材生成、教育应用等。代码结构清晰,功能模块划分明确,后续要扩展新功能或者调整现有功能都比较方便。
如果你也在考虑类似的需求,建议先从简单的版本开始,把核心的生成和预览功能跑通,然后再根据实际需要添加高级功能。过程中可能会遇到一些细节问题,比如图片加载优化、错误处理完善等,但整体框架是可行的。
最后,AI技术发展很快,新的模型和功能不断出现。保持代码的可扩展性很重要,这样未来要切换模型或者添加新特性时,改动成本会比较低。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐

所有评论(0)