practice_code/web/graduation/src/pages/cuisine/index.astro
2025-03-23 01:42:26 +08:00

893 lines
42 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import MainLayout from "../../layouts/MainLayout.astro";
import { getCollection, type CollectionEntry } from "astro:content";
import ScrollReveal from "../../components/aceternity/ScrollReveal.astro";
// 获取美食内容集合
const cuisines = await getCollection("cuisine");
// 按照日期排序
const sortByDate = <T extends { data: { pubDate?: Date | string, updatedDate?: Date | string } }>(a: T, b: T): number => {
return new Date(b.data.pubDate || b.data.updatedDate || 0).getTime() -
new Date(a.data.pubDate || a.data.updatedDate || 0).getTime();
};
// 按发布日期排序
const sortedCuisines = [...cuisines].sort(sortByDate);
// 提取所有标签
const allTags: {name: string, count: number}[] = [];
sortedCuisines.forEach((cuisine: CollectionEntry<"cuisine">) => {
cuisine.data.tags.forEach((tag: string) => {
const existingTag = allTags.find(t => t.name === tag);
if (existingTag) {
existingTag.count++;
} else {
allTags.push({ name: tag, count: 1 });
}
});
});
// 按照标签出现次数排序
allTags.sort((a, b) => b.count - a.count);
// 获取所有分类并计数
const categories: {name: string, count: number}[] = [];
sortedCuisines.forEach((cuisine: CollectionEntry<"cuisine">) => {
if (cuisine.data.category) {
const existingCategory = categories.find(c => c.name === cuisine.data.category);
if (existingCategory) {
existingCategory.count++;
} else {
categories.push({ name: cuisine.data.category, count: 1 });
}
}
});
// 按照分类出现次数排序
categories.sort((a, b) => b.count - a.count);
// 获取所有产地并计数
const citys: {name: string, count: number}[] = [];
sortedCuisines.forEach((cuisine: CollectionEntry<"cuisine">) => {
if (cuisine.data.city) {
const existingcity = citys.find(o => o.name === cuisine.data.city);
if (existingcity) {
existingcity.count++;
} else {
citys.push({ name: cuisine.data.city, count: 1 });
}
}
});
// 按照产地出现次数排序
citys.sort((a, b) => b.count - a.count);
// 获取所有口味并计数
const tastes: {name: string, count: number}[] = [];
sortedCuisines.forEach((cuisine: CollectionEntry<"cuisine">) => {
if (cuisine.data.taste) {
const existingTaste = tastes.find(t => t.name === cuisine.data.taste);
if (existingTaste) {
existingTaste.count++;
} else {
tastes.push({ name: cuisine.data.taste, count: 1 });
}
}
});
// 按照口味出现次数排序
tastes.sort((a, b) => b.count - a.count);
// 分页逻辑
const itemsPerPage = 9;
// 从URL参数获取当前页码
const url = new URL(Astro.request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const totalPages = Math.ceil(sortedCuisines.length / itemsPerPage);
const currentPageCuisines = sortedCuisines.slice((page - 1) * itemsPerPage, page * itemsPerPage);
// 搜索和筛选逻辑(实际应用中应该根据查询参数来筛选)
const searchQuery = '';
const selectedCategory = '';
const selectedcity = '';
const selectedTaste = '';
const selectedTags: string[] = [];
const sortBy: 'date' | 'name' = 'date';
// 分页参数
const queryParams = ''; // 在实际应用中,这里应该是基于筛选条件构建的查询字符串
---
<MainLayout title="河北美食食谱 - 河北游礼">
<!-- 食谱风格头部 -->
<div class="relative overflow-hidden bg-recipe-paper dark:bg-recipe-paper-dark min-h-[400px] flex items-center">
<!-- 纸张纹理和装饰 -->
<div class="absolute inset-0 bg-[url('/images/recipe-paper-texture.png')] opacity-10"></div>
<div class="absolute top-0 left-0 w-32 h-32 bg-[url('/images/spoon-fork.png')] bg-no-repeat bg-contain opacity-15 -rotate-12"></div>
<div class="absolute bottom-0 right-0 w-32 h-32 bg-[url('/images/chef-hat.png')] bg-no-repeat bg-contain opacity-15 rotate-12"></div>
<!-- 食谱标题区域 -->
<div class="container mx-auto px-4 relative z-10">
<div class="max-w-4xl mx-auto text-center recipe-card">
<!-- 食谱卡片装饰 -->
<div class="recipe-card-pins absolute -top-3 left-1/2 transform -translate-x-1/2">
<div class="w-6 h-6 bg-red-500 dark:bg-red-700 rounded-full absolute -left-16 shadow-md"></div>
<div class="w-6 h-6 bg-amber-500 dark:bg-amber-700 rounded-full absolute left-16 shadow-md"></div>
</div>
<!-- 手写风格标题 -->
<div class="handwritten-title py-6">
<h1 class="text-6xl md:text-7xl font-recipe text-brown-900 dark:text-brown-100 mb-2 recipe-title">河北美食食谱</h1>
<div class="w-3/4 mx-auto h-px bg-brown-300 dark:bg-brown-700 my-6"></div>
<p class="text-xl text-brown-700 dark:text-brown-300 font-recipe-body mb-6 leading-relaxed">
收集自河北各地的传统美食配方,
<br>家传秘方与地方特色,尽在此食谱
</p>
</div>
<!-- 食谱元数据 -->
<div class="recipe-metadata flex flex-wrap justify-center gap-8 text-sm text-brown-600 dark:text-brown-400 mb-8">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span>{cuisines.length} 道经典菜肴</span>
</div>
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{citys.length} 个地方特色</span>
</div>
<div class="flex items-center">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
</svg>
<span>收集于 2023 年</span>
</div>
</div>
<!-- 食谱快速搜索 -->
<div class="recipe-search relative max-w-lg mx-auto mb-6">
<input
type="text"
placeholder="搜索菜名、食材或地区..."
class="w-full px-4 py-3 border-2 border-secondary-300 dark:border-secondary-700 bg-recipe-paper/80 dark:bg-recipe-paper-dark/90 rounded-md font-recipe-body text-secondary-900 dark:text-secondary-200 placeholder-secondary-500 dark:placeholder-secondary-400 focus:outline-none focus:border-primary-500 dark:focus:border-primary-400"
/>
<button class="absolute right-3 top-3 text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-300">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button>
</div>
<!-- 食谱标签 -->
<div class="recipe-tags flex flex-wrap justify-center gap-2">
<a href="#" class="px-3 py-1 bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200 text-sm rounded-full font-recipe-body hover:bg-primary-200 dark:hover:bg-primary-800 transition-colors">家常菜</a>
<a href="#" class="px-3 py-1 bg-accent-100 dark:bg-accent-900/50 text-accent-800 dark:text-accent-200 text-sm rounded-full font-recipe-body hover:bg-accent-200 dark:hover:bg-accent-800 transition-colors">传统名菜</a>
<a href="#" class="px-3 py-1 bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200 text-sm rounded-full font-recipe-body hover:bg-green-200 dark:hover:bg-green-800 transition-colors">地方特色</a>
<a href="#" class="px-3 py-1 bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-200 text-sm rounded-full font-recipe-body hover:bg-orange-200 dark:hover:bg-orange-800 transition-colors">小吃点心</a>
</div>
</div>
</div>
<!-- 装饰性图案 -->
<div class="absolute -top-4 -left-4 w-24 h-24 border-t-4 border-l-4 border-brown-300 dark:border-brown-700 opacity-50"></div>
<div class="absolute -bottom-4 -right-4 w-24 h-24 border-b-4 border-r-4 border-brown-300 dark:border-brown-700 opacity-50"></div>
</div>
<!-- 主内容区域 - 食谱风格 -->
<div class="bg-recipe-paper dark:bg-recipe-paper-dark py-12">
<div class="container mx-auto px-4">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- 左侧筛选区域 - 食谱风格 -->
<div class="lg:col-span-3">
<div class="sticky top-20 space-y-8">
<!-- 食谱筛选卡片 -->
<div class="recipe-card-item p-6">
<!-- 搜索筛选框 -->
<div class="mb-8">
<h3 class="text-base font-recipe mb-4 text-brown-800 dark:text-brown-200 flex items-center">
<svg class="w-4 h-4 mr-2 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
食谱检索
</h3>
<div class="relative">
<input
type="text"
placeholder="输入菜名或食材..."
class="w-full bg-amber-50/50 dark:bg-amber-900/20 border border-brown-300 dark:border-brown-700 px-4 py-2 text-brown-800 dark:text-brown-200 focus:outline-none focus:border-amber-500 dark:focus:border-amber-400 focus:ring-2 focus:ring-amber-200 dark:focus:ring-amber-800/50 rounded-md font-recipe-body text-sm"
/>
<div class="absolute right-3 top-2 text-brown-500 dark:text-brown-400">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
<!-- 菜系筛选 -->
<div class="mb-8">
<h3 class="text-base font-recipe mb-4 text-brown-800 dark:text-brown-200 flex items-center">
<svg class="w-4 h-4 mr-2 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h14a2 2 0 012 2v14a2 2 0 01-2 2z" />
</svg>
菜系
</h3>
<div class="space-y-3">
{categories.map((category, index) => (
<label class="flex items-center group cursor-pointer font-recipe-body">
<div class="w-4 h-4 border border-brown-400 dark:border-brown-600 mr-3 group-hover:border-amber-500 dark:group-hover:border-amber-400 transition-colors"></div>
<div class="font-light text-brown-700 dark:text-brown-300 group-hover:text-amber-700 dark:group-hover:text-amber-300 transition-colors flex items-center justify-between w-full">
<span>{category.name}</span>
<span class="text-amber-600/70 dark:text-amber-500/50 bg-amber-50/80 dark:bg-amber-900/30 px-1.5 py-0.5 rounded-full text-xs">{category.count}</span>
</div>
</label>
))}
</div>
</div>
<!-- 地域筛选 -->
<div class="mb-8">
<h3 class="text-base font-recipe mb-4 text-brown-800 dark:text-brown-200 flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
地域特色
</h3>
<div class="grid grid-cols-2 gap-3">
{citys.map((city) => (
<label class="flex items-center group cursor-pointer font-recipe-body">
<div class="w-4 h-4 border border-brown-400 dark:border-brown-600 mr-2 group-hover:border-green-500 dark:group-hover:border-green-400 transition-colors"></div>
<div class="font-light text-brown-700 dark:text-brown-300 group-hover:text-green-700 dark:group-hover:text-green-300 transition-colors text-sm truncate">
<span>{city.name}</span> <span class="text-green-600/70 dark:text-green-500/50 text-xs">({city.count})</span>
</div>
</label>
))}
</div>
</div>
<!-- 口味筛选 -->
<div class="mb-8">
<h3 class="text-base font-recipe mb-4 text-brown-800 dark:text-brown-200 flex items-center">
<svg class="w-4 h-4 mr-2 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
味道特点
</h3>
<div class="flex flex-wrap gap-2">
{tastes.map((taste) => (
<label class="inline-flex items-center group cursor-pointer px-3 py-1 bg-orange-50/70 dark:bg-orange-900/30 border border-orange-200 dark:border-orange-800 rounded-full font-recipe-body">
<div class="w-3 h-3 border border-orange-400 dark:border-orange-500 mr-2 rounded-full group-hover:bg-orange-400 dark:group-hover:bg-orange-500 transition-colors"></div>
<div class="font-light text-orange-800 dark:text-orange-300 text-xs">
{taste.name} <span class="text-orange-600/70 dark:text-orange-500/50">({taste.count})</span>
</div>
</label>
))}
</div>
</div>
<!-- 食材筛选 -->
<div>
<h3 class="text-base font-recipe mb-4 text-brown-800 dark:text-brown-200 flex items-center">
<svg class="w-4 h-4 mr-2 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
主要食材
</h3>
<div class="flex flex-wrap gap-2">
{allTags.map((tag, i) => {
// 为标签生成不同的颜色
const colors = ['amber', 'red', 'green', 'orange', 'amber', 'red'];
const colorClasses = {
'amber': 'border-amber-200 dark:border-amber-800 text-amber-700 dark:text-amber-300 hover:text-amber-900 dark:hover:text-amber-200 hover:border-amber-400 dark:hover:border-amber-700 bg-amber-50/50 dark:bg-amber-900/20',
'red': 'border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 hover:text-red-900 dark:hover:text-red-200 hover:border-red-400 dark:hover:border-red-700 bg-red-50/50 dark:bg-red-900/20',
'green': 'border-green-200 dark:border-green-800 text-green-700 dark:text-green-300 hover:text-green-900 dark:hover:text-green-200 hover:border-green-400 dark:hover:border-green-700 bg-green-50/50 dark:bg-green-900/20',
'orange': 'border-orange-200 dark:border-orange-800 text-orange-700 dark:text-orange-300 hover:text-orange-900 dark:hover:text-orange-200 hover:border-orange-400 dark:hover:border-orange-700 bg-orange-50/50 dark:bg-orange-900/20'
};
const color = colors[i % colors.length] as keyof typeof colorClasses;
return (
<div class={`inline-block px-3 py-1 text-xs border rounded-full cursor-pointer transition-colors font-recipe-body ${colorClasses[color]}`}>
{tag.name}
</div>
);
})}
</div>
</div>
</div>
<!-- 烹饪小贴士 -->
<div class="recipe-card-item p-5 mt-6">
<div class="flex items-start space-x-3">
<div class="text-amber-600 dark:text-amber-400">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h4 class="font-recipe text-base text-brown-800 dark:text-brown-200 mb-2">食谱小贴士</h4>
<p class="text-sm text-brown-700 dark:text-brown-300 leading-relaxed font-recipe-body">
烹饪是一门艺术,每一道河北美食都融合了独特的地域文化和历史传承,讲究用料、火候与调味的绝妙平衡。
</p>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧食谱展示区 -->
<div class="lg:col-span-9">
<!-- 筛选状态 -->
{(searchQuery || selectedCategory || selectedcity || selectedTaste || selectedTags.length > 0) && (
<div class="mb-8 bg-amber-50/50 dark:bg-amber-900/20 p-4 border-l-4 border-amber-300 dark:border-amber-700 rounded-r-lg">
<div class="flex flex-wrap items-center gap-3 text-sm text-brown-700 dark:text-brown-300">
<div class="font-recipe text-xs tracking-wider text-amber-700 dark:text-amber-300 bg-amber-100/70 dark:bg-amber-900/50 px-2 py-1 rounded">筛选条件</div>
{/* 筛选条件显示 */}
<button class="ml-auto text-red-600 hover:text-red-800 dark:hover:text-red-300 text-sm flex items-center space-x-1 bg-white/80 dark:bg-black/30 px-3 py-1 rounded-full font-recipe-body">
<span>重置</span>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)}
<!-- 食谱卡片列表 -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{currentPageCuisines.map((cuisine, index) => {
// 随机食谱纸张背景颜色
const paperColors = [
'bg-amber-50/80 border-amber-200 dark:bg-amber-900/30 dark:border-amber-800',
'bg-orange-50/80 border-orange-200 dark:bg-orange-900/30 dark:border-orange-800',
'bg-red-50/80 border-red-200 dark:bg-red-900/30 dark:border-red-800',
'bg-green-50/80 border-green-200 dark:bg-green-900/30 dark:border-green-800'
];
const paperColor = paperColors[index % paperColors.length];
return (
<ScrollReveal animation="fade">
<a href={`/cuisine/${cuisine.slug}`} class="block group">
<div class={`recipe-card-item border transition-all duration-300 ${paperColor}`}>
<!-- 食谱卡片头部 -->
<div class="aspect-[4/3] relative overflow-hidden">
<div class={`absolute inset-0 bg-recipe-paper dark:bg-recipe-paper-dark flex items-center justify-center`}>
<span class="text-brown-400 dark:text-brown-600 font-recipe-body">{cuisine.data.title}</span>
</div>
<!-- 图钉装饰 -->
<div class="absolute top-3 left-3 w-4 h-4 bg-red-500 dark:bg-red-700 rounded-full shadow-sm"></div>
<div class="absolute top-3 right-3 w-4 h-4 bg-amber-500 dark:bg-amber-700 rounded-full shadow-sm"></div>
{cuisine.data.category && (
<div class="absolute bottom-3 right-3 px-2 py-1 text-xs font-recipe-body bg-white/90 dark:bg-black/70 text-brown-800 dark:text-brown-200 rounded-md shadow-sm border border-brown-200 dark:border-brown-800">
{cuisine.data.category}
</div>
)}
<!-- 推荐标记 -->
{Math.random() > 0.7 && (
<div class="absolute -top-1 -left-1 rotate-12">
<div class="bg-red-600 text-white px-6 py-1 text-xs font-recipe transform -rotate-45 shadow-md">
推荐
</div>
</div>
)}
</div>
<!-- 食谱内容 -->
<div class="p-5">
<h3 class="text-xl font-recipe text-brown-900 dark:text-brown-100 mb-3 group-hover:text-amber-700 dark:group-hover:text-amber-400 transition-colors">
{cuisine.data.title}
</h3>
<!-- 食谱元数据 -->
<div class="flex flex-wrap gap-3 text-sm mb-3">
{cuisine.data.city && (
<div class="flex items-center px-2 py-0.5 bg-amber-50/80 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-full">
<span class="text-amber-800 dark:text-amber-300 text-xs font-recipe-body">{cuisine.data.city}</span>
</div>
)}
{cuisine.data.taste && (
<div class="flex items-center px-2 py-0.5 bg-red-50/80 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-full">
<span class="text-red-800 dark:text-red-300 text-xs font-recipe-body">{cuisine.data.taste}</span>
</div>
)}
</div>
<!-- 食谱描述 -->
<p class="text-brown-700 dark:text-brown-300 text-sm line-clamp-3 mb-4 font-recipe-body">
{cuisine.data.description}
</p>
<!-- 食材标签 -->
<div class="flex flex-wrap gap-2 mb-4">
{cuisine.data.tags.slice(0, 3).map((tag: string, i: number) => (
<span class="px-2 py-0.5 text-xs font-recipe-body bg-brown-100/80 dark:bg-brown-900/30 text-brown-800 dark:text-brown-200 border border-brown-200 dark:border-brown-800 rounded-full">
#{tag}
</span>
))}
{cuisine.data.tags.length > 3 && (
<span class="px-2 py-0.5 text-xs font-recipe-body bg-brown-100/80 dark:bg-brown-900/30 text-brown-800 dark:text-brown-200 border border-brown-200 dark:border-brown-800 rounded-full">
+{cuisine.data.tags.length - 3}
</span>
)}
</div>
<!-- 查看详情 -->
<div class="flex justify-between items-center">
<div class="text-sm text-amber-700 dark:text-amber-300 flex items-center group-hover:translate-x-1 transition-transform font-recipe-body">
查看详细食谱
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</div>
<!-- 食谱标记 -->
{Math.random() > 0.5 && (
<span class="text-xs px-2 py-0.5 bg-orange-100/80 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 border border-orange-200 dark:border-orange-800 rounded-full font-recipe-body">传统</span>
)}
{Math.random() > 0.7 && (
<span class="text-xs px-2 py-0.5 bg-green-100/80 dark:bg-green-900/30 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800 rounded-full font-recipe-body">家常</span>
)}
</div>
</div>
</div>
</a>
</ScrollReveal>
);
})}
</div>
<!-- 分页控件 - 食谱风格分页 -->
<div class="flex justify-center items-center space-x-2 mt-12">
{totalPages > 1 && (
<div class="flex flex-wrap gap-2 items-center bg-recipe-paper-light dark:bg-recipe-paper-dark p-4 rounded-lg border border-amber-200 dark:border-amber-800 shadow-md">
<!-- 上一页按钮 -->
<a
href={page > 1 ? `?page=${page - 1}${queryParams}` : '#'}
class={`flex items-center px-3 py-1.5 ${
page === 1
? 'text-brown-400 dark:text-brown-600 cursor-not-allowed'
: 'text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/30'
} rounded-md font-recipe-body transition-colors`}
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
上一页
</a>
<!-- 页码 -->
{Array.from({ length: totalPages }, (_, i) => i + 1).map((pageNum) => (
<a
href={`?page=${pageNum}${queryParams}`}
class={`px-3 py-1.5 font-recipe ${
page === pageNum
? 'bg-amber-600 text-white dark:bg-amber-700'
: 'text-brown-700 dark:text-brown-300 hover:bg-amber-100 dark:hover:bg-amber-900/30'
} rounded-md transition-colors`}
>
{pageNum}
</a>
))}
<!-- 下一页按钮 -->
<a
href={page < totalPages ? `?page=${page + 1}${queryParams}` : '#'}
class={`flex items-center px-3 py-1.5 ${
page === totalPages
? 'text-brown-400 dark:text-brown-600 cursor-not-allowed'
: 'text-amber-700 dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/30'
} rounded-md font-recipe-body transition-colors`}
>
下一页
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</a>
</div>
)}
</div>
<!-- 食谱主题页脚 -->
<div class="mt-16 text-center">
<div class="flex flex-col items-center">
<div class="text-amber-600 dark:text-amber-400 mb-3">
<img src="/images/cuisine/chef-hat.svg" alt="Chef Hat" class="w-12 h-12 mx-auto opacity-80" />
</div>
<p class="text-brown-700 dark:text-brown-300 font-recipe max-w-2xl mx-auto">
河北美食宝库收录了<span class="text-amber-600 dark:text-amber-400 mx-1">{cuisines.length}+</span>种传统佳肴与地方特色小吃。
每一道食谱都承载着我们的文化记忆和烹饪智慧。尽情探索,找到属于你的美食灵感!
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</MainLayout>
<style>
.bg-recipe-paper {
background-color: var(--bg-recipe);
}
.bg-recipe-paper-dark {
background-color: var(--bg-recipe);
}
.bg-recipe-paper-light {
background-color: var(--color-primary-50);
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23f59e0b' fill-opacity='0.05' fill-rule='evenodd'/%3E%3C/svg%3E");
}
.bg-wood-texture {
background-color: var(--color-secondary-300);
background-image: url('/images/wood-texture.png');
background-blend-mode: multiply;
}
.bg-wood-texture-dark {
background-color: var(--color-secondary-900);
background-image: url('/images/wood-texture.png');
background-blend-mode: overlay;
}
.text-brown-900 {
color: var(--color-secondary-900);
}
.text-brown-800 {
color: var(--color-secondary-800);
}
.text-brown-700 {
color: var(--color-secondary-700);
}
.text-brown-600 {
color: var(--color-secondary-600);
}
.text-brown-500 {
color: var(--color-secondary-500);
}
.text-brown-400 {
color: var(--color-secondary-400);
}
.text-brown-300 {
color: var(--color-secondary-300);
}
.text-brown-200 {
color: var(--color-secondary-200);
}
.text-brown-100 {
color: var(--color-secondary-100);
}
.recipe-card {
position: relative;
padding: 1.5rem;
background-color: #f8f3e9;
border-radius: 0.5rem;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
:root.dark .recipe-card {
background-color: #2c2419;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}
.recipe-title {
position: relative;
display: inline-block;
}
.recipe-title::after {
content: '';
position: absolute;
bottom: -0.5rem;
left: 1rem;
right: 1rem;
height: 2px;
background-color: #b08f77;
transform: rotate(-0.5deg);
}
:root.dark .recipe-title::after {
background-color: #7c604a;
}
/* 手写风格的下划线 */
.handwritten-title p {
position: relative;
}
.handwritten-title p::after {
content: '';
position: absolute;
bottom: -0.25rem;
left: 25%;
right: 25%;
height: 1px;
background-color: #b08f77;
transform: rotate(-0.25deg);
}
:root.dark .handwritten-title p::after {
background-color: #7c604a;
}
/* 食谱卡片样式 */
.recipe-card-item {
position: relative;
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
background-color: var(--recipe-card-bg, rgba(255, 251, 235, 0.9));
}
.dark .recipe-card-item {
background-color: var(--recipe-card-bg-dark, rgba(120, 53, 15, 0.1));
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* 隐藏滚动条但保留功能 */
.hide-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* 食谱纸张纹理动画 */
@keyframes textureFloat {
0%, 100% { background-position: 0% 0%; }
50% { background-position: 1% 1%; }
}
.bg-recipe-paper, .bg-recipe-paper-dark {
animation: textureFloat 10s ease-in-out infinite;
background-size: 200px 200px;
}
/* 装饰性图案旋转动画 */
@keyframes slowRotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.recipe-decoration {
animation: slowRotate 120s linear infinite;
}
/* 食谱卡片悬停效果 */
.recipe-card-item:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
:root.dark .recipe-card-item:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
</style>
<script>
// 页面加载完成后执行
document.addEventListener('DOMContentLoaded', () => {
// 检测当前颜色模式
const isDarkMode = document.documentElement.classList.contains('dark');
// 获取元素
const searchInput = document.querySelector('input[type="text"]') as HTMLInputElement;
const categoryLabels = document.querySelectorAll('.font-recipe-body');
const resetButton = document.querySelector('button');
const recipeCards = document.querySelectorAll('.recipe-card-item');
const paperBg = document.querySelector('.bg-recipe-paper');
// 食谱卡片初始样式
recipeCards.forEach((card, index) => {
// 设置初始状态
(card as HTMLElement).style.opacity = '0';
(card as HTMLElement).style.transform = 'translateY(20px)';
(card as HTMLElement).style.transition = 'all 0.4s ease-out';
});
// 为食谱卡片添加进场动画
setTimeout(() => {
recipeCards.forEach((card, index) => {
setTimeout(() => {
(card as HTMLElement).style.opacity = '1';
(card as HTMLElement).style.transform = 'translateY(0)';
}, 100 + (index * 50));
});
}, 200);
// 食谱卡片悬停效果
recipeCards.forEach(card => {
card.addEventListener('mouseenter', () => {
(card as HTMLElement).style.transform = 'translateY(-5px)';
(card as HTMLElement).style.boxShadow = isDarkMode
? '0 10px 15px -3px rgba(0, 0, 0, 0.4)'
: '0 10px 15px -3px rgba(251, 191, 36, 0.1), 0 4px 6px -2px rgba(251, 191, 36, 0.05)';
});
card.addEventListener('mouseleave', () => {
(card as HTMLElement).style.transform = 'translateY(0)';
(card as HTMLElement).style.boxShadow = isDarkMode
? '0 4px 8px rgba(0, 0, 0, 0.2)'
: '0 4px 8px rgba(0, 0, 0, 0.05)';
});
});
// 搜索框交互效果
if (searchInput) {
searchInput.addEventListener('focus', () => {
if (searchInput.parentElement) {
searchInput.parentElement.classList.add('ring-2', 'ring-amber-200');
if (isDarkMode) {
searchInput.parentElement.classList.add('ring-amber-800/50');
}
}
});
searchInput.addEventListener('blur', () => {
if (searchInput.parentElement) {
searchInput.parentElement.classList.remove('ring-2', 'ring-amber-200', 'ring-amber-800/50');
}
});
// 搜索功能
searchInput.addEventListener('keyup', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
const searchValue = searchInput.value.trim();
if (searchValue) {
// 构建搜索URL并跳转
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.set('search', searchValue);
currentUrl.searchParams.delete('page'); // 重置页码
window.location.href = currentUrl.toString();
}
}
});
}
// 筛选标签点击效果
const filterLabels = document.querySelectorAll('.recipe-card-item label');
filterLabels.forEach(label => {
label.addEventListener('click', () => {
const checkbox = label.querySelector('div:first-child');
if (checkbox) {
// 切换选中状态
if (checkbox.classList.contains('bg-amber-500') ||
checkbox.classList.contains('bg-red-500') ||
checkbox.classList.contains('bg-green-500') ||
checkbox.classList.contains('bg-orange-500')) {
checkbox.classList.remove('bg-amber-500', 'bg-red-500', 'bg-green-500', 'bg-orange-500');
} else {
const labelText = label.textContent || '';
if (labelText.includes('菜系')) {
checkbox.classList.add('bg-amber-500');
} else if (labelText.includes('地域')) {
checkbox.classList.add('bg-green-500');
} else if (labelText.includes('味道')) {
checkbox.classList.add('bg-orange-500');
} else {
checkbox.classList.add('bg-red-500');
}
}
}
// 注意:实际筛选逻辑需要后端支持,这里只是添加视觉反馈
});
});
// 美食标签点击事件
const tagButtons = document.querySelectorAll('.flex-wrap .inline-block');
tagButtons.forEach(tag => {
tag.addEventListener('click', () => {
// 为标签添加选中效果
tag.classList.toggle('ring-2');
tag.classList.toggle('ring-amber-400');
tag.classList.toggle('dark:ring-amber-600');
// 注意:实际筛选逻辑需要后端支持,这里只是添加视觉反馈
});
});
// 重置按钮效果
if (resetButton) {
resetButton.addEventListener('mouseenter', () => {
resetButton.classList.add('bg-red-50', 'dark:bg-red-900/30');
});
resetButton.addEventListener('mouseleave', () => {
resetButton.classList.remove('bg-red-50', 'dark:bg-red-900/30');
});
resetButton.addEventListener('click', () => {
// 重置所有筛选条件
window.location.href = window.location.pathname;
});
}
// 添加页面滚动动画效果
const addScrollAnimation = () => {
const elements = document.querySelectorAll('.recipe-card-item');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-fade-in');
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
elements.forEach(el => {
observer.observe(el);
});
};
// 如果浏览器支持IntersectionObserver添加滚动动画
if ('IntersectionObserver' in window) {
addScrollAnimation();
}
// 添加食谱纸张效果装饰元素
if (paperBg) {
// 创建随机装饰元素
const createDecorElement = (className: string, icon: string) => {
const element = document.createElement('div');
element.className = className;
element.innerHTML = icon;
element.style.position = 'absolute';
element.style.opacity = '0.15';
element.style.zIndex = '1';
return element;
};
// 添加食谱装饰
const decorations = [
{ icon: '🍴', className: 'recipe-decor text-2xl' },
{ icon: '🥄', className: 'recipe-decor text-2xl' },
{ icon: '🍽️', className: 'recipe-decor text-2xl' },
{ icon: '📝', className: 'recipe-decor text-2xl' },
{ icon: '⭐', className: 'recipe-decor text-2xl' }
];
decorations.forEach((decor) => {
const el = createDecorElement(decor.className, decor.icon);
el.style.left = `${Math.random() * 90}%`;
el.style.top = `${Math.random() * 90}%`;
el.style.transform = `rotate(${Math.random() * 360}deg)`;
(paperBg as HTMLElement).appendChild(el);
});
}
});
// 添加键盘快捷键支持
document.addEventListener('keydown', (e: KeyboardEvent) => {
// Alt + S 聚焦搜索框
if (e.altKey && e.key === 's') {
e.preventDefault();
const searchInput = document.querySelector('input[type="text"]') as HTMLInputElement;
if (searchInput) {
searchInput.focus();
}
}
});
</script>