Explorar o código

新增我的收藏

liuziting hai 7 meses
pai
achega
d336c6736b

+ 39 - 0
src/components/ConfirmModal.vue

@@ -0,0 +1,39 @@
+<template>
+    <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" @click.self="$emit('cancel')">
+        <div class="bg-white rounded-lg border-2 border-black max-w-md w-full">
+            <div class="border-b-2 border-black p-4">
+                <h3 class="text-lg font-bold">{{ title }}</h3>
+            </div>
+            <div class="p-4">
+                <p class="text-gray-700 mb-6">{{ message }}</p>
+                <div class="flex gap-2">
+                    <button
+                        @click="$emit('confirm')"
+                        class="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium border-2 border-black transition-all duration-200"
+                    >
+                        确认
+                    </button>
+                    <button
+                        @click="$emit('cancel')"
+                        class="flex-1 px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg font-medium border-2 border-black transition-all duration-200"
+                    >
+                        取消
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+interface Props {
+    title: string
+    message: string
+}
+
+defineProps<Props>()
+defineEmits<{
+    confirm: []
+    cancel: []
+}>()
+</script>

+ 140 - 0
src/components/FavoriteButton.vue

@@ -0,0 +1,140 @@
+<template>
+    <button
+        @click="toggleFavorite"
+        :disabled="isLoading"
+        class="favorite-icon p-2 rounded-full transition-all duration-200 hover:scale-110 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
+        :title="isFavorited ? '取消收藏' : '收藏菜谱'"
+    >
+        <span class="text-xl transition-transform duration-200" :class="{ 'animate-pulse': isLoading }">
+            {{ isFavorited ? '❤️' : '🤍' }}
+        </span>
+    </button>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import type { Recipe } from '@/types'
+import { FavoriteService } from '@/services/favoriteService'
+
+interface Props {
+    recipe: Recipe
+}
+
+const props = defineProps<Props>()
+
+const emit = defineEmits<{
+    favoriteChanged: [isFavorited: boolean]
+}>()
+
+const isLoading = ref(false)
+const isFavorited = ref(false)
+
+// 检查收藏状态
+const checkFavoriteStatus = () => {
+    isFavorited.value = FavoriteService.isFavorite(props.recipe.id)
+}
+
+// 切换收藏状态
+const toggleFavorite = async () => {
+    if (isLoading.value) return
+
+    isLoading.value = true
+
+    try {
+        let success = false
+
+        if (isFavorited.value) {
+            // 取消收藏
+            success = FavoriteService.removeFavorite(props.recipe.id)
+            if (success) {
+                isFavorited.value = false
+                showToast('已取消收藏', 'info')
+            }
+        } else {
+            // 添加收藏
+            success = FavoriteService.addFavorite(props.recipe)
+            if (success) {
+                isFavorited.value = true
+                showToast('已添加到收藏', 'success')
+            } else {
+                showToast('该菜谱已在收藏中', 'warning')
+            }
+        }
+
+        if (success) {
+            emit('favoriteChanged', isFavorited.value)
+        }
+    } catch (error) {
+        console.error('收藏操作失败:', error)
+        showToast('操作失败,请重试', 'error')
+    } finally {
+        isLoading.value = false
+    }
+}
+
+// 简单的提示功能
+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(() => {
+    checkFavoriteStatus()
+})
+</script>
+
+<style scoped>
+.favorite-icon {
+    background: transparent;
+    border: none;
+}
+
+.favorite-icon:hover:not(:disabled) {
+    background: rgba(0, 0, 0, 0.05);
+}
+
+.favorite-icon:active:not(:disabled) {
+    transform: scale(0.95);
+}
+
+@keyframes pulse {
+    0%,
+    100% {
+        opacity: 1;
+    }
+    50% {
+        opacity: 0.5;
+    }
+}
+
+.animate-pulse {
+    animation: pulse 1s ease-in-out infinite;
+}
+</style>

+ 22 - 1
src/components/GlobalNavigation.vue

@@ -42,6 +42,14 @@
                         <span>🍽️</span>
                         <span>一桌菜设计</span>
                     </router-link>
+                    <router-link
+                        to="/favorites"
+                        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 === '/favorites' ? 'bg-red-400 text-white' : 'bg-red-100 text-red-700 hover:bg-red-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"
@@ -62,7 +70,7 @@
                             <span class="text-white text-lg font-bold">饭</span>
                         </div>
                         <div>
-                            <div class="text-lg font-black text-dark-800 font-['PingFangLiuAngLeTianTi'] tracking-wider">
+                            <div class="text-lg font-black text-dark-800 tracking-wider">
                                 {{ pageTitle }}
                             </div>
                             <div class="text-xs text-gray-600 font-medium">{{ pageSubtitle }}</div>
@@ -104,6 +112,15 @@
                         <span>🍽️</span>
                         <span>一桌菜设计</span>
                     </router-link>
+                    <router-link
+                        to="/favorites"
+                        @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 === '/favorites' ? 'bg-red-400 text-white' : 'bg-red-100 text-red-700 hover:bg-red-200'"
+                    >
+                        <span>❤️</span>
+                        <span>我的收藏</span>
+                    </router-link>
                     <router-link
                         to="/about"
                         @click="showMobileMenu = false"
@@ -135,6 +152,8 @@ const pageTitle = computed(() => {
             return '今天吃什么?'
         case '/table-design':
             return '一桌菜设计师'
+        case '/favorites':
+            return '我的收藏'
         case '/about':
             return '关于一饭封神'
         default:
@@ -150,6 +169,8 @@ const pageSubtitle = computed(() => {
             return "盲盒美食:'绝了!' or '寄了!'"
         case '/table-design':
             return '让每顿饭,都有剧本!'
+        case '/favorites':
+            return '珍藏美味,随时回味!'
         case '/about':
             return '算法烹万物,一键即封神!'
         default:

+ 62 - 0
src/components/NotesModal.vue

@@ -0,0 +1,62 @@
+<template>
+    <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" @click.self="$emit('close')">
+        <div class="bg-white rounded-lg border-2 border-black max-w-md w-full">
+            <div class="border-b-2 border-black p-4">
+                <h3 class="text-lg font-bold">编辑备注</h3>
+            </div>
+            <div class="p-4">
+                <div class="mb-4">
+                    <label class="block text-sm font-medium text-gray-700 mb-2">菜谱:{{ favorite.recipe.name }}</label>
+                    <textarea
+                        v-model="notes"
+                        placeholder="添加你的备注..."
+                        class="w-full p-3 border-2 border-black rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-red-400"
+                        rows="4"
+                        maxlength="200"
+                    ></textarea>
+                    <div class="text-xs text-gray-500 mt-1">{{ notes.length }}/200</div>
+                </div>
+                <div class="flex gap-2">
+                    <button
+                        @click="$emit('save', notes)"
+                        class="flex-1 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium border-2 border-black transition-all duration-200"
+                    >
+                        保存
+                    </button>
+                    <button
+                        @click="$emit('close')"
+                        class="flex-1 px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white rounded-lg font-medium border-2 border-black transition-all duration-200"
+                    >
+                        取消
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import type { FavoriteRecipe } from '@/types'
+
+interface Props {
+    favorite: FavoriteRecipe
+}
+
+const props = defineProps<Props>()
+const emit = defineEmits<{
+    close: []
+    save: [notes: string]
+}>()
+
+const notes = ref(props.favorite?.notes || '')
+
+// 监听favorite变化,更新notes
+watch(
+    () => props.favorite,
+    newFavorite => {
+        notes.value = newFavorite?.notes || ''
+    },
+    { immediate: true }
+)
+</script>

+ 19 - 2
src/components/RecipeCard.vue

@@ -11,7 +11,11 @@
                         <span>📊 {{ difficultyText }}</span>
                     </div>
                 </div>
-                <div class="text-2xl ml-2">🍽️</div>
+                <div class="flex items-center gap-2 ml-2">
+                    <!-- <div class="text-2xl">🍽️</div> -->
+                    <!-- 收藏按钮 -->
+                    <FavoriteButton v-if="showFavoriteButton" :recipe="recipe" @favorite-changed="onFavoriteChanged" />
+                </div>
             </div>
         </div>
 
@@ -203,14 +207,22 @@ import { computed, ref, onUnmounted } from 'vue'
 import type { Recipe } from '@/types'
 import { generateRecipeImage, type GeneratedImage } from '@/services/imageService'
 import { getNutritionAnalysis, getWinePairing } from '@/services/aiService'
+import FavoriteButton from './FavoriteButton.vue'
 import NutritionAnalysis from './NutritionAnalysis.vue'
 import WinePairing from './WinePairing.vue'
 
 interface Props {
     recipe: Recipe
+    showFavoriteButton?: boolean
 }
 
-const props = defineProps<Props>()
+const props = withDefaults(defineProps<Props>(), {
+    showFavoriteButton: true
+})
+
+const emit = defineEmits<{
+    favoriteChanged: [isFavorited: boolean]
+}>()
 const isExpanded = ref(false)
 const isGeneratingImage = ref(false)
 const generatedImage = ref<GeneratedImage | null>(null)
@@ -298,6 +310,11 @@ const toggleExpanded = () => {
     isExpanded.value = !isExpanded.value
 }
 
+// 处理收藏状态变化
+const onFavoriteChanged = (isFavorited: boolean) => {
+    emit('favoriteChanged', isFavorited)
+}
+
 const generateImage = async () => {
     if (isGeneratingImage.value) return
 

+ 27 - 0
src/components/RecipeModal.vue

@@ -0,0 +1,27 @@
+<template>
+    <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" @click.self="$emit('close')">
+        <div class="bg-white rounded-lg border-2 border-black max-w-4xl max-h-[90vh] overflow-y-auto">
+            <div class="sticky top-0 bg-white border-b-2 border-black p-4 flex items-center justify-between">
+                <h3 class="text-lg font-bold">菜谱详情</h3>
+                <button @click="$emit('close')" class="text-gray-500 hover:text-gray-700 text-xl">✕</button>
+            </div>
+            <div class="p-4">
+                <RecipeCard :recipe="recipe" :showFavoriteButton="false" />
+            </div>
+        </div>
+    </div>
+</template>
+
+<script setup lang="ts">
+import type { Recipe } from '@/types'
+import RecipeCard from './RecipeCard.vue'
+
+interface Props {
+    recipe: Recipe
+}
+
+defineProps<Props>()
+defineEmits<{
+    close: []
+}>()
+</script>

+ 3 - 1
src/main.ts

@@ -5,13 +5,15 @@ import Home from './views/Home.vue'
 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 './style.css'
 
 const routes = [
     { path: '/', component: Home },
     { path: '/about', component: About },
     { path: '/today-eat', component: TodayEat },
-    { path: '/table-design', component: TableDesign }
+    { path: '/table-design', component: TableDesign },
+    { path: '/favorites', component: Favorites }
 ]
 
 const router = createRouter({

+ 135 - 0
src/services/favoriteService.ts

@@ -0,0 +1,135 @@
+import type { Recipe, FavoriteRecipe } from '@/types'
+
+const FAVORITES_KEY = 'yifan-fengshen-favorites'
+
+/**
+ * 收藏服务 - 管理本地收藏的菜谱
+ */
+export class FavoriteService {
+    /**
+     * 获取所有收藏的菜谱
+     */
+    static getFavorites(): FavoriteRecipe[] {
+        try {
+            const stored = localStorage.getItem(FAVORITES_KEY)
+            return stored ? JSON.parse(stored) : []
+        } catch (error) {
+            console.error('获取收藏列表失败:', error)
+            return []
+        }
+    }
+
+    /**
+     * 添加菜谱到收藏
+     */
+    static addFavorite(recipe: Recipe, notes?: string): boolean {
+        try {
+            const favorites = this.getFavorites()
+
+            // 检查是否已收藏
+            if (favorites.some(fav => fav.recipe.id === recipe.id)) {
+                return false // 已收藏
+            }
+
+            const favoriteRecipe: FavoriteRecipe = {
+                id: `fav-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
+                recipe,
+                favoriteDate: new Date().toISOString(),
+                notes
+            }
+
+            favorites.unshift(favoriteRecipe) // 添加到开头
+            localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites))
+            return true
+        } catch (error) {
+            console.error('添加收藏失败:', error)
+            return false
+        }
+    }
+
+    /**
+     * 从收藏中移除菜谱
+     */
+    static removeFavorite(recipeId: string): boolean {
+        try {
+            const favorites = this.getFavorites()
+            const filteredFavorites = favorites.filter(fav => fav.recipe.id !== recipeId)
+
+            if (filteredFavorites.length === favorites.length) {
+                return false // 未找到要删除的项
+            }
+
+            localStorage.setItem(FAVORITES_KEY, JSON.stringify(filteredFavorites))
+            return true
+        } catch (error) {
+            console.error('移除收藏失败:', error)
+            return false
+        }
+    }
+
+    /**
+     * 检查菜谱是否已收藏
+     */
+    static isFavorite(recipeId: string): boolean {
+        try {
+            const favorites = this.getFavorites()
+            return favorites.some(fav => fav.recipe.id === recipeId)
+        } catch (error) {
+            console.error('检查收藏状态失败:', error)
+            return false
+        }
+    }
+
+    /**
+     * 更新收藏备注
+     */
+    static updateFavoriteNotes(recipeId: string, notes: string): boolean {
+        try {
+            const favorites = this.getFavorites()
+            const favoriteIndex = favorites.findIndex(fav => fav.recipe.id === recipeId)
+
+            if (favoriteIndex === -1) {
+                return false
+            }
+
+            favorites[favoriteIndex].notes = notes
+            localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites))
+            return true
+        } catch (error) {
+            console.error('更新收藏备注失败:', error)
+            return false
+        }
+    }
+
+    /**
+     * 清空所有收藏
+     */
+    static clearAllFavorites(): boolean {
+        try {
+            localStorage.removeItem(FAVORITES_KEY)
+            return true
+        } catch (error) {
+            console.error('清空收藏失败:', error)
+            return false
+        }
+    }
+
+    /**
+     * 获取收藏统计信息
+     */
+    static getFavoriteStats() {
+        const favorites = this.getFavorites()
+        const cuisineStats: Record<string, number> = {}
+
+        favorites.forEach(fav => {
+            const cuisine = fav.recipe.cuisine
+            cuisineStats[cuisine] = (cuisineStats[cuisine] || 0) + 1
+        })
+
+        return {
+            total: favorites.length,
+            cuisineStats,
+            latestFavorite: favorites[0]?.favoriteDate
+        }
+    }
+}

+ 8 - 0
src/types/index.ts

@@ -86,6 +86,14 @@ export interface WinePairing {
     origin?: string // 产地
 }
 
+// 收藏菜谱类型
+export interface FavoriteRecipe {
+    id: string
+    recipe: Recipe
+    favoriteDate: string // 收藏日期
+    notes?: string // 用户备注
+}
+
 // AI响应类型
 export interface AIResponse {
     success: boolean

+ 400 - 0
src/views/Favorites.vue

@@ -0,0 +1,400 @@
+<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-red-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 mb-4">
+                        <div class="flex items-center gap-3">
+                            <div class="w-12 h-12 bg-red-500 rounded-lg flex items-center justify-center">
+                                <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>
+                            </div>
+                        </div>
+
+                        <!-- 操作按钮 -->
+                        <div class="flex items-center gap-2">
+                            <!-- <button
+                                @click="refreshFavorites"
+                                class="px-3 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
+                                v-if="favorites.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 v-if="false" class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
+                        <div class="bg-gradient-to-r from-pink-100 to-red-100 p-4 rounded-lg border-2 border-black">
+                            <div class="flex items-center gap-2">
+                                <span class="text-2xl">📊</span>
+                                <div>
+                                    <div class="text-lg font-bold text-gray-800">{{ stats.total }}</div>
+                                    <div class="text-sm text-gray-600">总收藏数</div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="bg-gradient-to-r from-green-100 to-emerald-100 p-4 rounded-lg border-2 border-black">
+                            <div class="flex items-center gap-2">
+                                <span class="text-2xl">👨‍🍳</span>
+                                <div>
+                                    <div class="text-lg font-bold text-gray-800">{{ Object.keys(stats.cuisineStats).length }}</div>
+                                    <div class="text-sm text-gray-600">菜系种类</div>
+                                </div>
+                            </div>
+                        </div>
+
+                        <div class="bg-gradient-to-r from-blue-100 to-cyan-100 p-4 rounded-lg border-2 border-black">
+                            <div class="flex items-center gap-2">
+                                <span class="text-2xl">📅</span>
+                                <div>
+                                    <div class="text-lg font-bold text-gray-800">{{ formatDate(stats.latestFavorite) }}</div>
+                                    <div class="text-sm text-gray-600">最近收藏</div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <!-- 搜索和筛选 -->
+            <div v-if="favorites.length > 0" 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-red-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-red-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-red-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="filteredFavorites.length > 0" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
+                <div
+                    v-for="favorite in filteredFavorites"
+                    :key="favorite.id"
+                    class="bg-white border-2 border-black rounded-lg overflow-hidden hover:shadow-lg transition-all duration-200"
+                >
+                    <!-- 收藏信息头部 -->
+                    <div class="bg-red-100 border-b-2 border-black p-3">
+                        <div class="flex items-center justify-between">
+                            <div class="flex items-center gap-2">
+                                <span class="text-red-500">❤️</span>
+                                <span class="text-sm text-gray-600">收藏于 {{ formatDate(favorite.favoriteDate) }}</span>
+                            </div>
+                            <div class="flex items-center gap-2">
+                                <button @click="editNotes(favorite)" class="text-blue-500 hover:text-blue-600 text-sm" title="编辑备注">📝</button>
+                                <button @click="removeFavorite(favorite.recipe.id)" class="text-red-500 hover:text-red-600 text-sm" title="取消收藏">🗑️</button>
+                            </div>
+                        </div>
+
+                        <!-- 用户备注 -->
+                        <div v-if="favorite.notes" class="mt-2 p-2 bg-yellow-100 rounded border border-yellow-300">
+                            <div class="text-xs text-gray-600 mb-1">我的备注:</div>
+                            <div class="text-sm text-gray-800">{{ favorite.notes }}</div>
+                        </div>
+                    </div>
+
+                    <!-- 菜谱卡片 -->
+                    <div>
+                        <RecipeCard :recipe="favorite.recipe" :showFavoriteButton="false" />
+                    </div>
+                </div>
+            </div>
+
+            <!-- 空状态 -->
+            <div v-else-if="favorites.length === 0" class="text-center py-16">
+                <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-orange-500 to-red-500 hover:from-orange-600 hover:to-red-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>
+
+        <!-- 备注编辑弹窗 -->
+        <NotesModal v-if="editingFavorite" :favorite="editingFavorite" @close="editingFavorite = null" @save="saveNotes" />
+
+        <!-- 清空确认弹窗 -->
+        <ConfirmModal
+            v-if="showClearConfirm"
+            title="确认清空收藏"
+            message="确定要清空所有收藏的菜谱吗?此操作不可恢复。"
+            @confirm="clearAllFavorites"
+            @cancel="showClearConfirm = false"
+        />
+
+        <!-- 底部 -->
+        <GlobalFooter />
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import type { Recipe, FavoriteRecipe } from '@/types'
+import { FavoriteService } from '@/services/favoriteService'
+import RecipeCard from '@/components/RecipeCard.vue'
+import GlobalNavigation from '@/components/GlobalNavigation.vue'
+import GlobalFooter from '@/components/GlobalFooter.vue'
+
+import NotesModal from '@/components/NotesModal.vue'
+import ConfirmModal from '@/components/ConfirmModal.vue'
+
+// 响应式数据
+const favorites = ref<FavoriteRecipe[]>([])
+const searchQuery = ref('')
+const selectedCuisine = ref('')
+const sortBy = ref('date-desc')
+
+const editingFavorite = ref<FavoriteRecipe | null>(null)
+const showClearConfirm = ref(false)
+
+// 统计信息
+const stats = computed(() => FavoriteService.getFavoriteStats())
+
+// 可用菜系列表
+const availableCuisines = computed(() => {
+    const cuisines = new Set(favorites.value.map(fav => fav.recipe.cuisine))
+    return Array.from(cuisines).sort()
+})
+
+// 筛选后的收藏列表
+const filteredFavorites = computed(() => {
+    let filtered = [...favorites.value]
+
+    // 搜索筛选
+    if (searchQuery.value.trim()) {
+        const query = searchQuery.value.toLowerCase()
+        filtered = filtered.filter(
+            fav =>
+                fav.recipe.name.toLowerCase().includes(query) ||
+                fav.recipe.ingredients.some(ingredient => ingredient.toLowerCase().includes(query)) ||
+                (fav.notes && fav.notes.toLowerCase().includes(query))
+        )
+    }
+
+    // 菜系筛选
+    if (selectedCuisine.value) {
+        filtered = filtered.filter(fav => fav.recipe.cuisine === selectedCuisine.value)
+    }
+
+    // 排序
+    filtered.sort((a, b) => {
+        switch (sortBy.value) {
+            case 'date-desc':
+                return new Date(b.favoriteDate).getTime() - new Date(a.favoriteDate).getTime()
+            case 'date-asc':
+                return new Date(a.favoriteDate).getTime() - new Date(b.favoriteDate).getTime()
+            case 'name-asc':
+                return a.recipe.name.localeCompare(b.recipe.name)
+            case 'name-desc':
+                return b.recipe.name.localeCompare(a.recipe.name)
+            default:
+                return 0
+        }
+    })
+
+    return filtered
+})
+
+// 格式化日期
+const formatDate = (dateString?: string) => {
+    if (!dateString) return '未知'
+
+    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'
+        })
+    }
+}
+
+// 刷新收藏列表
+const refreshFavorites = () => {
+    favorites.value = FavoriteService.getFavorites()
+}
+
+// 编辑备注
+const editNotes = (favorite: FavoriteRecipe) => {
+    editingFavorite.value = favorite
+}
+
+// 保存备注
+const saveNotes = (notes: string) => {
+    if (editingFavorite.value) {
+        const success = FavoriteService.updateFavoriteNotes(editingFavorite.value.recipe.id, notes)
+        if (success) {
+            refreshFavorites()
+            showToast('备注已更新', 'success')
+        } else {
+            showToast('更新备注失败', 'error')
+        }
+    }
+    editingFavorite.value = null
+}
+
+// 移除收藏
+const removeFavorite = (recipeId: string) => {
+    const success = FavoriteService.removeFavorite(recipeId)
+    if (success) {
+        refreshFavorites()
+        showToast('已取消收藏', 'info')
+    } else {
+        showToast('取消收藏失败', 'error')
+    }
+}
+
+// 清空所有收藏
+const clearAllFavorites = () => {
+    const success = FavoriteService.clearAllFavorites()
+    if (success) {
+        refreshFavorites()
+        showToast('已清空所有收藏', 'info')
+    } else {
+        showToast('清空失败', 'error')
+    }
+    showClearConfirm.value = false
+}
+
+// 清除筛选条件
+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(() => {
+    refreshFavorites()
+})
+</script>
+
+<style scoped>
+/* 动画效果 */
+@keyframes fadeIn {
+    from {
+        opacity: 0;
+        transform: translateY(20px);
+    }
+    to {
+        opacity: 1;
+        transform: translateY(0);
+    }
+}
+
+.hover\:scale-\[1\.02\]:hover {
+    transform: scale(1.02);
+}
+
+/* 响应式调整 */
+@media (max-width: 640px) {
+    .grid-cols-1 {
+        gap: 1rem;
+    }
+}
+</style>