practice_code/web/graduation/src/pages/cuisine/index.astro

893 lines
42 KiB
Plaintext
Raw Normal View History

2025-03-23 01:42:26 +08:00
---
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>