Sfoglia il codice sorgente

新增根据菜谱生成图片

liuziting 7 mesi fa
parent
commit
f7fb0d37dd

+ 101 - 1
src/components/RecipeCard.vue

@@ -77,13 +77,57 @@
                     </ul>
                 </div>
             </div>
+
+            <!-- 效果图区域 -->
+            <div class="mt-4 pt-4 border-t border-gray-200">
+                <div class="flex items-center justify-between mb-3">
+                    <h4 class="text-sm font-bold text-dark-800 flex items-center gap-1">🖼️ 菜品效果图</h4>
+                    <button
+                        @click="generateImage"
+                        :disabled="isGeneratingImage"
+                        class="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-3 py-1 rounded text-xs font-medium border border-black transition-all duration-200 disabled:cursor-not-allowed"
+                    >
+                        <span class="flex items-center gap-1">
+                            <template v-if="isGeneratingImage">
+                                <div class="animate-spin w-3 h-3 border border-white border-t-transparent rounded-full"></div>
+                                生成中...
+                            </template>
+                            <template v-else> ✨ 生成效果图 </template>
+                        </span>
+                    </button>
+                </div>
+
+                <!-- 加载状态 -->
+                <div v-if="isGeneratingImage" class="bg-gray-50 border-2 border-gray-300 rounded-lg p-6 text-center">
+                    <div class="w-12 h-12 border-4 border-gray-300 border-t-blue-500 rounded-full animate-spin mx-auto mb-3"></div>
+                    <h5 class="text-sm font-bold text-dark-800 mb-1">AI画师正在创作中...</h5>
+                    <p class="text-gray-600 text-xs">{{ imageLoadingText }}</p>
+                </div>
+
+                <!-- 生成的图片 -->
+                <div v-else-if="generatedImage" class="mb-3">
+                    <img :src="generatedImage.url" :alt="`${recipe.name}效果图`" class="w-full h-48 object-cover rounded-lg border-2 border-black" @error="handleImageError" />
+                </div>
+
+                <!-- 错误提示 -->
+                <div v-else-if="imageError" class="bg-red-100 border border-red-400 text-red-700 px-3 py-2 rounded text-xs">
+                    {{ imageError }}
+                </div>
+
+                <!-- 空状态 -->
+                <div v-else class="bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
+                    <div class="text-gray-400 text-2xl mb-2">📷</div>
+                    <p class="text-gray-500 text-xs">点击上方按钮生成菜品效果图</p>
+                </div>
+            </div>
         </div>
     </div>
 </template>
 
 <script setup lang="ts">
-import { computed, ref } from 'vue'
+import { computed, ref, onUnmounted } from 'vue'
 import type { Recipe } from '@/types'
+import { generateRecipeImage, type GeneratedImage } from '@/services/imageService'
 
 interface Props {
     recipe: Recipe
@@ -91,6 +135,23 @@ interface Props {
 
 const props = defineProps<Props>()
 const isExpanded = ref(false)
+const isGeneratingImage = ref(false)
+const generatedImage = ref<GeneratedImage | null>(null)
+const imageError = ref<string>('')
+const imageLoadingText = ref('正在构思画面布局...')
+
+// 图片生成加载文字轮播
+const imageLoadingTexts = [
+    '正在构思画面布局...',
+    '正在调配色彩搭配...',
+    '正在绘制食材细节...',
+    '正在优化光影效果...',
+    '正在精修画面质感...',
+    '正在添加最后润色...',
+    '精美效果图即将完成...'
+]
+
+let imageLoadingInterval: NodeJS.Timeout | null = null
 
 const difficultyText = computed(() => {
     const difficultyMap = {
@@ -104,6 +165,45 @@ const difficultyText = computed(() => {
 const toggleExpanded = () => {
     isExpanded.value = !isExpanded.value
 }
+
+const generateImage = async () => {
+    if (isGeneratingImage.value) return
+
+    isGeneratingImage.value = true
+    imageError.value = ''
+
+    // 开始图片生成加载文字轮播
+    let textIndex = 0
+    imageLoadingInterval = setInterval(() => {
+        imageLoadingText.value = imageLoadingTexts[textIndex]
+        textIndex = (textIndex + 1) % imageLoadingTexts.length
+    }, 2000)
+
+    try {
+        const image = await generateRecipeImage(props.recipe)
+        generatedImage.value = image
+    } catch (error) {
+        console.error('生成图片失败:', error)
+        imageError.value = '生成图片失败,请稍后重试'
+    } finally {
+        isGeneratingImage.value = false
+        if (imageLoadingInterval) {
+            clearInterval(imageLoadingInterval)
+            imageLoadingInterval = null
+        }
+    }
+}
+
+const handleImageError = () => {
+    imageError.value = '图片加载失败'
+    generatedImage.value = null
+}
+
+onUnmounted(() => {
+    if (imageLoadingInterval) {
+        clearInterval(imageLoadingInterval)
+    }
+})
 </script>
 
 <style scoped>

+ 0 - 39
src/config/ai.ts

@@ -8,45 +8,6 @@ export interface AIConfig {
     temperature: number
 }
 
-// 默认配置 - 智谱AI
-export const defaultAIConfig: AIConfig = {
-    baseURL: 'https://open.bigmodel.cn/api/paas/v4/', // 智谱AI接口地址
-    apiKey: 'a835b9f6866d48ec956d341418df8a50.NuhlKYn58EkCb5iP', // API密钥
-    model: 'glm-4-flash-250414', // 智谱AI模型
-    timeout: 30000, // 30秒超时
-    maxTokens: 2000, // 最大token数
-    temperature: 0.7 // 创造性参数
-}
-
-// 支持的AI服务提供商
-export const AI_PROVIDERS = {
-    ZHIPU: {
-        name: '智谱AI',
-        baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
-        models: ['glm-4-flash-250414', 'glm-4', 'glm-3-turbo']
-    },
-    OPENAI: {
-        name: 'OpenAI',
-        baseURL: 'https://api.openai.com/v1',
-        models: ['gpt-3.5-turbo', 'gpt-4', 'gpt-4-turbo']
-    },
-    AZURE: {
-        name: 'Azure OpenAI',
-        baseURL: '', // 需要填入Azure端点
-        models: ['gpt-35-turbo', 'gpt-4']
-    },
-    ANTHROPIC: {
-        name: 'Anthropic Claude',
-        baseURL: 'https://api.anthropic.com/v1',
-        models: ['claude-3-sonnet', 'claude-3-opus']
-    },
-    CUSTOM: {
-        name: '自定义接口',
-        baseURL: '', // 自定义端点
-        models: ['custom-model']
-    }
-}
-
 // 菜系提示词模板
 export const CUISINE_PROMPTS = {
     system: '你是一位专业的厨师,请根据用户提供的食材和菜系要求,生成详细的菜谱。',

+ 8 - 8
src/config/cuisines.ts

@@ -7,7 +7,7 @@ export const cuisines: CuisineType[] = [
         description: '江南水乡的精致美味',
         avatar: '👨‍🍳',
         specialty: '清淡鲜美,刀工精细',
-        prompt: `你是一位苏菜大师,擅长江苏菜系。苏菜以清淡鲜美、刀工精细、造型美观著称。请根据用户提供的食材,设计一道精致的苏菜,注重食材的原味和营养搭配。回答要包含菜名、详细制作步骤、烹饪技巧和营养价值。`
+        prompt: `作为苏菜传承人,你精通淮扬菜系精髓。苏菜以清鲜雅致、刀工精湛、造型玲珑著称。请基于用户食材设计一道正统苏菜,突出食材本味与养生搭配。回答需包含:创意菜名、分步烹饪流程、刀工技法解析、营养平衡说明。`
     },
     {
         id: 'lu',
@@ -15,7 +15,7 @@ export const cuisines: CuisineType[] = [
         description: '齐鲁大地的豪放风味',
         avatar: '👨‍🍳',
         specialty: '咸鲜为主,火候精准',
-        prompt: `你是一位鲁菜大师,精通山东菜系。鲁菜以咸鲜为主、火候精准、营养丰富著称,是宫廷菜的代表。请根据用户提供的食材,设计一道正宗的鲁菜,注重火候控制和调味平衡。回答要包含菜名、详细制作步骤、火候要点和传统做法。`
+        prompt: `身为鲁菜宗师,你深谙孔府宫廷菜真谛。鲁菜讲究咸鲜纯正、火功严谨、礼仪考究。请依据用户食材创作经典鲁菜,强调火候层次与五味调和。回答需包含:传统菜名、分步烹饪图解、关键火候节点、宫廷技法溯源。`
     },
     {
         id: 'chuan',
@@ -23,7 +23,7 @@ export const cuisines: CuisineType[] = [
         description: '巴蜀之地的麻辣传奇',
         avatar: '👨‍🍳',
         specialty: '麻辣鲜香,变化多端',
-        prompt: `你是一位川菜大师,精通四川菜系。川菜以麻辣鲜香、口味多变著称,有"一菜一格,百菜百味"的美誉。请根据用户提供的食材,设计一道地道的川菜,突出麻辣特色和层次丰富的口感。回答要包含菜名、详细制作步骤、调料配比和川菜技法。`
+        prompt: `作为川味掌门,你掌握二十三味型精髓。川菜擅长麻辣平衡、复合调味、一菜一格。请针对用户食材设计地道川味,突出口感层次与红油运用。回答需包含:特色菜名、七步烹饪法、秘制调料配方、味型创新解析。`
     },
     {
         id: 'yue',
@@ -31,7 +31,7 @@ export const cuisines: CuisineType[] = [
         description: '岭南文化的鲜美诠释',
         avatar: '👨‍🍳',
         specialty: '清淡鲜美,原汁原味',
-        prompt: `你是一位粤菜大师,精通广东菜系。粤菜以清淡鲜美、原汁原味、食材新鲜著称,注重营养和健康。请根据用户提供的食材,设计一道精致的粤菜,保持食材的天然鲜味。回答要包含菜名、详细制作步骤、火候掌握和营养搭配。`
+        prompt: `身为粤菜泰斗,你崇尚清中求鲜理念。粤菜注重时令本味、镬气逼人、养生之道。请根据用户食材构思广府佳肴,凸显生猛鲜香与少油烹饪。回答需包含:意境菜名、精准火候时序、锁鲜技巧、药膳融合建议。`
     },
     {
         id: 'zhe',
@@ -39,7 +39,7 @@ export const cuisines: CuisineType[] = [
         description: '江南水乡的清雅之味',
         avatar: '👨‍🍳',
         specialty: '清香淡雅,鲜嫩爽滑',
-        prompt: `你是一位浙菜大师,精通浙江菜系。浙菜以清香淡雅、鲜嫩爽滑、原汁原味著称,讲究时令和食材的自然美味。请根据用户提供的食材,设计一道精美的浙菜,体现江南水乡的清雅风味。回答要包含菜名、详细制作步骤、时令特色和烹饪要点。`
+        prompt: `作为浙菜传人,你深得南宋遗风真传。浙菜追求清雅时鲜、南料北烹、滑嫩见长。请基于用户食材创作江南风韵菜,突出时令搭配与脆嫩口感。回答需包含:诗意菜名、分步滑炒技法、时令食材解析、勾芡要诀。`
     },
     {
         id: 'xiang',
@@ -47,7 +47,7 @@ export const cuisines: CuisineType[] = [
         description: '湖湘文化的辣味人生',
         avatar: '👨‍🍳',
         specialty: '香辣浓郁,口味厚重',
-        prompt: `你是一位湘菜大师,精通湖南菜系。湘菜以香辣浓郁、口味厚重、色泽鲜艳著称,有"无辣不成菜"的特色。请根据用户提供的食材,设计一道正宗的湘菜,突出香辣特色和浓郁口感。回答要包含菜名、详细制作步骤、辣椒运用和湘菜精髓。`
+        prompt: `身为湘味宗师,你精通腊熏剁椒秘技。湘菜讲究酸辣透味、油重色浓、乡野本真。请针对用户食材设计火辣湘肴,突出发酵辣味与油色融合。回答需包含:霸气菜名、三重辣味调制法、腊味处理秘笈、油色控制要诀。`
     },
     {
         id: 'min',
@@ -55,7 +55,7 @@ export const cuisines: CuisineType[] = [
         description: '八闽大地的海鲜盛宴',
         avatar: '👨‍🍳',
         specialty: '鲜香清淡,汤鲜味美',
-        prompt: `你是一位闽菜大师,精通福建菜系。闽菜以鲜香清淡、汤鲜味美、海鲜丰富著称,注重原料的鲜美和营养。请根据用户提供的食材,设计一道精致的闽菜,体现海鲜的鲜美和汤品的清香。回答要包含菜名、详细制作步骤、汤品调制和海鲜处理技巧。`
+        prompt: `作为闽菜大家,你传承佛跳墙精髓。闽菜擅长汤醇味隽、糟香四溢、山珍海味。请依据用户食材创作闽派珍馐,突出红糟提鲜与汤品层次。回答需包含:典故菜名、吊汤八法详解、海鲜保鲜术、糟汁调配比例。`
     },
     {
         id: 'hui',
@@ -63,6 +63,6 @@ export const cuisines: CuisineType[] = [
         description: '徽州文化的朴实醇香',
         avatar: '👨‍🍳',
         specialty: '重油重色,醇厚朴实',
-        prompt: `你是一位徽菜大师,精通安徽菜系。徽菜以重油重色、醇厚朴实、山珍野味著称,体现了徽州文化的深厚底蕴。请根据用户提供的食材,设计一道传统的徽菜,突出醇厚的口感和朴实的风味。回答要包含菜名、详细制作步骤、传统技法和文化内涵。`
+        prompt: `身为徽菜掌门,你掌握文火炖焖绝技。徽菜强调重油保色、火腿提鲜、山野本味。请针对用户食材设计徽州古韵菜,突出炭火慢炖与油色控制。回答需包含:徽派菜名、三阶段火功法、火腿吊味技巧、收汁成色要诀。`
     }
 ]

+ 8 - 3
src/services/aiService.ts

@@ -3,9 +3,14 @@ import type { Recipe, CuisineType } from '@/types'
 
 // AI服务配置 - 智谱AI
 const AI_CONFIG = {
-    baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
-    apiKey: 'a835b9f6866d48ec956d341418df8a50.NuhlKYn58EkCb5iP',
-    model: 'glm-4-flash-250414',
+    // baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
+    // apiKey: 'a835b9f6866d48ec956d341418df8a50.NuhlKYn58EkCb5iP',
+    // model: 'glm-4-flash-250414',
+    // temperature: 0.7,
+    // timeout: 30000
+    baseURL: 'https://api.deepseek.com/v1/',
+    apiKey: 'sk-78d4fed678fa4a5ebc5f7beac54b1a78',
+    model: 'deepseek-chat',
     temperature: 0.7,
     timeout: 30000
 }

+ 60 - 0
src/services/imageService.ts

@@ -0,0 +1,60 @@
+import type { Recipe } from '@/types'
+
+const API_KEY = 'a835b9f6866d48ec956d341418df8a50.NuhlKYn58EkCb5iP'
+const API_URL = 'https://open.bigmodel.cn/api/paas/v4/images/generations'
+
+export interface GeneratedImage {
+    url: string
+    id: string
+}
+
+export const generateRecipeImage = async (recipe: Recipe): Promise<GeneratedImage> => {
+    // 构建图片生成的提示词
+    const prompt = buildImagePrompt(recipe)
+
+    const sizeToUse = { width: 1024, height: 1024 }
+
+    try {
+        const response = await fetch(API_URL, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                Authorization: `Bearer ${API_KEY}`
+            },
+            body: JSON.stringify({
+                model: 'cogview-3-flash',
+                prompt: prompt,
+                size: `${sizeToUse.width}x${sizeToUse.height}`,
+                n: 1,
+                style: 'vivid',
+                quality: 'hd'
+            })
+        })
+
+        if (!response.ok) {
+            throw new Error(`API请求失败: ${response.status}`)
+        }
+
+        const data = await response.json()
+
+        if (data.data && data.data.length > 0) {
+            return {
+                url: data.data[0].url,
+                id: `${recipe.id}-${Date.now()}`
+            }
+        } else {
+            throw new Error('API返回数据格式错误')
+        }
+    } catch (error) {
+        console.error('生成图片失败:', error)
+        throw error
+    }
+}
+
+const buildImagePrompt = (recipe: Recipe): string => {
+    // 根据菜谱信息构建详细的图片生成提示词
+    const ingredients = recipe.ingredients.join('、')
+    const cuisineStyle = recipe.cuisine.replace('大师', '').replace('菜', '')
+
+    return `一道精美的${cuisineStyle}菜肴:${recipe.name},主要食材包括${ingredients}。菜品摆盘精致,色彩丰富,光线柔和,专业美食摄影风格,高清画质,餐厅级别的视觉效果。背景简洁,突出菜品本身的美感。`
+}

+ 19 - 11
src/views/Home.vue

@@ -2,9 +2,9 @@
     <div class="min-h-screen bg-yellow-400">
         <!-- 头部 - 粉色区域 -->
         <header class="bg-pink-400 border-4 border-black mx-4 mt-4 rounded-lg relative">
-            <div class="absolute top-2 right-2">
+            <!-- <div class="absolute top-2 right-2">
                 <button class="bg-white/20 hover:bg-white/30 rounded-full px-3 py-1 text-sm text-white transition-colors">中文</button>
-            </div>
+            </div> -->
             <div class="text-center py-8">
                 <h1 class="text-5xl font-black text-yellow-300 mb-2 tracking-wider">一饭封神</h1>
                 <p class="text-white text-lg font-medium">UPLOAD YOUR INGREDIENTS | SPIT OUT RECIPES!</p>
@@ -27,7 +27,7 @@
             </div>
         </div> -->
 
-        <div class="max-w-4xl mx-auto px-4 py-6">
+        <div class="max-w-5xl mx-auto px-4 py-6">
             <!-- 步骤1: 输入食材 -->
             <div class="mb-6">
                 <div class="bg-pink-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
@@ -99,14 +99,14 @@
                     <div class="bg-green-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
                         <span class="font-bold">2. 选择菜系</span>
                     </div>
-                    <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-6">
+                    <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-6 h-full">
                         <div v-if="customPrompt.trim()" class="text-center py-8 text-gray-500">
                             <p class="text-sm">已设置自定义要求,将忽略菜系选择</p>
                             <button @click="customPrompt = ''" class="text-blue-600 hover:text-blue-700 underline text-sm mt-2">清除自定义要求以选择菜系</button>
                         </div>
                         <div v-else class="grid grid-cols-2 gap-3">
                             <button
-                                v-for="cuisine in cuisines.slice(0, 6)"
+                                v-for="cuisine in cuisines.slice(0, 10)"
                                 :key="cuisine.id"
                                 @click="selectCuisine(cuisine)"
                                 :class="[
@@ -125,21 +125,20 @@
                     <div class="bg-blue-400 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
                         <span class="font-bold">3. 或自定义要求</span>
                     </div>
-                    <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-6">
-                        <div class="mb-3">
+                    <div class="bg-white border-2 border-black rounded-lg rounded-tl-none p-6 h-full flex flex-col">
+                        <div class="flex-1">
                             <label class="block text-sm font-bold text-dark-800 mb-2">描述你的需求:</label>
                             <textarea
                                 v-model="customPrompt"
                                 placeholder="例如:做一道清淡的汤,适合老人食用..."
-                                class="w-full p-3 border-2 border-gray-300 rounded-lg text-sm resize-none focus:outline-none focus:border-blue-400"
-                                rows="4"
+                                class="w-full p-4 border-2 border-gray-300 rounded-lg text-base resize-none focus:outline-none focus:border-blue-400 h-40"
                             ></textarea>
                             <div v-if="customPrompt.trim()" class="mt-2 flex justify-between items-center">
                                 <span class="text-xs text-green-600">✓ 已设置自定义要求</span>
                                 <button @click="customPrompt = ''" class="text-xs text-red-600 hover:text-red-700 underline">清除</button>
                             </div>
                         </div>
-                        <p class="text-xs text-gray-500">越具体越好!</p>
+                        <p class="text-xs text-gray-500 mt-auto">越具体越好!</p>
                     </div>
                 </div>
             </div>
@@ -150,7 +149,7 @@
             </div>
 
             <!-- 步骤4: 结果 -->
-            <div>
+            <div ref="resultsSection">
                 <div class="bg-dark-800 text-white px-4 py-2 rounded-t-lg border-2 border-black border-b-0 inline-block">
                     <span class="font-bold">4. 菜谱结果</span>
                 </div>
@@ -203,6 +202,7 @@ const customPrompt = ref('')
 const recipes = ref<Recipe[]>([])
 const isLoading = ref(false)
 const loadingText = ref('大师正在挑选食材...')
+const resultsSection = ref<HTMLElement | null>(null)
 
 // 加载文字轮播
 const loadingTexts = [
@@ -253,6 +253,14 @@ const generateRecipes = async () => {
     isLoading.value = true
     recipes.value = []
 
+    // 滚动到结果区域
+    if (resultsSection.value) {
+        resultsSection.value.scrollIntoView({
+            behavior: 'smooth',
+            block: 'start'
+        })
+    }
+
     // 开始加载文字轮播
     let textIndex = 0
     loadingInterval = setInterval(() => {