news 2026/5/17 5:24:43

Vue前端+LongCat-Image-Edit:构建现代化动物图像编辑Web应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue前端+LongCat-Image-Edit:构建现代化动物图像编辑Web应用

Vue前端+LongCat-Image-Edit:构建现代化动物图像编辑Web应用

你有没有想过,如果家里的宠物猫能变成熊猫医生会是什么样子?或者让自家的小狗戴上贝雷帽,瞬间变身文艺青年?以前要实现这种创意,你可能需要学习复杂的图像处理软件,但现在,只需要一个简单的Web应用就能搞定。

今天要聊的,就是如何用Vue.js前端框架,结合LongCat-Image-Edit这个强大的动物图像编辑AI服务,搭建一个现代化的Web应用。这个应用能让用户上传动物图片,然后用自然语言描述想要的效果,比如“猫变熊猫医生”、“小狗戴贝雷帽”,AI就能在30秒内生成编辑后的图片。

听起来很酷对吧?但更酷的是,我们自己就能动手搭建这样一个应用。不需要深厚的AI背景,也不需要复杂的服务器配置,只需要一些前端开发的基础知识,就能让这个创意变成现实。

1. 为什么选择Vue + LongCat-Image-Edit?

在开始动手之前,我们先聊聊为什么这个组合特别适合做动物图像编辑应用。

Vue.js的优势大家可能都比较熟悉了——渐进式框架、组件化开发、响应式数据绑定,这些特性让前端开发变得高效又愉快。但你可能不知道的是,Vue在处理图像上传、实时预览、状态管理这些场景时,表现尤其出色。

LongCat-Image-Edit这个AI服务,最大的特点就是“会听人话”。它不像传统的图像编辑器那样需要手动圈选、调整参数,你只需要用自然语言描述想要的效果,比如“给这只猫戴上巫师帽”、“把背景换成森林”,它就能理解并执行。

我最近试用了一下,效果确实让人惊喜。上传一张普通的猫咪照片,输入“变成熊猫医生”,30秒后生成的图片里,猫咪真的穿上了白大褂,手里还拿着听诊器,细节处理得相当自然。

更难得的是,这个服务对中文指令的理解很到位。你不需要翻译成英文,直接用中文描述就行,这对国内用户来说方便太多了。

2. 项目环境搭建与初始化

好了,理论说完了,咱们开始动手。首先得把开发环境准备好。

2.1 创建Vue项目

如果你还没有安装Vue CLI,先装一下:

npm install -g @vue/cli

然后创建新项目:

vue create animal-image-editor

创建过程中,我建议选择这些配置:

  • Vue 3
  • TypeScript(可选,但推荐)
  • Router(需要的话)
  • Vuex/Pinia(状态管理)
  • 其他按需选择

项目创建好后,进入目录安装一些必要的依赖:

cd animal-image-editor npm install axios element-plus vue-axios

这里解释一下为什么要装这些:

  • axios:用来和后端API通信
  • element-plus:UI组件库,让界面好看又省事
  • vue-axios:把axios集成到Vue里用起来更方便

2.2 配置API基础设置

接下来,我们需要配置LongCat-Image-Edit的API地址。在项目根目录创建.env.development文件:

VUE_APP_API_BASE_URL=http://your-longcat-api-server VUE_APP_API_TIMEOUT=30000

注意把your-longcat-api-server换成实际的API地址。这个服务通常部署在GPU服务器上,因为图像生成比较吃算力。

3. 核心组件设计与实现

一个图像编辑应用,核心就是几个关键组件:图片上传、编辑指令输入、结果展示。咱们一个一个来。

3.1 图片上传组件

先创建一个ImageUpload.vue组件:

<template> <div class="upload-container"> <div class="upload-area" @dragover.prevent="handleDragOver" @dragleave="handleDragLeave" @drop="handleDrop" :class="{ 'drag-over': isDragOver }" > <el-upload ref="uploadRef" class="upload-demo" action="#" :auto-upload="false" :on-change="handleFileChange" :show-file-list="false" accept="image/*" > <div class="upload-content"> <el-icon class="upload-icon"><UploadFilled /></el-icon> <div class="upload-text"> <p>点击或拖拽上传图片</p> <p class="upload-hint">支持JPG、PNG格式,最大10MB</p> </div> </div> </el-upload> <div v-if="previewUrl" class="preview-container"> <img :src="previewUrl" alt="预览" class="preview-image" /> <div class="preview-actions"> <el-button type="danger" @click="removeImage">移除</el-button> </div> </div> </div> <div v-if="errorMessage" class="error-message"> <el-alert :title="errorMessage" type="error" show-icon /> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import { UploadFilled } from '@element-plus/icons-vue' import type { UploadFile } from 'element-plus' const emit = defineEmits(['upload-success']) const isDragOver = ref(false) const previewUrl = ref('') const errorMessage = ref('') const uploadRef = ref() const handleDragOver = (e: DragEvent) => { e.preventDefault() isDragOver.value = true } const handleDragLeave = () => { isDragOver.value = false } const handleDrop = (e: DragEvent) => { e.preventDefault() isDragOver.value = false const files = e.dataTransfer?.files if (files && files.length > 0) { handleFile(files[0]) } } const handleFileChange = (file: UploadFile) => { if (file.raw) { handleFile(file.raw) } } const handleFile = (file: File) => { // 验证文件类型 if (!file.type.startsWith('image/')) { errorMessage.value = '请上传图片文件' return } // 验证文件大小(10MB) if (file.size > 10 * 1024 * 1024) { errorMessage.value = '文件大小不能超过10MB' return } // 生成预览 const reader = new FileReader() reader.onload = (e) => { previewUrl.value = e.target?.result as string errorMessage.value = '' // 通知父组件 emit('upload-success', { file, previewUrl: previewUrl.value }) } reader.readAsDataURL(file) } const removeImage = () => { previewUrl.value = '' if (uploadRef.value) { uploadRef.value.clearFiles() } emit('upload-success', null) } </script> <style scoped> .upload-container { width: 100%; max-width: 600px; margin: 0 auto; } .upload-area { border: 2px dashed #dcdfe6; border-radius: 8px; padding: 40px 20px; text-align: center; transition: all 0.3s; background-color: #fafafa; } .upload-area.drag-over { border-color: #409eff; background-color: #ecf5ff; } .upload-content { cursor: pointer; } .upload-icon { font-size: 48px; color: #909399; margin-bottom: 16px; } .upload-text { color: #606266; } .upload-hint { font-size: 12px; color: #909399; margin-top: 8px; } .preview-container { margin-top: 20px; } .preview-image { max-width: 100%; max-height: 300px; border-radius: 4px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); } .preview-actions { margin-top: 16px; } .error-message { margin-top: 16px; } </style>

这个组件做了几件事:

  1. 支持拖拽上传和点击上传
  2. 实时预览上传的图片
  3. 验证文件类型和大小
  4. 提供友好的错误提示

3.2 编辑指令输入组件

图片上传好了,接下来要告诉AI我们想怎么编辑。创建EditInstruction.vue

<template> <div class="instruction-container"> <div class="instruction-header"> <h3>编辑指令</h3> <p class="instruction-hint">用自然语言描述你想要的效果</p> </div> <div class="instruction-input"> <el-input v-model="instruction" type="textarea" :rows="4" placeholder="例如:猫变熊猫医生、小狗戴贝雷帽、给猫咪加上巫师帽..." :maxlength="200" show-word-limit /> </div> <div class="instruction-examples"> <h4>参考示例:</h4> <div class="example-tags"> <el-tag v-for="example in examples" :key="example" class="example-tag" @click="useExample(example)" > {{ example }} </el-tag> </div> </div> <div class="instruction-actions"> <el-button type="primary" :loading="loading" :disabled="!canSubmit" @click="handleSubmit" > {{ loading ? '生成中...' : '开始编辑' }} </el-button> </div> </div> </template> <script setup lang="ts"> import { ref, computed } from 'vue' const emit = defineEmits(['submit']) const instruction = ref('') const loading = ref(false) const examples = [ '猫变熊猫医生', '小狗戴贝雷帽', '给猫咪加上巫师帽', '背景换成森林', '变成卡通风格', '穿上小衣服', '眼睛变大变亮', '加上蝴蝶结' ] const canSubmit = computed(() => { return instruction.value.trim().length > 0 && !loading.value }) const useExample = (example: string) => { instruction.value = example } const handleSubmit = async () => { if (!canSubmit.value) return loading.value = true try { await emit('submit', instruction.value.trim()) } finally { loading.value = false } } defineExpose({ reset: () => { instruction.value = '' loading.value = false } }) </script> <style scoped> .instruction-container { width: 100%; max-width: 600px; margin: 0 auto; } .instruction-header { margin-bottom: 20px; } .instruction-header h3 { margin: 0 0 8px 0; color: #303133; } .instruction-hint { margin: 0; color: #909399; font-size: 14px; } .instruction-input { margin-bottom: 20px; } .instruction-examples { margin-bottom: 24px; } .instruction-examples h4 { margin: 0 0 12px 0; color: #606266; font-size: 14px; } .example-tags { display: flex; flex-wrap: wrap; gap: 8px; } .example-tag { cursor: pointer; transition: all 0.2s; } .example-tag:hover { transform: translateY(-2px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .instruction-actions { text-align: center; } </style>

这个组件的设计考虑了用户体验:

  1. 提供参考示例,降低用户的学习成本
  2. 实时验证输入是否有效
  3. 显示生成状态,让用户知道应用正在工作

3.3 结果展示组件

最后,AI生成的结果需要展示出来。创建ResultDisplay.vue

<template> <div class="result-container"> <div v-if="loading" class="loading-container"> <el-skeleton :rows="3" animated /> <div class="loading-text"> <el-icon class="loading-icon"><Loading /></el-icon> <span>AI正在努力生成中,大约需要30秒...</span> </div> </div> <div v-else-if="result" class="result-content"> <div class="result-header"> <h3>编辑结果</h3> <div class="result-actions"> <el-button @click="downloadImage">下载图片</el-button> <el-button type="primary" @click="emit('edit-again')">再次编辑</el-button> </div> </div> <div class="result-comparison"> <div class="comparison-item"> <h4>原图</h4> <img :src="originalImage" alt="原图" class="comparison-image" /> </div> <div class="comparison-item"> <h4>编辑后</h4> <img :src="result.imageUrl" alt="编辑结果" class="comparison-image" /> <p class="result-instruction">{{ result.instruction }}</p> </div> </div> <div class="result-meta"> <el-tag type="info">生成时间:{{ result.timeCost }}秒</el-tag> <el-tag type="success">分辨率:{{ result.resolution }}</el-tag> </div> </div> <div v-else class="empty-result"> <el-empty description="等待生成结果" /> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import { Loading } from '@element-plus/icons-vue' interface ResultData { imageUrl: string instruction: string timeCost: number resolution: string } const props = defineProps<{ loading: boolean result?: ResultData originalImage: string }>() const emit = defineEmits(['edit-again']) const downloadImage = () => { if (!props.result) return const link = document.createElement('a') link.href = props.result.imageUrl link.download = `edited-${Date.now()}.png` document.body.appendChild(link) link.click() document.body.removeChild(link) } </script> <style scoped> .result-container { width: 100%; max-width: 800px; margin: 0 auto; } .loading-container { text-align: center; padding: 40px 0; } .loading-text { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 20px; color: #606266; } .loading-icon { animation: rotate 2s linear infinite; } @keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .result-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; } .result-header h3 { margin: 0; color: #303133; } .result-comparison { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 20px; } @media (max-width: 768px) { .result-comparison { grid-template-columns: 1fr; } } .comparison-item { text-align: center; } .comparison-item h4 { margin: 0 0 12px 0; color: #606266; } .comparison-image { width: 100%; max-height: 300px; object-fit: contain; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } .result-instruction { margin-top: 12px; color: #909399; font-size: 14px; font-style: italic; } .result-meta { display: flex; gap: 8px; justify-content: center; } .empty-result { padding: 60px 0; } </style>

这个组件展示了:

  1. 生成过程中的加载状态
  2. 原图和编辑结果的对比
  3. 生成结果的元信息(时间、分辨率)
  4. 下载和再次编辑的功能

4. API对接与状态管理

组件准备好了,现在需要让它们能跟后端API通信。这里我们用Pinia来做状态管理。

4.1 创建API服务

先创建一个API服务文件src/services/imageEditService.ts

import axios from 'axios' const API_BASE_URL = process.env.VUE_APP_API_BASE_URL const API_TIMEOUT = parseInt(process.env.VUE_APP_API_TIMEOUT || '30000') const apiClient = axios.create({ baseURL: API_BASE_URL, timeout: API_TIMEOUT, headers: { 'Content-Type': 'multipart/form-data' } }) export interface EditRequest { image: File instruction: string } export interface EditResponse { success: boolean data?: { imageUrl: string timeCost: number resolution: string } error?: string } export class ImageEditService { static async editImage(request: EditRequest): Promise<EditResponse> { try { const formData = new FormData() formData.append('image', request.image) formData.append('instruction', request.instruction) const response = await apiClient.post('/edit', formData) return { success: true, data: { imageUrl: response.data.image_url, timeCost: response.data.time_cost, resolution: response.data.resolution } } } catch (error: any) { console.error('API调用失败:', error) let errorMessage = '生成失败,请重试' if (error.response) { if (error.response.status === 413) { errorMessage = '图片文件太大' } else if (error.response.status === 400) { errorMessage = '指令不明确,请重新描述' } else if (error.response.status === 429) { errorMessage = '请求过于频繁,请稍后再试' } } else if (error.code === 'ECONNABORTED') { errorMessage = '请求超时,请检查网络连接' } return { success: false, error: errorMessage } } } static async getStatus(taskId: string) { try { const response = await apiClient.get(`/status/${taskId}`) return response.data } catch (error) { console.error('获取状态失败:', error) throw error } } }

这个服务类封装了:

  1. 文件上传和表单数据构建
  2. 错误处理和用户友好的错误提示
  3. 超时控制

4.2 创建状态管理Store

用Pinia创建store来管理应用状态。创建src/stores/imageEditStore.ts

import { defineStore } from 'pinia' import { ref } from 'vue' import { ImageEditService, type EditRequest, type EditResponse } from '@/services/imageEditService' interface EditHistory { id: string originalImage: string instruction: string result: EditResponse['data'] timestamp: number } export const useImageEditStore = defineStore('imageEdit', () => { // 状态 const originalImage = ref<string>('') const currentFile = ref<File | null>(null) const instruction = ref<string>('') const isLoading = ref<boolean>(false) const currentResult = ref<EditResponse['data'] | null>(null) const errorMessage = ref<string>('') const editHistory = ref<EditHistory[]>([]) // 计算属性 const hasImage = () => !!originalImage.value const canEdit = () => hasImage() && instruction.value.trim().length > 0 && !isLoading.value const hasResult = () => !!currentResult.value // 动作 const setImage = (file: File, previewUrl: string) => { currentFile.value = file originalImage.value = previewUrl errorMessage.value = '' } const setInstruction = (text: string) => { instruction.value = text } const clearImage = () => { currentFile.value = null originalImage.value = '' currentResult.value = null } const editImage = async (): Promise<boolean> => { if (!currentFile.value || !instruction.value.trim()) { errorMessage.value = '请先上传图片并输入编辑指令' return false } isLoading.value = true errorMessage.value = '' try { const request: EditRequest = { image: currentFile.value, instruction: instruction.value.trim() } const response = await ImageEditService.editImage(request) if (response.success && response.data) { currentResult.value = response.data // 保存到历史记录 const historyItem: EditHistory = { id: Date.now().toString(), originalImage: originalImage.value, instruction: instruction.value, result: response.data, timestamp: Date.now() } editHistory.value.unshift(historyItem) // 只保留最近10条记录 if (editHistory.value.length > 10) { editHistory.value = editHistory.value.slice(0, 10) } return true } else { errorMessage.value = response.error || '生成失败' return false } } catch (error) { errorMessage.value = '网络错误,请检查连接' console.error('编辑失败:', error) return false } finally { isLoading.value = false } } const reset = () => { currentFile.value = null originalImage.value = '' instruction.value = '' currentResult.value = null errorMessage.value = '' isLoading.value = false } const clearError = () => { errorMessage.value = '' } return { // 状态 originalImage, currentFile, instruction, isLoading, currentResult, errorMessage, editHistory, // 计算属性 hasImage, canEdit, hasResult, // 动作 setImage, setInstruction, clearImage, editImage, reset, clearError } })

这个store管理了:

  1. 当前编辑的状态(图片、指令、结果)
  2. 编辑历史记录
  3. 所有与编辑相关的业务逻辑

5. 主页面集成与路由配置

现在把各个组件组合起来,创建主页面。

5.1 创建主页面组件

创建src/views/HomeView.vue

<template> <div class="home-container"> <div class="home-header"> <h1>动物图像编辑工坊</h1> <p class="header-subtitle">上传动物图片,用自然语言描述,AI帮你实现创意</p> </div> <div class="home-content"> <div class="edit-flow"> <!-- 步骤1:上传图片 --> <div class="flow-step" :class="{ 'step-active': !store.hasImage() }"> <div class="step-header"> <div class="step-number">1</div> <h3>上传图片</h3> </div> <div class="step-content"> <ImageUpload @upload-success="handleImageUpload" :disabled="store.isLoading" /> </div> </div> <!-- 步骤2:输入指令 --> <div class="flow-step" :class="{ 'step-active': store.hasImage() && !store.hasResult(), 'step-disabled': !store.hasImage() }"> <div class="step-header"> <div class="step-number">2</div> <h3>输入编辑指令</h3> </div> <div class="step-content"> <EditInstruction ref="instructionRef" @submit="handleEditSubmit" :disabled="!store.hasImage() || store.isLoading" /> </div> </div> <!-- 步骤3:查看结果 --> <div class="flow-step" :class="{ 'step-active': store.hasResult(), 'step-disabled': !store.hasImage() }"> <div class="step-header"> <div class="step-number">3</div> <h3>查看编辑结果</h3> </div> <div class="step-content"> <ResultDisplay :loading="store.isLoading" :result="store.currentResult" :original-image="store.originalImage" @edit-again="handleEditAgain" /> </div> </div> </div> <!-- 错误提示 --> <div v-if="store.errorMessage" class="error-container"> <el-alert :title="store.errorMessage" type="error" show-icon closable @close="store.clearError()" /> </div> <!-- 历史记录 --> <div v-if="store.editHistory.length > 0" class="history-section"> <div class="section-header"> <h3>编辑历史</h3> <el-button type="text" @click="clearHistory" :disabled="store.isLoading" > 清空历史 </el-button> </div> <div class="history-grid"> <div v-for="item in store.editHistory" :key="item.id" class="history-item" @click="viewHistory(item)" > <img :src="item.result?.imageUrl" alt="历史记录" class="history-image" /> <div class="history-info"> <p class="history-instruction">{{ item.instruction }}</p> <p class="history-time">{{ formatTime(item.timestamp) }}</p> </div> </div> </div> </div> </div> </div> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue' import { useImageEditStore } from '@/stores/imageEditStore' import ImageUpload from '@/components/ImageUpload.vue' import EditInstruction from '@/components/EditInstruction.vue' import ResultDisplay from '@/components/ResultDisplay.vue' import type EditInstructionComponent from '@/components/EditInstruction.vue' const store = useImageEditStore() const instructionRef = ref<typeof EditInstructionComponent>() const handleImageUpload = (data: any) => { if (data) { store.setImage(data.file, data.previewUrl) } else { store.clearImage() } } const handleEditSubmit = async (instruction: string) => { store.setInstruction(instruction) const success = await store.editImage() if (success) { // 滚动到结果区域 setTimeout(() => { const resultElement = document.querySelector('.step-3') if (resultElement) { resultElement.scrollIntoView({ behavior: 'smooth' }) } }, 100) } } const handleEditAgain = () => { store.currentResult = null if (instructionRef.value) { instructionRef.value.reset() } } const viewHistory = (item: any) => { // 可以在这里实现查看历史详情的功能 console.log('查看历史:', item) } const clearHistory = () => { store.editHistory = [] } const formatTime = (timestamp: number) => { const date = new Date(timestamp) return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}` } onMounted(() => { // 可以在这里加载保存的历史记录 const savedHistory = localStorage.getItem('editHistory') if (savedHistory) { try { store.editHistory = JSON.parse(savedHistory) } catch (error) { console.error('加载历史记录失败:', error) } } }) </script> <style scoped> .home-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .home-header { text-align: center; margin-bottom: 40px; } .home-header h1 { margin: 0 0 12px 0; color: #303133; font-size: 2.5rem; } .header-subtitle { margin: 0; color: #909399; font-size: 1.1rem; } .edit-flow { display: flex; flex-direction: column; gap: 32px; margin-bottom: 40px; } .flow-step { opacity: 0.6; transition: opacity 0.3s; } .flow-step.step-active { opacity: 1; } .flow-step.step-disabled { pointer-events: none; } .step-header { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; } .step-number { width: 32px; height: 32px; background: #409eff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; } .step-header h3 { margin: 0; color: #303133; } .error-container { margin-bottom: 24px; } .history-section { margin-top: 40px; padding-top: 40px; border-top: 1px solid #ebeef5; } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .section-header h3 { margin: 0; color: #303133; } .history-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } .history-item { cursor: pointer; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); transition: transform 0.2s, box-shadow 0.2s; } .history-item:hover { transform: translateY(-4px); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); } .history-image { width: 100%; height: 150px; object-fit: cover; } .history-info { padding: 12px; background: white; } .history-instruction { margin: 0 0 8px 0; color: #606266; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .history-time { margin: 0; color: #909399; font-size: 12px; } </style>

5.2 配置路由

src/router/index.ts中配置路由:

import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') } ] }) export default router

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

一个Web应用不仅要能用,还要好用。这里分享几个优化点:

6.1 图片压缩与优化

在上传前压缩图片,减少传输时间和服务器压力:

// 在ImageUpload组件中添加 const compressImage = async (file: File, maxWidth = 1024, quality = 0.8): Promise<File> => { return new Promise((resolve, reject) => { const reader = new FileReader() reader.onload = (e) => { const img = new Image() img.onload = () => { const canvas = document.createElement('canvas') let width = img.width let height = img.height if (width > maxWidth) { height = (height * maxWidth) / width width = maxWidth } canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') if (!ctx) { reject(new Error('无法创建画布上下文')) return } ctx.drawImage(img, 0, 0, width, height) canvas.toBlob( (blob) => { if (blob) { const compressedFile = new File([blob], file.name, { type: 'image/jpeg', lastModified: Date.now() }) resolve(compressedFile) } else { reject(new Error('压缩失败')) } }, 'image/jpeg', quality ) } img.onerror = reject img.src = e.target?.result as string } reader.onerror = reject reader.readAsDataURL(file) }) }

6.2 请求防抖与重试

对于编辑请求,添加防抖和自动重试:

// 在store中添加 const editImageWithRetry = async (maxRetries = 2): Promise<boolean> => { let retries = 0 while (retries <= maxRetries) { try { return await store.editImage() } catch (error) { retries++ if (retries > maxRetries) { throw error } // 等待一段时间后重试 await new Promise(resolve => setTimeout(resolve, 1000 * retries)) } } return false }

6.3 本地存储与离线支持

保存用户的历史记录到本地:

// 在store中添加watch watch( () => store.editHistory, (newHistory) => { try { localStorage.setItem('editHistory', JSON.stringify(newHistory)) } catch (error) { console.error('保存历史记录失败:', error) } }, { deep: true } )

7. 部署与上线

开发完成后,需要部署到生产环境。

7.1 构建生产版本

npm run build

这会生成dist目录,里面是优化后的静态文件。

7.2 配置Nginx

创建Nginx配置文件:

server { listen 80; server_name your-domain.com; root /path/to/your/dist; index index.html; # 开启gzip压缩 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; # 静态资源缓存 location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 1y; add_header Cache-Control "public, immutable"; } # API代理 location /api/ { proxy_pass http://your-longcat-api-server/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # Vue路由支持 location / { try_files $uri $uri/ /index.html; } }

7.3 使用Docker部署

创建Dockerfile:

# 构建阶段 FROM node:18-alpine as build-stage WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # 生产阶段 FROM nginx:alpine as production-stage COPY --from=build-stage /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

然后构建和运行:

docker build -t animal-image-editor . docker run -d -p 80:80 --name animal-editor animal-image-editor

8. 总结

整个项目做下来,感觉Vue和LongCat-Image-Edit的配合确实很默契。Vue的响应式系统和组件化开发,让前端逻辑变得清晰可控;而LongCat-Image-Edit的强大AI能力,又让应用有了实实在在的价值。

实际用起来,这个应用的效果比我预想的还要好。特别是看到用户上传的普通宠物照片,经过AI编辑后变成各种有趣的创意作品,那种成就感是实实在在的。而且整个过程对用户来说非常友好——不需要学习复杂的工具,用自然语言描述就能得到想要的效果。

当然,项目还有可以改进的地方。比如可以加入更多编辑选项的预设模板,或者支持批量处理多张图片。性能方面,如果图片生成量大的话,可能需要考虑队列管理和负载均衡。

不过就目前来说,这个应用已经能够很好地满足基本的动物图像编辑需求了。如果你也想尝试搭建类似的应用,建议先从简单的功能开始,逐步完善。最重要的是保持代码的清晰和可维护性,这样后续添加新功能时会轻松很多。


获取更多AI镜像

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/6 21:07:41

ComfyUI插件管理完全指南:从入门到精通的AI绘画工作流优化工具

ComfyUI插件管理完全指南&#xff1a;从入门到精通的AI绘画工作流优化工具 【免费下载链接】ComfyUI-Manager 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-Manager ComfyUI插件管理是AI绘画工作流优化的核心环节&#xff0c;而ComfyUI-Manager作为ComfyUI生态…

作者头像 李华
网站建设 2026/5/8 2:06:41

实测对比后,AI论文平台千笔 VS PaperRed,继续教育写作更高效!

随着人工智能技术的迅猛迭代与普及&#xff0c;AI辅助写作工具已逐步渗透到高校学术写作场景中&#xff0c;成为专科生、本科生、研究生完成毕业论文不可或缺的辅助手段。越来越多面临毕业论文压力的学生&#xff0c;开始依赖各类AI工具简化写作流程、提升创作效率。但与此同时…

作者头像 李华
网站建设 2026/5/1 7:22:18

阿里通义千问进阶版体验:Qwen2.5-7B-Instruct长文写作实测

阿里通义千问进阶版体验&#xff1a;Qwen2.5-7B-Instruct长文写作实测 如果你用过通义千问的轻量版模型&#xff0c;可能会觉得它们像是个“聪明的小助手”&#xff0c;处理日常对话、简单问答绰绰有余。但当你真正需要它帮你写一篇结构严谨的行业报告、创作一个情节完整的故事…

作者头像 李华
网站建设 2026/5/10 9:13:04

ccmusic-database应用场景:音乐治疗中患者偏好流派自动识别与干预建议

ccmusic-database应用场景&#xff1a;音乐治疗中患者偏好流派自动识别与干预建议 1. 音乐治疗的新突破口&#xff1a;为什么流派识别如此关键 在临床音乐治疗实践中&#xff0c;治疗师常常面临一个看似简单却极具挑战性的问题&#xff1a;如何快速、准确地判断一位患者真正偏…

作者头像 李华
网站建设 2026/5/11 3:26:18

中文NLP神器RexUniNLU:一键搞定实体识别与情感分析

中文NLP神器RexUniNLU&#xff1a;一键搞定实体识别与情感分析 1. 引言 1.1 你是不是也遇到过这些事&#xff1f; 写一段电商评论分析脚本&#xff0c;结果卡在命名实体识别上——“iPhone15”被识别成产品名还是品牌&#xff1f; 做舆情监控时&#xff0c;想同时知道“用户…

作者头像 李华