浏览代码

新增图鉴

liuziting 7 月之前
父节点
当前提交
72b3639a21

+ 22 - 1
src/components/GlobalNavigation.vue

@@ -4,7 +4,7 @@
             <!-- 桌面端导航 -->
             <div class="hidden md:flex items-center justify-between">
                 <!-- Logo区域 -->
-                <router-link to="/" class="flex items-center gap-3 hover:scale-105 transition-transform duration-200">
+                <router-link to="/" class="flex items-center gap-3 transition-transform duration-200">
                     <div class="w-12 h-12 bg-gradient-to-br from-yellow-400 to-orange-500 rounded-lg flex items-center justify-center border-2 border-black">
                         <span class="text-white text-xl font-bold">饭</span>
                     </div>
@@ -50,6 +50,14 @@
                         <span>❤️</span>
                         <span>我的收藏</span>
                     </router-link>
+                    <router-link
+                        to="/gallery"
+                        class="flex items-center gap-2 px-4 py-2 rounded-lg font-bold border-2 border-black transition-all duration-200 transform hover:scale-105 text-sm"
+                        :class="$route.path === '/gallery' ? 'bg-yellow-400 text-gray-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
+                    >
+                        <span>🖼️</span>
+                        <span>封神图鉴</span>
+                    </router-link>
                     <router-link
                         to="/about"
                         class="flex items-center gap-2 px-4 py-2 rounded-lg font-bold border-2 border-black transition-all duration-200 transform hover:scale-105 text-sm"
@@ -121,6 +129,15 @@
                         <span>❤️</span>
                         <span>我的收藏</span>
                     </router-link>
+                    <router-link
+                        to="/gallery"
+                        @click="showMobileMenu = false"
+                        class="flex items-center gap-2 w-full px-3 py-2 rounded-lg font-bold border-2 border-black transition-all duration-200 text-sm"
+                        :class="$route.path === '/gallery' ? 'bg-yellow-400 text-gray-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
+                    >
+                        <span>🖼️</span>
+                        <span>封神图鉴</span>
+                    </router-link>
                     <router-link
                         to="/about"
                         @click="showMobileMenu = false"
@@ -154,6 +171,8 @@ const pageTitle = computed(() => {
             return '一桌菜设计师'
         case '/favorites':
             return '我的收藏'
+        case '/gallery':
+            return '封神图鉴'
         case '/about':
             return '关于一饭封神'
         default:
@@ -171,6 +190,8 @@ const pageSubtitle = computed(() => {
             return '让每顿饭,都有剧本!'
         case '/favorites':
             return '珍藏美味,随时回味!'
+        case '/gallery':
+            return '每一帧都是厨艺的封神时刻!'
         case '/about':
             return '算法烹万物,一键即封神!'
         default:

+ 104 - 0
src/components/ImageModal.vue

@@ -0,0 +1,104 @@
+<template>
+    <div class="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4" @click="$emit('close')">
+        <div class="bg-white rounded-lg border-2 border-black max-w-4xl max-h-[90vh] overflow-hidden" @click.stop>
+            <!-- 头部 -->
+            <div class="bg-blue-500 text-white p-4 flex items-center justify-between">
+                <div>
+                    <h3 class="font-bold text-lg">{{ image.recipeName }}</h3>
+                    <p class="text-blue-100 text-sm">{{ image.cuisine }} • {{ formatDate(image.generatedAt) }}</p>
+                </div>
+                <button @click="$emit('close')" class="text-white hover:text-gray-200 text-2xl">×</button>
+            </div>
+
+            <!-- 图片 -->
+            <div class="relative">
+                <img :src="image.url" :alt="image.recipeName" class="w-full max-h-[60vh] object-contain" />
+            </div>
+
+            <!-- 详情信息 -->
+            <div class="p-4 max-h-48 overflow-y-auto">
+                <!-- 食材 -->
+                <div class="mb-4">
+                    <h4 class="font-bold text-gray-800 mb-2 flex items-center gap-1">🥬 食材</h4>
+                    <div class="flex flex-wrap gap-1">
+                        <span v-for="ingredient in image.ingredients" :key="ingredient" class="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm border">
+                            {{ ingredient }}
+                        </span>
+                    </div>
+                </div>
+
+                <!-- 生成提示词 -->
+                <div v-if="image.prompt" class="mb-4">
+                    <h4 class="font-bold text-gray-800 mb-2 flex items-center gap-1">🎨 生成提示</h4>
+                    <p class="text-gray-600 text-sm bg-gray-50 p-3 rounded border">
+                        {{ image.prompt }}
+                    </p>
+                </div>
+            </div>
+
+            <!-- 操作按钮 -->
+            <div class="bg-gray-50 border-t border-gray-200 p-4 flex items-center justify-between">
+                <div class="text-sm text-gray-500">生成于 {{ new Date(image.generatedAt).toLocaleString('zh-CN') }}</div>
+                <div class="flex items-center gap-2">
+                    <button
+                        @click="$emit('download', image)"
+                        class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg text-sm font-medium border-2 border-black transition-all duration-200 hover:scale-105"
+                    >
+                        📥 下载
+                    </button>
+                    <button
+                        @click="$emit('delete', image.id)"
+                        class="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-medium border-2 border-black transition-all duration-200 hover:scale-105"
+                    >
+                        🗑️ 删除
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import type { GalleryImage } from '@/services/galleryService'
+
+interface Props {
+    image: GalleryImage
+}
+
+defineProps<Props>()
+
+defineEmits<{
+    close: []
+    delete: [imageId: string]
+    download: [image: GalleryImage]
+}>()
+
+// 格式化日期
+const formatDate = (dateString: string) => {
+    const date = new Date(dateString)
+    const now = new Date()
+    const diffTime = now.getTime() - date.getTime()
+    const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
+
+    if (diffDays === 0) {
+        return '今天'
+    } else if (diffDays === 1) {
+        return '昨天'
+    } else if (diffDays < 7) {
+        return `${diffDays}天前`
+    } else {
+        return date.toLocaleDateString('zh-CN', {
+            year: 'numeric',
+            month: 'short',
+            day: 'numeric'
+        })
+    }
+}
+</script>
+
+<style scoped>
+/* 确保弹窗在最顶层 */
+.z-50 {
+    z-index: 50;
+}
+</style>

+ 5 - 0
src/components/RecipeCard.vue

@@ -331,6 +331,11 @@ const generateImage = async () => {
     try {
         const image = await generateRecipeImage(props.recipe)
         generatedImage.value = image
+
+        // 将生成的图片添加到图库
+        const { GalleryService } = await import('@/services/galleryService')
+        const prompt = `一道精美的${props.recipe.cuisine.replace('大师', '').replace('菜', '')}菜肴:${props.recipe.name}`
+        GalleryService.addToGallery(props.recipe, image.url, image.id, prompt)
     } catch (error) {
         console.error('生成图片失败:', error)
         imageError.value = '生成图片失败,请稍后重试'

+ 3 - 1
src/main.ts

@@ -6,6 +6,7 @@ import About from './views/About.vue'
 import TodayEat from './views/TodayEat.vue'
 import TableDesign from './views/TableDesign.vue'
 import Favorites from './views/Favorites.vue'
+import Gallery from './views/Gallery.vue'
 import './style.css'
 
 const routes = [
@@ -13,7 +14,8 @@ const routes = [
     { path: '/about', component: About },
     { path: '/today-eat', component: TodayEat },
     { path: '/table-design', component: TableDesign },
-    { path: '/favorites', component: Favorites }
+    { path: '/favorites', component: Favorites },
+    { path: '/gallery', component: Gallery }
 ]
 
 const router = createRouter({

+ 9 - 9
src/services/aiService.ts

@@ -3,17 +3,17 @@ import type { Recipe, CuisineType, NutritionAnalysis, WinePairing } from '@/type
 
 // AI服务配置 - 从环境变量读取
 const AI_CONFIG = {
-    baseURL: 'https://api.deepseek.com/v1/',
-    apiKey: import.meta.env.VITE_TEXT_GENERATION_API_KEY,
-    model: 'deepseek-chat',
-    temperature: 0.7,
-    timeout: 300000
-
-    // baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
-    // apiKey: import.meta.env.VITE_IMAGE_GENERATION_API_KEY,
-    // model: 'glm-4-flash-250414',
+    // baseURL: 'https://api.deepseek.com/v1/',
+    // apiKey: import.meta.env.VITE_TEXT_GENERATION_API_KEY,
+    // model: 'deepseek-chat',
     // temperature: 0.7,
     // timeout: 300000
+
+    baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
+    apiKey: import.meta.env.VITE_IMAGE_GENERATION_API_KEY,
+    model: 'glm-4-flash-250414',
+    temperature: 0.7,
+    timeout: 300000
 }
 
 // 创建axios实例

+ 133 - 0
src/services/galleryService.ts

@@ -0,0 +1,133 @@
+import type { Recipe } from '@/types'
+
+export interface GalleryImage {
+    id: string
+    url: string
+    recipeName: string
+    recipeId: string
+    cuisine: string
+    ingredients: string[]
+    generatedAt: string
+    prompt?: string
+}
+
+export interface GalleryStats {
+    total: number
+    cuisineStats: Record<string, number>
+    latestGenerated?: string
+}
+
+class GalleryServiceClass {
+    private readonly STORAGE_KEY = 'recipe-gallery-images'
+
+    // 获取所有图库图片
+    getGalleryImages(): GalleryImage[] {
+        try {
+            const stored = localStorage.getItem(this.STORAGE_KEY)
+            return stored ? JSON.parse(stored) : []
+        } catch (error) {
+            console.error('获取图库数据失败:', error)
+            return []
+        }
+    }
+
+    // 添加图片到图库
+    addToGallery(recipe: Recipe, imageUrl: string, imageId: string, prompt?: string): boolean {
+        try {
+            const images = this.getGalleryImages()
+
+            // 检查是否已存在相同的图片
+            const existingIndex = images.findIndex(img => img.id === imageId)
+
+            const galleryImage: GalleryImage = {
+                id: imageId,
+                url: imageUrl,
+                recipeName: recipe.name,
+                recipeId: recipe.id,
+                cuisine: recipe.cuisine,
+                ingredients: recipe.ingredients,
+                generatedAt: new Date().toISOString(),
+                prompt
+            }
+
+            if (existingIndex >= 0) {
+                // 更新现有图片
+                images[existingIndex] = galleryImage
+            } else {
+                // 添加新图片到开头
+                images.unshift(galleryImage)
+            }
+
+            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(images))
+            return true
+        } catch (error) {
+            console.error('添加图片到图库失败:', error)
+            return false
+        }
+    }
+
+    // 从图库删除图片
+    removeFromGallery(imageId: string): boolean {
+        try {
+            const images = this.getGalleryImages()
+            const filteredImages = images.filter(img => img.id !== imageId)
+            localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredImages))
+            return true
+        } catch (error) {
+            console.error('从图库删除图片失败:', error)
+            return false
+        }
+    }
+
+    // 清空图库
+    clearGallery(): boolean {
+        try {
+            localStorage.removeItem(this.STORAGE_KEY)
+            return true
+        } catch (error) {
+            console.error('清空图库失败:', error)
+            return false
+        }
+    }
+
+    // 获取图库统计信息
+    getGalleryStats(): GalleryStats {
+        const images = this.getGalleryImages()
+
+        const cuisineStats: Record<string, number> = {}
+        images.forEach(img => {
+            cuisineStats[img.cuisine] = (cuisineStats[img.cuisine] || 0) + 1
+        })
+
+        return {
+            total: images.length,
+            cuisineStats,
+            latestGenerated: images.length > 0 ? images[0].generatedAt : undefined
+        }
+    }
+
+    // 根据菜系筛选图片
+    getImagesByCuisine(cuisine: string): GalleryImage[] {
+        return this.getGalleryImages().filter(img => img.cuisine === cuisine)
+    }
+
+    // 搜索图片
+    searchImages(query: string): GalleryImage[] {
+        const images = this.getGalleryImages()
+        const lowerQuery = query.toLowerCase()
+
+        return images.filter(
+            img =>
+                img.recipeName.toLowerCase().includes(lowerQuery) ||
+                img.cuisine.toLowerCase().includes(lowerQuery) ||
+                img.ingredients.some(ingredient => ingredient.toLowerCase().includes(lowerQuery))
+        )
+    }
+
+    // 获取最近生成的图片
+    getRecentImages(limit: number = 10): GalleryImage[] {
+        return this.getGalleryImages().slice(0, limit)
+    }
+}
+
+export const GalleryService = new GalleryServiceClass()

+ 2 - 2
src/views/Favorites.vue

@@ -16,8 +16,8 @@
                                 <span class="text-white text-2xl">❤️</span>
                             </div>
                             <div>
-                                <h1 class="text-xl font-bold text-gray-800">收藏菜谱</h1>
-                                <p class="text-gray-600 text-sm">共收藏了 {{ favorites.length }} 道菜谱</p>
+                                <h1 class="text-md font-bold text-gray-800">收藏菜谱</h1>
+                                <p class="text-gray-600 text-xs">共收藏了 {{ favorites.length }} 道菜谱</p>
                             </div>
                         </div>
 

+ 386 - 0
src/views/Gallery.vue

@@ -0,0 +1,386 @@
+<template>
+    <div class="min-h-screen bg-yellow-400 px-2 md:px-4 py-6">
+        <!-- 全局导航 -->
+        <GlobalNavigation />
+
+        <div class="max-w-7xl mx-auto">
+            <!-- 页面标题 -->
+            <div class="mb-6">
+                <div class="bg-blue-500 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
+                    <span class="font-bold">封神图鉴</span>
+                </div>
+                <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-4 md:p-6">
+                    <div class="flex items-center justify-between">
+                        <div class="flex items-center gap-3">
+                            <div class="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center">
+                                <span class="text-white text-2xl">🖼️</span>
+                            </div>
+                            <div>
+                                <h1 class="text-md font-bold text-gray-800">AI厨艺的视觉宝典</h1>
+                                <p class="text-gray-600 text-xs">共生成了 {{ images.length }} 张菜品图片</p>
+                            </div>
+                        </div>
+
+                        <!-- 操作按钮 -->
+                        <div class="flex items-center gap-2">
+                            <button
+                                v-if="images.length > 0"
+                                @click="showClearConfirm = true"
+                                class="px-3 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg text-sm font-medium border-2 border-black transition-all duration-200 hover:scale-105"
+                            >
+                                🗑️ 清空
+                            </button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 搜索和筛选 -->
+            <div v-if="false" class="mb-6">
+                <div class="bg-white border-2 border-black rounded-lg p-4">
+                    <div class="flex flex-col md:flex-row gap-4">
+                        <!-- 搜索框 -->
+                        <div class="flex-1">
+                            <input
+                                v-model="searchQuery"
+                                placeholder="搜索菜谱名称、菜系或食材..."
+                                class="w-full p-3 border-2 border-black rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
+                            />
+                        </div>
+
+                        <!-- 菜系筛选 -->
+                        <div class="md:w-48">
+                            <select v-model="selectedCuisine" class="w-full p-3 border-2 border-black rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
+                                <option value="">全部菜系</option>
+                                <option v-for="cuisine in availableCuisines" :key="cuisine" :value="cuisine">
+                                    {{ cuisine }}
+                                </option>
+                            </select>
+                        </div>
+
+                        <!-- 排序 -->
+                        <div class="md:w-48">
+                            <select v-model="sortBy" class="w-full p-3 border-2 border-black rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-400">
+                                <option value="date-desc">最新生成</option>
+                                <option value="date-asc">最早生成</option>
+                                <option value="name-asc">菜名 A-Z</option>
+                                <option value="name-desc">菜名 Z-A</option>
+                            </select>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 图片网格 -->
+            <div v-if="filteredImages.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+                <div
+                    v-for="image in filteredImages"
+                    :key="image.id"
+                    class="bg-white border-2 border-black rounded-lg overflow-hidden hover:shadow-lg transition-all duration-200 group"
+                >
+                    <!-- 图片 -->
+                    <div class="relative aspect-[4/3] overflow-hidden">
+                        <img
+                            :src="image.url"
+                            :alt="image.recipeName"
+                            class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
+                            @error="handleImageError(image.id)"
+                        />
+
+                        <!-- 悬浮信息层 -->
+                        <div
+                            class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
+                        >
+                            <!-- 顶部操作按钮 -->
+                            <div class="absolute top-3 right-3">
+                                <button
+                                    @click.stop="confirmDeleteImage(image.id)"
+                                    class="p-2 bg-red-500/80 hover:bg-red-500 text-white rounded-full text-sm transition-colors backdrop-blur-sm"
+                                    title="删除图片"
+                                >
+                                    🗑️
+                                </button>
+                            </div>
+
+                            <!-- 底部信息 -->
+                            <div class="absolute bottom-0 left-0 right-0 p-4">
+                                <h3 class="font-bold text-white text-lg mb-2 line-clamp-1">{{ image.recipeName }}</h3>
+                                <div class="flex items-center justify-between mb-3">
+                                    <span class="text-white/90 text-sm">{{ image.cuisine }}</span>
+                                    <span class="text-white/80 text-xs">{{ formatDate(image.generatedAt) }}</span>
+                                </div>
+
+                                <!-- 食材标签 -->
+                                <div class="flex flex-wrap gap-1">
+                                    <span
+                                        v-for="ingredient in image.ingredients.slice(0, 4)"
+                                        :key="ingredient"
+                                        class="bg-white/20 backdrop-blur-sm text-white px-2 py-1 rounded text-xs border border-white/30"
+                                    >
+                                        {{ ingredient }}
+                                    </span>
+                                    <span v-if="image.ingredients.length > 4" class="bg-white/10 backdrop-blur-sm text-white/80 px-2 py-1 rounded text-xs border border-white/20">
+                                        +{{ image.ingredients.length - 4 }}
+                                    </span>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 空状态 -->
+            <div v-else-if="images.length === 0" class="text-center py-6">
+                <div class="bg-white border-2 border-black rounded-lg p-8">
+                    <div class="text-6xl mb-4">🖼️</div>
+                    <h3 class="text-xl font-bold text-gray-800 mb-2">图库还是空的</h3>
+                    <p class="text-gray-600 mb-6">去生成一些菜品效果图,让图库丰富起来吧!</p>
+                    <router-link
+                        to="/"
+                        class="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white rounded-lg font-bold border-2 border-black transition-all duration-200 hover:scale-105"
+                    >
+                        <span>✨</span>
+                        <span>开始生成菜谱</span>
+                    </router-link>
+                </div>
+            </div>
+
+            <!-- 搜索无结果 -->
+            <div v-else class="text-center py-16">
+                <div class="bg-white border-2 border-black rounded-lg p-8">
+                    <div class="text-4xl mb-4">🔍</div>
+                    <h3 class="text-xl font-bold text-gray-800 mb-2">没有找到匹配的图片</h3>
+                    <p class="text-gray-600 mb-4">试试调整搜索条件或筛选选项</p>
+                    <button
+                        @click="clearFilters"
+                        class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg font-medium border-2 border-black transition-all duration-200"
+                    >
+                        清除筛选条件
+                    </button>
+                </div>
+            </div>
+        </div>
+
+        <!-- 图片详情弹窗 -->
+        <ImageModal v-if="selectedImage" :image="selectedImage" @close="selectedImage = null" @delete="confirmDeleteImage" @download="downloadImage" />
+
+        <!-- 删除确认弹窗 -->
+        <ConfirmModal v-if="deletingImageId" title="确认删除图片" message="确定要删除这张图片吗?此操作不可恢复。" @confirm="deleteImage" @cancel="deletingImageId = null" />
+
+        <!-- 清空确认弹窗 -->
+        <ConfirmModal v-if="showClearConfirm" title="确认清空图库" message="确定要清空所有图片吗?此操作不可恢复。" @confirm="clearAllImages" @cancel="showClearConfirm = false" />
+
+        <!-- 底部 -->
+        <GlobalFooter />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { GalleryService, type GalleryImage } from '@/services/galleryService'
+import GlobalNavigation from '@/components/GlobalNavigation.vue'
+import GlobalFooter from '@/components/GlobalFooter.vue'
+import ImageModal from '@/components/ImageModal.vue'
+import ConfirmModal from '@/components/ConfirmModal.vue'
+
+// 响应式数据
+const images = ref<GalleryImage[]>([])
+const searchQuery = ref('')
+const selectedCuisine = ref('')
+const sortBy = ref('date-desc')
+const selectedImage = ref<GalleryImage | null>(null)
+const deletingImageId = ref<string | null>(null)
+const showClearConfirm = ref(false)
+
+// 可用菜系列表
+const availableCuisines = computed(() => {
+    const cuisines = new Set(images.value.map(img => img.cuisine))
+    return Array.from(cuisines).sort()
+})
+
+// 筛选后的图片列表
+const filteredImages = computed(() => {
+    let filtered = [...images.value]
+
+    // 搜索筛选
+    if (searchQuery.value.trim()) {
+        const query = searchQuery.value.toLowerCase()
+        filtered = filtered.filter(
+            img =>
+                img.recipeName.toLowerCase().includes(query) ||
+                img.cuisine.toLowerCase().includes(query) ||
+                img.ingredients.some(ingredient => ingredient.toLowerCase().includes(query))
+        )
+    }
+
+    // 菜系筛选
+    if (selectedCuisine.value) {
+        filtered = filtered.filter(img => img.cuisine === selectedCuisine.value)
+    }
+
+    // 排序
+    filtered.sort((a, b) => {
+        switch (sortBy.value) {
+            case 'date-desc':
+                return new Date(b.generatedAt).getTime() - new Date(a.generatedAt).getTime()
+            case 'date-asc':
+                return new Date(a.generatedAt).getTime() - new Date(b.generatedAt).getTime()
+            case 'name-asc':
+                return a.recipeName.localeCompare(b.recipeName)
+            case 'name-desc':
+                return b.recipeName.localeCompare(a.recipeName)
+            default:
+                return 0
+        }
+    })
+
+    return filtered
+})
+
+// 格式化日期
+const formatDate = (dateString: string) => {
+    const date = new Date(dateString)
+    const now = new Date()
+    const diffTime = now.getTime() - date.getTime()
+    const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
+
+    if (diffDays === 0) {
+        return '今天'
+    } else if (diffDays === 1) {
+        return '昨天'
+    } else if (diffDays < 7) {
+        return `${diffDays}天前`
+    } else {
+        return date.toLocaleDateString('zh-CN', {
+            month: 'short',
+            day: 'numeric'
+        })
+    }
+}
+
+// 刷新图库
+const refreshGallery = () => {
+    images.value = GalleryService.getGalleryImages()
+}
+
+// 打开图片详情弹窗
+const openImageModal = (image: GalleryImage) => {
+    selectedImage.value = image
+}
+
+// 确认删除图片
+const confirmDeleteImage = (imageId: string) => {
+    deletingImageId.value = imageId
+    selectedImage.value = null
+}
+
+// 删除图片
+const deleteImage = () => {
+    if (!deletingImageId.value) return
+
+    const success = GalleryService.removeFromGallery(deletingImageId.value)
+    if (success) {
+        refreshGallery()
+        showToast('图片已删除', 'info')
+    } else {
+        showToast('删除失败', 'error')
+    }
+    deletingImageId.value = null
+}
+
+// 清空所有图片
+const clearAllImages = () => {
+    const success = GalleryService.clearGallery()
+    if (success) {
+        refreshGallery()
+        showToast('图库已清空', 'info')
+    } else {
+        showToast('清空失败', 'error')
+    }
+    showClearConfirm.value = false
+}
+
+// 下载图片
+const downloadImage = async (image: GalleryImage) => {
+    try {
+        const response = await fetch(image.url)
+        const blob = await response.blob()
+        const url = window.URL.createObjectURL(blob)
+
+        const link = document.createElement('a')
+        link.href = url
+        link.download = `${image.recipeName}-${formatDate(image.generatedAt)}.jpg`
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+
+        window.URL.revokeObjectURL(url)
+        showToast('图片下载成功', 'success')
+    } catch (error) {
+        console.error('下载图片失败:', error)
+        showToast('下载失败', 'error')
+    }
+}
+
+// 处理图片加载错误
+const handleImageError = (imageId: string) => {
+    console.warn(`图片加载失败: ${imageId}`)
+}
+
+// 清除筛选条件
+const clearFilters = () => {
+    searchQuery.value = ''
+    selectedCuisine.value = ''
+    sortBy.value = 'date-desc'
+}
+
+// 简单的提示功能
+const showToast = (message: string, type: 'success' | 'error' | 'warning' | 'info') => {
+    const toast = document.createElement('div')
+    toast.className = `fixed top-4 right-4 px-4 py-2 rounded-lg text-white text-sm font-medium z-50 transition-all duration-300 transform translate-x-full`
+
+    const styles = {
+        success: 'bg-green-500',
+        error: 'bg-red-500',
+        warning: 'bg-yellow-500',
+        info: 'bg-blue-500'
+    }
+
+    toast.className += ` ${styles[type]}`
+    toast.textContent = message
+
+    document.body.appendChild(toast)
+
+    setTimeout(() => {
+        toast.style.transform = 'translateX(0)'
+    }, 10)
+
+    setTimeout(() => {
+        toast.style.transform = 'translateX(full)'
+        setTimeout(() => {
+            document.body.removeChild(toast)
+        }, 300)
+    }, 2000)
+}
+
+// 初始化
+onMounted(() => {
+    refreshGallery()
+})
+</script>
+
+<style scoped>
+.line-clamp-1 {
+    display: -webkit-box;
+    -webkit-line-clamp: 1;
+    -webkit-box-orient: vertical;
+    overflow: hidden;
+}
+
+/* 响应式调整 */
+@media (max-width: 640px) {
+    .grid-cols-1 {
+        gap: 1rem;
+    }
+}
+</style>

+ 2 - 2
src/views/Home.vue

@@ -129,7 +129,7 @@
                                         :key="cuisine.id"
                                         @click="selectCuisine(cuisine)"
                                         :class="[
-                                            'p-2 rounded-lg border-2 border-black font-medium text-xs transition-all duration-200 relative flex items-center justify-center gap-1',
+                                            'p-2 rounded-lg border-2 border-black font-medium text-sm transition-all duration-200 relative flex items-center justify-center gap-1',
                                             selectedCuisines.includes(cuisine.id) ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
                                         ]"
                                     >
@@ -148,7 +148,7 @@
                                         :key="cuisine.id"
                                         @click="selectCuisine(cuisine)"
                                         :class="[
-                                            'p-2 rounded-lg border-2 border-black font-medium text-xs transition-all duration-200 relative flex items-center justify-center gap-1',
+                                            'p-2 rounded-lg border-2 border-black font-medium text-sm transition-all duration-200 relative flex items-center justify-center gap-1',
                                             selectedCuisines.includes(cuisine.id) ? 'bg-yellow-400 text-dark-800' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
                                         ]"
                                     >

+ 1 - 1
src/views/TodayEat.vue

@@ -158,7 +158,7 @@
                     <span>📖</span>
                     <span>专属菜谱</span>
                 </h3>
-                <div class="border-2 border-[#333333] rounded-lg overflow-hidden">
+                <div class="max-w-2xl mx-auto border-2 border-[#333333] rounded-lg overflow-hidden">
                     <RecipeCard :recipe="recipe" />
                 </div>
             </div>