feat: implement Home view with image generation
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
import api from './index'
|
||||||
|
|
||||||
|
export interface PromptOptimizeRequest {
|
||||||
|
prompt: string
|
||||||
|
mode: 'text2image' | 'image2image'
|
||||||
|
original_description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageGenerateRequest {
|
||||||
|
prompt: string
|
||||||
|
optimized_prompt: string
|
||||||
|
mode: 'text2image' | 'image2image'
|
||||||
|
model: string
|
||||||
|
input_image_b64?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imagesApi = {
|
||||||
|
optimizePrompt(data: PromptOptimizeRequest) {
|
||||||
|
return api.post<{ optimized_prompt: string }>('/images/optimize', data)
|
||||||
|
},
|
||||||
|
generateImage(data: ImageGenerateRequest) {
|
||||||
|
return api.post<{ image_url: string; model: string }>('/images/generate', data)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50">
|
||||||
|
<nav class="bg-white shadow">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 py-4 flex justify-between items-center">
|
||||||
|
<h1 class="text-xl font-bold">ImageCreator</h1>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<router-link to="/" class="text-blue-500">生成</router-link>
|
||||||
|
<router-link to="/history" class="text-blue-500">历史</router-link>
|
||||||
|
<n-button size="small" @click="handleLogout">退出</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||||
|
<n-tabs v-model:value="activeMode" type="segment">
|
||||||
|
<n-tab-pane name="text2image" tab="文生图" />
|
||||||
|
<n-tab-pane name="image2image" tab="图生图" />
|
||||||
|
</n-tabs>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<n-card title="提示词输入">
|
||||||
|
<n-input
|
||||||
|
v-model:value="originalPrompt"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="描述你想要生成的图片..."
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
<div class="mt-4">
|
||||||
|
<n-button type="primary" block @click="handleOptimize" :loading="optimizing">
|
||||||
|
优化提示词
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-if="optimizedPrompt" class="mt-4">
|
||||||
|
<n-divider>优化后</n-divider>
|
||||||
|
<p class="text-gray-700 whitespace-pre-wrap">{{ optimizedPrompt }}</p>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card v-if="activeMode === 'image2image'" title="上传参考图片" class="mt-4">
|
||||||
|
<n-upload
|
||||||
|
accept="image/*"
|
||||||
|
:max-size="10 * 1024 * 1024"
|
||||||
|
@change="handleImageUpload"
|
||||||
|
@error="handleUploadError"
|
||||||
|
>
|
||||||
|
<n-button>选择图片</n-button>
|
||||||
|
</n-upload>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<n-card title="生成结果">
|
||||||
|
<div v-if="generating" class="flex justify-center py-8">
|
||||||
|
<n-spin size="large" />
|
||||||
|
</div>
|
||||||
|
<div v-else-if="generatedImageUrl" class="text-center">
|
||||||
|
<img :src="generatedImageUrl" class="max-w-full rounded" alt="Generated" />
|
||||||
|
<n-button type="primary" class="mt-4" tag="a" :href="generatedImageUrl" download>
|
||||||
|
下载图片
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center text-gray-400 py-8">
|
||||||
|
生成的图片将显示在这里
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
@click="handleGenerate"
|
||||||
|
:disabled="!optimizedPrompt"
|
||||||
|
:loading="generating"
|
||||||
|
>
|
||||||
|
生成图片
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NCard, NInput, NButton, NTabs, NTabPane, NDivider, NSpin, NUpload, useMessage, useDialog } from 'naive-ui'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { imagesApi } from '../api/images'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const activeMode = ref<'text2image' | 'image2image'>('text2image')
|
||||||
|
const originalPrompt = ref('')
|
||||||
|
const optimizedPrompt = ref('')
|
||||||
|
const generatedImageUrl = ref('')
|
||||||
|
const inputImageB64 = ref('')
|
||||||
|
const optimizing = ref(false)
|
||||||
|
const generating = ref(false)
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
authStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOptimize = async () => {
|
||||||
|
if (!originalPrompt.value.trim()) {
|
||||||
|
message.warning('请输入提示词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
optimizing.value = true
|
||||||
|
try {
|
||||||
|
const response = await imagesApi.optimizePrompt({
|
||||||
|
prompt: originalPrompt.value,
|
||||||
|
mode: activeMode.value,
|
||||||
|
})
|
||||||
|
optimizedPrompt.value = response.data.optimized_prompt
|
||||||
|
} catch (error) {
|
||||||
|
message.error('优化失败')
|
||||||
|
} finally {
|
||||||
|
optimizing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImageUpload = (options: any) => {
|
||||||
|
const file = options.file.file
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const result = e.target?.result as string
|
||||||
|
inputImageB64.value = result.split(',')[1] // Remove data URL prefix
|
||||||
|
message.success('图片上传成功')
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadError = () => {
|
||||||
|
message.error('图片上传失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGenerate = async () => {
|
||||||
|
if (!optimizedPrompt.value) {
|
||||||
|
message.warning('请先优化提示词')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generating.value = true
|
||||||
|
generatedImageUrl.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await imagesApi.generateImage({
|
||||||
|
prompt: originalPrompt.value,
|
||||||
|
optimized_prompt: optimizedPrompt.value,
|
||||||
|
mode: activeMode.value,
|
||||||
|
model: 'gemini-2.5-flash-image',
|
||||||
|
input_image_b64: activeMode.value === 'image2image' ? inputImageB64.value : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Construct full URL for image
|
||||||
|
generatedImageUrl.value = `/uploads/${response.data.image_url.split('/uploads/')[1]}`
|
||||||
|
} catch (error) {
|
||||||
|
message.error('生成失败')
|
||||||
|
} finally {
|
||||||
|
generating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user