569 lines
23 KiB
HTML
569 lines
23 KiB
HTML
<!--
|
||
文件名称: cookbook.html
|
||
文件作用: 菜谱单页展示 - 每页一道菜品,沉浸式卡片浏览体验
|
||
创建时间: 2026-04-14
|
||
最后更新: 2026-04-14 - 初始版本创建
|
||
作者: AI Assistant
|
||
-->
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||
<title>如何做饭 - 美味菜谱</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;600&display=swap" rel="stylesheet">
|
||
<style>
|
||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||
|
||
:root{
|
||
--bg:#1a1614;--card-bg:#faf6f1;--text:#2d2420;--text-light:#6b5d52;
|
||
--accent:#c45c3e;--accent-light:#e8a090;--gold:#d4a853;--cream:#f5ede3;
|
||
--overlay:rgba(26,22,20,0.65);--radius:24px;--shadow:0 25px 80px rgba(0,0,0,.35);
|
||
--font-display:'Noto Serif SC',serif;--font-body:'Noto Sans SC',sans-serif;
|
||
}
|
||
|
||
html,body{height:100%;overflow:hidden;background:var(--bg);font-family:var(--font-body);
|
||
color:var(--text);-webkit-font-smoothing:antialiased}
|
||
|
||
.app{position:relative;width:100%;height:100vh;overflow:hidden}
|
||
|
||
.top-bar{
|
||
position:fixed;top:0;left:0;right:0;z-index:100;
|
||
display:flex;align-items:center;justify-content:space-between;
|
||
padding:16px 24px;padding-top:max(16px,env(safe-area-inset-top));
|
||
background:linear-gradient(to bottom,var(--bg) 40%,transparent);
|
||
transition:opacity .4s ease
|
||
}
|
||
.top-bar.hidden{opacity:0;pointer-events:none}
|
||
.logo{font-family:var(--font-display);font-size:18px;font-weight:700;color:var(--cream);
|
||
letter-spacing:2px;display:flex;align-items:center;gap:8px}
|
||
.logo-icon{width:28px;height:28px;border-radius:8px;background:var(--accent);
|
||
display:flex;align-items:center;justify-content:center;font-size:16px}
|
||
.counter{color:var(--text-light);font-size:13px;font-weight:500;
|
||
background:rgba(255,255,255,.08);padding:6px 14px;border-radius:20px;
|
||
backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.06)}
|
||
|
||
.category-nav{
|
||
position:fixed;top:64px;left:0;right:0;z-index:99;
|
||
display:flex;gap:8px;padding:0 20px;overflow-x:auto;
|
||
scrollbar-width:none;-ms-overflow-style:none;
|
||
transition:transform .4s cubic-bezier(.4,0,.2,1),opacity .4s ease
|
||
}
|
||
.category-nav::-webkit-scrollbar{display:none}
|
||
.category-nav.hidden{transform:translateY(-120%);opacity:0}
|
||
.cat-chip{
|
||
flex-shrink:0;padding:7px 18px;border-radius:20px;font-size:13px;
|
||
font-weight:500;cursor:pointer;white-space:nowrap;transition:all .3s ease;
|
||
background:rgba(255,255,255,.07);color:var(--cream);opacity:.7;
|
||
border:1px solid transparent
|
||
}
|
||
.cat-chip:hover{opacity:1;background:rgba(255,255,255,.12)}
|
||
.cat-chip.active{background:var(--accent);color:#fff;opacity:1;
|
||
border-color:var(--accent-light);box-shadow:0 4px 16px rgba(196,92,62,.35)}
|
||
|
||
.viewport{
|
||
position:relative;width:100%;height:100%;perspective:1400px;
|
||
cursor:grab;cursor:-webkit-grab
|
||
}
|
||
.viewport:active{cursor:grabbing;cursor:-webkit-grabbing}
|
||
|
||
.card-container{
|
||
position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
|
||
padding:100px 20px 40px
|
||
}
|
||
|
||
.dish-card{
|
||
position:relative;width:100%;max-width:520px;height:calc(100vh - 160px);
|
||
max-height:800px;border-radius:var(--radius);overflow:hidden;
|
||
background:var(--card-bg);box-shadow:var(--shadow);
|
||
transform-style:preserve-3d;transition:all .55s cubic-bezier(.4,0,.2,1);
|
||
opacity:0;pointer-events:none
|
||
}
|
||
.dish-card.active{opacity:1;pointer-events:auto;transform:translateZ(0) scale(1)}
|
||
.dish-card.prev{transform:translateX(-110%) rotateY(12deg) scale(.88);opacity:.4}
|
||
.dish-card.next{transform:translateX(110%) rotateY(-12deg) scale(.88);opacity:.4}
|
||
|
||
.card-image-wrap{
|
||
position:relative;width:100%;height:62%;overflow:hidden;background:var(--cream)
|
||
}
|
||
.card-image{
|
||
width:100%;height:100%;object-fit:cover;
|
||
transition:transform .6s cubic-bezier(.4,0,.2,1),filter .4s ease
|
||
}
|
||
.dish-card.active .card-image{transform:scale(1.02)}
|
||
.dish-card.active:hover .card-image{transform:scale(1.06)}
|
||
.image-overlay{
|
||
position:absolute;bottom:0;left:0;right:0;height:50%;
|
||
background:linear-gradient(to top,rgba(0,0,0,.55) 0%,transparent 100%);
|
||
pointer-events:none
|
||
}
|
||
.card-badge{
|
||
position:absolute;top:18px;left:18px;z-index:3;
|
||
padding:6px 14px;border-radius:12px;font-size:11.5px;font-weight:600;
|
||
color:#fff;background:var(--accent);backdrop-filter:blur(12px);
|
||
letter-spacing:.5px;text-transform:uppercase;
|
||
box-shadow:0 4px 14px rgba(196,92,62,.4)
|
||
}
|
||
.fav-btn{
|
||
position:absolute;top:18px;right:18px;z-index:3;width:40px;height:40px;
|
||
border-radius:50%;background:rgba(255,255,255,.9);border:none;
|
||
cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||
font-size:18px;transition:all .3s ease;box-shadow:0 3px 12px rgba(0,0,0,.15)
|
||
}
|
||
.fav-btn:hover{transform:scale(1.12)}
|
||
.fav-btn.favorited{background:var(--accent);color:#fff}
|
||
.fav-btn.favorited svg{fill:#fff}
|
||
|
||
.card-content{padding:28px 28px 24px;height:38%;display:flex;flex-direction:column}
|
||
.card-category{
|
||
display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;
|
||
color:var(--accent);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:10px
|
||
}
|
||
.cat-dot{width:6px;height:6px;border-radius:50%;background:var(--accent)}
|
||
.card-title{
|
||
font-family:var(--font-display);font-size:30px;font-weight:900;line-height:1.25;
|
||
color:var(--text);margin-bottom:12px;letter-spacing:-.5px
|
||
}
|
||
.card-desc{
|
||
font-size:14.5px;line-height:1.75;color:var(--text-light);flex:1;
|
||
overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;line-clamp:3;
|
||
-webkit-box-orient:vertical
|
||
}
|
||
.card-footer{
|
||
display:flex;align-items:center;justify-content:space-between;
|
||
padding-top:16px;border-top:1px solid rgba(0,0,0,.06);margin-top:auto
|
||
}
|
||
.card-meta{display:flex;gap:16px;font-size:12.5px;color:var(--text-light)}
|
||
.meta-item{display:flex;align-items:center;gap:5px}
|
||
.nav-hint{font-size:11px;color:var(--text-light);opacity:.5}
|
||
|
||
.nav-controls{
|
||
position:fixed;bottom:0;left:0;right:0;z-index:100;
|
||
display:flex;align-items:center;justify-content:center;gap:12px;
|
||
padding:16px 24px;padding-bottom:max(16px,env(safe-area-inset-bottom));
|
||
background:linear-gradient(to top,var(--bg) 50%,transparent)
|
||
}
|
||
.nav-btn{
|
||
width:48px;height:48px;border-radius:50%;border:none;background:rgba(255,255,255,.08);
|
||
color:var(--cream);font-size:20px;cursor:pointer;display:flex;
|
||
align-items:center;justify-content:center;transition:all .3s ease;
|
||
backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.06)
|
||
}
|
||
.nav-btn:hover:not(:disabled){background:rgba(255,255,255,.15);transform:scale(1.08)}
|
||
.nav-btn:disabled{opacity:.25;cursor:not-allowed}
|
||
.nav-btn:active:not(:disabled){transform:scale(.94)}
|
||
.progress-track{
|
||
width:120px;height:3px;background:rgba(255,255,255,.12);border-radius:2px;
|
||
overflow:hidden
|
||
}
|
||
.progress-fill{
|
||
height:100%;background:var(--accent);border-radius:2px;
|
||
transition:width .4s cubic-bezier(.4,0,.2,1)
|
||
}
|
||
|
||
.search-overlay{
|
||
position:fixed;inset:0;z-index:200;background:rgba(26,22,20,.95);
|
||
backdrop-filter:blur(20px);display:flex;flex-direction:column;
|
||
align-items:center;justify-content:flex-start;padding-top:120px;
|
||
opacity:0;pointer-events:none;transition:all .4s ease
|
||
}
|
||
.search-overlay.show{opacity:1;pointer-events:auto}
|
||
.search-overlay .search-input-wrap{
|
||
width:90%;max-width:480px;position:relative
|
||
}
|
||
.search-overlay input{
|
||
width:100%;padding:16px 56px 16px 24px;border-radius:16px;border:1px solid rgba(255,255,255,.15);
|
||
background:rgba(255,255,255,.07);color:var(--cream);font-size:17px;
|
||
font-family:var(--font-body);outline:none;transition:border-color .3s
|
||
}
|
||
.search-overlay input:focus{border-color:var(--accent)}
|
||
.search-overlay input::placeholder{color:rgba(255,255,255,.35)}
|
||
.search-close{
|
||
position:absolute;right:10px;top:50%;transform:translateY(-50%);
|
||
width:36px;height:36px;border-radius:50%;border:none;background:transparent;
|
||
color:var(--cream);font-size:18px;cursor:pointer;display:flex;
|
||
align-items:center;justify-content:center
|
||
}
|
||
.search-results{
|
||
width:90%;max-width:480px;margin-top:24px;max-height:50vh;overflow-y:auto
|
||
}
|
||
.search-item{
|
||
display:flex;align-items:center;gap:14px;padding:14px 16px;border-radius:14px;
|
||
cursor:pointer;transition:background .2s;margin-bottom:4px
|
||
}
|
||
.search-item:hover{background:rgba(255,255,255,.07)}
|
||
.search-item img{width:48px;height:48px;border-radius:10px;object-fit:cover}
|
||
.search-item-info h4{color:var(--cream);font-size:15px;font-weight:500}
|
||
.search-item-info span{color:rgba(255,255,255,.45);font-size:12px}
|
||
|
||
.search-btn{
|
||
width:40px;height:40px;border-radius:50%;border:none;background:transparent;
|
||
color:var(--cream);font-size:18px;cursor:pointer;display:flex;
|
||
align-items:center;justify-content:center;transition:all .3s ease
|
||
}
|
||
.search-btn:hover{background:rgba(255,255,255,.1);border-radius:50%}
|
||
|
||
.grid-toggle{
|
||
position:fixed;bottom:90px;right:24px;z-index:98;
|
||
width:46px;height:46px;border-radius:14px;border:none;
|
||
background:rgba(255,255,255,.09);color:var(--cream);
|
||
font-size:19px;cursor:pointer;display:flex;align-items:center;
|
||
justify-content:center;transition:all .3s ease;backdrop-filter:blur(10px);
|
||
border:1px solid rgba(255,255,255,.06)
|
||
}
|
||
.grid-toggle:hover{background:rgba(255,255,255,.15);transform:scale(1.06)}
|
||
|
||
.grid-view{
|
||
position:fixed;inset:0;z-index:150;background:var(--bg);
|
||
overflow-y:auto;padding:90px 16px 40px;
|
||
opacity:0;pointer-events:none;transition:opacity .4s ease
|
||
}
|
||
.grid-view.show{opacity:1;pointer-events:auto}
|
||
.grid-header{text-align:center;margin-bottom:32px}
|
||
.grid-header h2{
|
||
font-family:var(--font-display);font-size:28px;font-weight:700;color:var(--cream)
|
||
}
|
||
.grid-header p{color:var(--text-light);font-size:14px;margin-top:6px;opacity:.6}
|
||
.grid-cats{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;margin-bottom:28px}
|
||
.grid-cat{
|
||
padding:6px 16px;border-radius:20px;font-size:13px;font-weight:500;
|
||
cursor:pointer;background:rgba(255,255,255,.07);color:var(--cream);
|
||
opacity:.7;transition:all .3s;border:1px solid transparent
|
||
}
|
||
.grid-cat:hover{opacity:1}
|
||
.grid-cat.active{background:var(--accent);color:#fff;opacity:1}
|
||
.grid-grid{
|
||
display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:14px;
|
||
max-width:900px;margin:0 auto
|
||
}
|
||
.grid-item{
|
||
border-radius:16px;overflow:hidden;background:var(--card-bg);
|
||
cursor:pointer;transition:all .3s ease;aspect-ratio:3/4
|
||
}
|
||
.grid-item:hover{transform:translateY(-4px);box-shadow:0 12px 32px rgba(0,0,0,.25)}
|
||
.grid-item img{width:100%;height:70%;object-fit:cover}
|
||
.grid-item-info{padding:10px 12px}
|
||
.grid-item-info h4{font-size:13.5px;font-weight:600;color:var(--text);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis
|
||
}
|
||
.grid-item-info span{font-size:11px;color:var(--text-light)}
|
||
|
||
.toast{
|
||
position:fixed;top:100px;left:50%;transform:translateX(-50%) translateY(-20px);
|
||
z-index:300;padding:12px 24px;border-radius:14px;background:var(--accent);
|
||
color:#fff;font-size:14px;font-weight:500;opacity:0;pointer-events:none;
|
||
transition:all .4s cubic-bezier(.4,0,.2,1);box-shadow:0 8px 30px rgba(196,92,62,.4)
|
||
}
|
||
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
|
||
|
||
@keyframes fadeInUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}
|
||
.dish-card.active .card-content>*,.dish-card.active .card-badge,.dish-card.active .fav-btn{
|
||
animation:fadeInUp .5s cubic-bezier(.4,0,.2,1) both
|
||
}
|
||
.dish-card.active .card-category{animation-delay:.05s}
|
||
.dish-card.active .card-title{animation-delay:.1s}
|
||
.dish-card.active .card-desc{animation-delay:.15s}
|
||
.dish-card.active .card-footer{animation-delay:.2s}
|
||
.dish-card.active .card-badge{animation-delay:.05s}
|
||
.dish-card.active .fav-btn{animation-delay:.15s}
|
||
|
||
@media(max-width:600px){
|
||
:root{--radius:18px}
|
||
.card-container{padding:80px 12px 30px}
|
||
.dish-card{max-height:760px;border-radius:18px}
|
||
.card-title{font-size:25px}
|
||
.card-content{padding:22px 20px 18px}
|
||
.top-bar{padding:12px 16px}
|
||
.category-nav{top:56px;padding:0 12px}
|
||
.cat-chip{padding:5px 14px;font-size:12px}
|
||
.nav-btn{width:42px;height:42px}
|
||
.grid-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app" id="app">
|
||
<div class="top-bar" id="topBar">
|
||
<div class="logo"><span class="logo-icon">🍳</span><span>如何做饭</span></div>
|
||
<div class="counter" id="counter">1 / 341</div>
|
||
<button class="search-btn" id="searchBtn" title="搜索">🔍</button>
|
||
</div>
|
||
|
||
<div class="category-nav" id="catNav"></div>
|
||
|
||
<div class="viewport" id="viewport">
|
||
<div class="card-container" id="cardContainer"></div>
|
||
</div>
|
||
|
||
<div class="nav-controls">
|
||
<button class="nav-btn" id="prevBtn" title="上一道">‹</button>
|
||
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
|
||
<button class="nav-btn" id="nextBtn" title="下一道">›</button>
|
||
</div>
|
||
|
||
<button class="grid-toggle" id="gridToggle" title="网格视图">⊞</button>
|
||
</div>
|
||
|
||
<div class="search-overlay" id="searchOverlay">
|
||
<div class="search-input-wrap">
|
||
<input type="text" id="searchInput" placeholder="搜索菜谱...">
|
||
<button class="search-close" id="searchClose">✕</button>
|
||
</div>
|
||
<div class="search-results" id="searchResults"></div>
|
||
</div>
|
||
|
||
<div class="grid-view" id="gridView">
|
||
<div class="grid-header">
|
||
<h2>全部菜谱</h2>
|
||
<p>共 341 道美味佳肴</p>
|
||
</div>
|
||
<div class="grid-cats" id="gridCats"></div>
|
||
<div class="grid-grid" id="gridGrid"></div>
|
||
</div>
|
||
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<script>
|
||
let recipes=[],filteredRecipes=[],currentIndex=0,favorites=new Set(),touchStartX=0,activeCategory='all',CATEGORY_MAP={},DESC_MAP={};
|
||
|
||
async function init(){
|
||
try{
|
||
const res=await fetch('recipes.json');
|
||
const d=await res.json();
|
||
recipes=d.recipes||[];
|
||
CATEGORY_MAP=(d._meta&&d._meta.categories)||{};
|
||
DESC_MAP=(d._meta&&d._meta.descriptions)||{};
|
||
}catch(e){console.error('Failed to load recipes:',e);return}
|
||
filteredRecipes=[...recipes];
|
||
renderCategories();
|
||
renderCards();
|
||
bindEvents();
|
||
updateUI();
|
||
}
|
||
|
||
function renderCategories(){
|
||
const nav=document.getElementById('catNav');
|
||
nav.innerHTML='<div class="cat-chip active" data-cat="all">✨ 全部</div>';
|
||
Object.keys(CATEGORY_MAP).forEach(cat=>{
|
||
const c=CATEGORY_MAP[cat];
|
||
const count=recipes.filter(r=>r.category===cat).length;
|
||
nav.innerHTML+=`<div class="cat-chip" data-cat="${cat}">${c.icon} ${c.name} (${count})</div>`;
|
||
});
|
||
nav.querySelectorAll('.cat-chip').forEach(chip=>{
|
||
chip.addEventListener('click',()=>filterByCategory(chip.dataset.cat))
|
||
});
|
||
}
|
||
|
||
function filterByCategory(cat){
|
||
activeCategory=cat;
|
||
document.querySelectorAll('.cat-chip').forEach(c=>c.classList.toggle('active',c.dataset.cat===cat));
|
||
if(cat==='all')filteredRecipes=[...recipes];
|
||
else filteredRecipes=recipes.filter(r=>r.category===cat);
|
||
currentIndex=0;
|
||
renderCards();
|
||
updateUI();
|
||
}
|
||
|
||
function renderCards(){
|
||
const container=document.getElementById('cardContainer');
|
||
container.innerHTML='';
|
||
const total=filteredRecipes.length;
|
||
for(let i=0;i<Math.min(3,total);i++){
|
||
const idx=(currentIndex+i)%total;
|
||
const r=filteredRecipes[idx];
|
||
const pos=i===0?'active':i===1?'next':'prev';
|
||
container.appendChild(createCard(r,pos,idx));
|
||
}
|
||
}
|
||
|
||
function createCard(recipe,pos,index){
|
||
const cat=CATEGORY_MAP[recipe.category]||{};
|
||
const desc=DESC_MAP[recipe.category]||'一道精心制作的美味佳肴,等待你来品尝与发现。';
|
||
const isFav=favorites.has(recipe.name);
|
||
const div=document.createElement('div');
|
||
div.className='dish-card '+pos;
|
||
div.dataset.index=index;
|
||
div.innerHTML=`
|
||
<div class="card-image-wrap">
|
||
<img class="card-image" src="${recipe.image}" alt="${recipe.name}" loading="lazy"
|
||
onerror="this.style.background='linear-gradient(135deg,#f5e6d3,#e8d4bc)'">
|
||
<div class="image-overlay"></div>
|
||
<span class="card-badge">${cat.icon||''} ${cat.name||''}</span>
|
||
<button class="fav-btn ${isFav?'favorited':''}" data-name="${recipe.name}"
|
||
onclick="toggleFavorite('${recipe.name}',this)" title="收藏">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="${isFav?'#fff':'none'}" stroke="${isFav?'#fff':'currentColor'}" stroke-width="2">
|
||
<path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="card-content">
|
||
<div class="card-category"><span class="cat-dot"></span>${cat.name||'菜谱'}</div>
|
||
<h2 class="card-title">${recipe.name}</h2>
|
||
<p class="card-desc">${desc}</p>
|
||
<div class="card-footer">
|
||
<div class="card-meta">
|
||
<span class="meta-item">⏱ 随时享用</span>
|
||
<span class="meta-item">👨🍳 家常做法</span>
|
||
</div>
|
||
<span class="nav-hint">← → 切换</span>
|
||
</div>
|
||
</div>`;
|
||
return div;
|
||
}
|
||
|
||
function updateUI(){
|
||
const total=filteredRecipes.length;
|
||
if(total===0)return;
|
||
document.getElementById('counter').textContent=`${currentIndex+1} / ${total}`;
|
||
document.getElementById('progressFill').style.width=((currentIndex+1)/total*100)+'%';
|
||
document.getElementById('prevBtn').disabled=currentIndex<=0;
|
||
document.getElementById('nextBtn').disabled=currentIndex>=total-1;
|
||
}
|
||
|
||
function go(dir){
|
||
const total=filteredRecipes.length;
|
||
if(total===0)return;
|
||
const newIndex=currentIndex+dir;
|
||
if(newIndex<0||newIndex>=total)return;
|
||
currentIndex=newIndex;
|
||
const cards=document.querySelectorAll('.dish-card');
|
||
cards.forEach(card=>{
|
||
card.classList.remove('active','prev','next');
|
||
const idx=parseInt(card.dataset.index);
|
||
if(idx===currentIndex)card.classList.add('active');
|
||
else if(idx<currentIndex)card.classList.add('prev');
|
||
else card.classList.add('next');
|
||
});
|
||
const container=document.getElementById('cardContainer');
|
||
const lastCard=container.querySelector('[data-index="'+(currentIndex-dir)+'"]');
|
||
if(lastCard)lastCard.remove();
|
||
const nextIdx=(currentIndex+(dir>0?2:-1)+total)%total;
|
||
const r=filteredRecipes[nextIdx];
|
||
const pos=dir>0?'next':'prev';
|
||
container.appendChild(createCard(r,pos,nextIdx));
|
||
updateUI();
|
||
}
|
||
|
||
function toggleFavorite(name,btn){
|
||
if(favorites.has(name)){favorites.delete(name);btn.classList.remove('favorited');
|
||
btn.querySelector('svg').setAttribute('fill','none');btn.querySelector('svg').setAttribute('stroke','currentColor');
|
||
showToast('已取消收藏')}else{favorites.add(name);btn.classList.add('favorited');
|
||
btn.querySelector('svg').setAttribute('fill','#fff');btn.querySelector('svg').setAttribute('stroke','#fff');
|
||
showToast('已收藏:'+name)}
|
||
}
|
||
|
||
function showToast(msg){
|
||
const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');
|
||
setTimeout(()=>t.classList.remove('show'),1800)
|
||
}
|
||
|
||
function openSearch(){document.getElementById('searchOverlay').classList.add('show');
|
||
setTimeout(()=>document.getElementById('searchInput').focus(),100)}
|
||
function closeSearch(){document.getElementById('searchOverlay').classList.remove('show');
|
||
document.getElementById('searchInput').value='';document.getElementById('searchResults').innerHTML=''}
|
||
|
||
function doSearch(q){
|
||
const results=document.getElementById('results');
|
||
const query=q.trim().toLowerCase();
|
||
if(!query){document.getElementById('searchResults').innerHTML='';return}
|
||
const found=recipes.filter(r=>r.name.toLowerCase().includes(query));
|
||
const html=found.slice(0,20).map(r=>`
|
||
<div class="search-item" onclick="jumpToDish('${r.name}')">
|
||
<img src="${r.image}" alt="${r.name}">
|
||
<div class="search-item-info">
|
||
<h4>${r.name}</h4><span>${CATEGORY_MAP[r.category]?.name||''}</span>
|
||
</div>
|
||
</div>`).join('');
|
||
document.getElementById('searchResults').innerHTML=html||
|
||
'<p style="color:rgba(255,255,255,.4);text-align:center;padding:20px">未找到相关菜谱</p>'
|
||
}
|
||
|
||
function jumpToDish(name){
|
||
const idx=filteredRecipes.findIndex(r=>r.name===name);
|
||
if(idx!==-1){closeSearch();if(idx!==currentIndex){
|
||
while(currentIndex<idx)go(1);while(currentIndex>idx)go(-1)}}else{
|
||
closeSearch();
|
||
activeCategory='all';document.querySelectorAll('.cat-chip').forEach(c=>c.classList.toggle('active',c.dataset.cat==='all'));
|
||
filteredRecipes=[...recipes];const ri=recipes.findIndex(r=>r.name===name);
|
||
if(ri!==-1){currentIndex=ri;renderCards();updateUI()}
|
||
}
|
||
}
|
||
|
||
function toggleGridView(){
|
||
const gv=document.getElementById('gridView');gv.classList.toggle('show');
|
||
if(gv.classList.contains('show'))renderGrid()
|
||
}
|
||
|
||
function renderGrid(){
|
||
const cats=document.getElementById('gridCats');
|
||
cats.innerHTML='<div class="grid-cat active" data-gcat="all">✨ 全部</div>';
|
||
Object.keys(CATEGORY_MAP).forEach(cat=>{
|
||
const c=CATEGORY_MAP[cat];
|
||
cats.innerHTML+=`<div class="grid-cat" data-gcat="${cat}">${c.icon} ${c.name}</div>`
|
||
});
|
||
cats.querySelectorAll('.grid-cat').forEach(c=>{
|
||
c.addEventListener('click',()=>{
|
||
cats.querySelectorAll('.grid-cat').forEach(x=>x.classList.remove('active'));
|
||
c.classList.add('active');renderGridItems(c.dataset.gcat)
|
||
})
|
||
});
|
||
renderGridItems('all')
|
||
}
|
||
|
||
function renderGridItems(filterCat){
|
||
const list=filterCat==='all'?recipes:recipes.filter(r=>r.category===filterCat);
|
||
const grid=document.getElementById('gridGrid');
|
||
grid.innerHTML=list.map(r=>`
|
||
<div class="grid-item" onclick="jumpToDish('${r.name}');toggleGridView()">
|
||
<img src="${r.image}" alt="${r.name}" loading="lazy">
|
||
<div class="grid-item-info"><h4>${r.name}</h4><span>${CATEGORY_MAP[r.category]?.name||''}</span></div>
|
||
</div>`).join('')
|
||
}
|
||
|
||
function bindEvents(){
|
||
document.getElementById('prevBtn').addEventListener('click',()=>go(-1));
|
||
document.getElementById('nextBtn').addEventListener('click',()=>go(1));
|
||
document.getElementById('searchBtn').addEventListener('click',openSearch);
|
||
document.getElementById('searchClose').addEventListener('click',closeSearch);
|
||
document.getElementById('searchInput').addEventListener('input',e=>doSearch(e.target.value));
|
||
document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape')closeSearch()});
|
||
document.getElementById('searchOverlay').addEventListener('click',e=>{if(e.target.id==='searchOverlay')closeSearch()});
|
||
document.getElementById('gridToggle').addEventListener('click',toggleGridView);
|
||
|
||
const vp=document.getElementById('viewport');
|
||
let autoHideTop;
|
||
vp.addEventListener('mousemove',()=>{
|
||
document.getElementById('topBar').classList.remove('hidden');
|
||
document.getElementById('catNav').classList.remove('hidden');
|
||
clearTimeout(autoHideTop);
|
||
autoHideTop=setTimeout(()=>{
|
||
document.getElementById('topBar').classList.add('hidden');
|
||
document.getElementById('catNav').classList.add('hidden')
|
||
},3000)
|
||
});
|
||
|
||
vp.addEventListener('touchstart',e=>{touchStartX=e.touches[0].clientX},{passive:true});
|
||
vp.addEventListener('touchend',e=>{
|
||
const diff=e.changedTouches[0].clientX-touchStartX;
|
||
if(Math.abs(diff)>60)go(diff<0?1:-1)
|
||
},{passive:true});
|
||
|
||
document.addEventListener('keydown',e=>{
|
||
if(document.getElementById('searchOverlay').classList.contains('show'))return;
|
||
if(document.getElementById('gridView').classList.contains('show'))return;
|
||
if(e.key==='ArrowLeft'||e.key==='ArrowUp'){e.preventDefault();go(-1)}
|
||
if(e.key==='ArrowRight'||e.key==='ArrowDown'){e.preventDefault();go(1)}
|
||
if(e.key===' '||e.key==='Enter'){
|
||
e.preventDefault();
|
||
const fav=document.querySelector('.dish-card.active .fav-btn');
|
||
if(fav)fav.click()}
|
||
});
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|