<!DOCTYPE html>
<html lang="zh-HK">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>今餐食乜餸 - 精準食譜版</title>
<style>
body {
margin: 0;
background: #f5f7fb;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans", Arial, sans-serif;
color: #111827;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 24px 16px 40px; }
.card {
background: #fff; border: 1px solid #e5e7eb; border-radius: 20px;
box-shadow: 0 10px 24px rgba(0,0,0,0.05); padding: 20px;
}
h1 { margin: 0 0 8px; font-size: 1.8rem; }
.sub { margin: 0 0 18px; color: #4b5563; line-height: 1.5; }
.controls { display: grid; grid-template-columns: 1fr 1fr auto; gap: 12px; align-items: end; }
label { display: block; font-weight: 700; margin-bottom: 6px; }
select, button { border-radius: 12px; min-height: 48px; font-size: 1rem; }
select { width: 100%; border: 1px solid #d1d5db; padding: 12px 14px; background: #fff; }
.actions { display: flex; gap: 8px; }
.btn { border: none; padding: 12px 16px; font-weight: 700; cursor: pointer; transition: 0.2s; }
.btn:hover { opacity: 0.9; }
.btn-primary { background: #111827; color: #fff; }
.btn-secondary { background: #f3f4f6; color: #111827; }
.status { margin-top: 14px; min-height: 1.4em; color: #374151; }
.panel { margin-top: 16px; border: 1px solid #e5e7eb; border-radius: 16px; padding: 16px; background: #fafafa; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #eef2ff; color: #3730a3; font-size: .85rem; font-weight: 700; margin-right: 8px; }
.badge-discount { background: #fef2f2; color: #991b1b; }
h2 { margin: 12px 0 10px; font-size: 1.15rem; }
ul.menu { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
ul.menu li { padding: 14px 16px; border-radius: 12px; border: 1px solid rgba(0,0,0,0.05); display: flex; flex-direction: column; gap: 10px; }
.dish-header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.links { display: flex; flex-wrap: wrap; gap: 8px; }
button.link-btn, a.link {
display: inline-block; text-decoration: none; padding: 8px 12px;
border-radius: 8px; font-size: .9rem; font-weight: 600; cursor: pointer; border: 1px solid transparent;
}
button.link-btn { background: #fff; color: #0f766e; border-color: #a5f3fc; }
a.link-yt { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
a.link-shop { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
/* 食譜展開內容 */
.recipe-content {
display: none; background: #fff; border-radius: 8px; padding: 16px;
margin-top: 8px; border: 1px solid #e5e7eb; font-size: 0.95rem;
}
.recipe-content.active { display: block; animation: fadeIn 0.3s ease; }
.recipe-content h4 { margin: 0 0 8px; color: #111827; font-size: 1rem; }
.recipe-content ul, .recipe-content ol { margin: 0 0 12px; padding-left: 20px; color: #4b5563; }
.recipe-content li { margin-bottom: 4px; }
.section { margin-top: 16px; padding-top: 14px; border-top: 1px solid #e5e7eb; }
.section h3 { margin: 0 0 10px; font-size: 1rem; }
.pills { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.pill { display: inline-block; padding: 4px 10px; border-radius: 999px; background: #f3f4f6; font-size: .85rem; }
.pill-discount { background: #fee2e2; color: #991b1b; font-weight: bold; }
.note { margin-top: 10px; color: #6b7280; font-size: .92rem; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 720px) {
.controls { grid-template-columns: 1fr; }
.actions { flex-direction: column; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="card">
<h1>今餐食乜餸</h1>
<p class="sub">內置精準匹配嘅「食材與步驟」食譜,確保同教學影片一致。附帶買餸指南,保證每味餸隔 10 次先會再出現。</p>
<div class="controls">
<div>
<label for="city">你而家喺邊度?</label>
<select id="city"></select>
</div>
<div>
<label for="mealType">想食咩類型?</label>
<select id="mealType">
<option value="dinner">三餸一湯 (住家飯)</option>
<option value="tea">港式茶餐 (粉麵/快餐)</option>
</select>
</div>
<div class="actions">
<button class="btn btn-primary" id="generate">幫我諗食乜</button>
<button class="btn btn-secondary" id="reset">清除紀錄</button>
</div>
</div>
<div class="status" id="status"></div>
<div id="result"></div>
</div>
</div>
<script>
// 切換顯示食譜內容
function toggleRecipe(id) {
const el = document.getElementById(id);
if (el.classList.contains('active')) {
el.classList.remove('active');
} else {
el.classList.add('active');
}
}
(function () {
const STORAGE_KEY = 'jcsm_history_v4';
const MAX_HISTORY = 10;
const pastelColors = ['#fef2f2', '#fff7ed', '#f0fdf4', '#eff6ff', '#fdf2f8', '#f5f3ff', '#ecfeff', '#fefce8'];
const ingredientCatalog = {
eggs: { label: '雞蛋' }, chicken: { label: '雞肉' }, pork: { label: '豬肉' },
beef: { label: '牛肉' }, fish: { label: '魚類' }, shrimp: { label: '蝦' },
clam: { label: '蜆' }, tofu: { label: '豆腐' }, leafy_greens: { label: '葉菜' },
tomato: { label: '番茄' }, potato: { label: '薯仔' }, eggplant: { label: '茄子' },
cucumber: { label: '青瓜' }, mushroom: { label: '菇類' }, broccoli: { label: '西蘭花' },
corn: { label: '粟米' }, noodles: { label: '粉麵' }, macaroni: { label: '通粉' },
luncheon_meat: { label: '午餐肉' }, satay: { label: '沙嗲醬' }, ginger: { label: '薑' },
onion: { label: '洋蔥' }
};
const cities = {
hong_kong: {
label: 'Hong Kong 香港',
shops: [{name: 'HKTVmall', url: 'https://www.hktvmall.com/'}, {name: '百佳', url: 'https://www.pns.hk/'}]
},
london: {
label: 'London 倫敦',
shops: [{name: '龍鳳行', url: 'https://www.loonfung.com/'}, {name: 'Tesco', url: 'https://www.tesco.com/'}]
},
vancouver: {
label: 'Vancouver 溫哥華',
shops: [{name: '大統華', url: 'https://www.tntsupermarket.com/'}, {name: 'Walmart', url: 'https://www.walmart.ca/'}]
},
sydney: {
label: 'Sydney 悉尼',
shops: [{name: '通利', url: 'https://tongli.com.au/'}, {name: 'Woolworths', url: 'https://www.woolworths.com.au/'}]
}
};
// 內置精準食譜資料庫
const dishes = [
{
id:'tomato_egg', type:'main', name:'番茄炒蛋', required:['eggs','tomato'],
recipe: {
ingredients: ['番茄 2個 (切塊)', '雞蛋 3隻 (打勻)', '蔥花 少許', '糖 1茶匙', '鹽 少許', '茄汁 1湯匙'],
steps: ['燒熱油鑊,將雞蛋炒至半熟,盛起備用。', '原鑊加少許油,爆香番茄塊。', '加入糖、鹽、茄汁及少許水,煮至番茄軟身出汁。', '將雞蛋回鑊,與番茄炒勻,撒上蔥花即成。']
}
},
{
id:'chicken_potato_onion', type:'main', name:'洋蔥薯仔炆雞', required:['chicken','potato','onion'],
recipe: {
ingredients: ['雞件 半隻', '薯仔 2個 (切塊)', '洋蔥 1個 (切塊)', '生抽、老抽、糖、生粉 (醃料)'],
steps: ['雞件用醃料醃20分鐘。', '燒熱油,先將薯仔煎至表面微黃,盛起。', '爆香洋蔥,加入雞件炒至表面變色。', '加入薯仔及適量清水,加蓋中火炆15分鐘至汁濃即成。']
}
},
{
id:'broccoli_beef', type:'main', name:'西蘭花炒牛肉', required:['beef','broccoli'],
recipe: {
ingredients: ['牛肉 200g (切片)', '西蘭花 1個 (切小朵)', '蒜蓉 1湯匙', '蠔油 1湯匙'],
steps: ['牛肉用生抽、糖、生粉醃15分鐘。', '西蘭花放入滾水灼2分鐘,撈起瀝乾。', '燒熱油鑊,爆香蒜蓉,放入牛肉快炒至七成熟。', '加入西蘭花及蠔油炒勻,埋芡即成。']
}
},
{
id:'steamed_pork_patty', type:'main', name:'梅菜蒸肉餅', required:['pork'],
recipe: {
ingredients: ['免治豬肉 300g', '甜梅菜 1棵', '生抽 1茶匙', '糖 半茶匙', '生粉 1茶匙', '水 2湯匙'],
steps: ['梅菜浸洗乾淨,擠乾水份後切碎。', '免治豬肉加入醃料及水,順同一方向攪拌至起膠。', '拌入梅菜碎,平鋪在蒸碟上。', '水滾後大火蒸12-15分鐘至熟透。']
}
},
{
id:'steamed_fish', type:'main', name:'清蒸石斑', required:['fish','ginger'],
recipe: {
ingredients: ['石斑魚 1條', '薑絲 適量', '蔥絲 適量', '蒸魚豉油 2湯匙', '熟油 2湯匙'],
steps: ['魚洗淨抹乾,碟底鋪少許薑絲,放上魚,魚面再放薑絲。', '水滾後隔水大火蒸8-10分鐘。', '倒去碟內多餘水份,鋪上蔥絲。', '燒熱2湯匙油淋在蔥絲上,最後淋上蒸魚豉油即成。']
}
},
{
id:'tomato_potato_soup', type:'soup', name:'番茄薯仔排骨湯', required:['tomato','potato','pork'],
recipe: {
ingredients: ['番茄 3個', '薯仔 2個', '排骨 300g', '薑 2片', '鹽 適量'],
steps: ['排骨飛水洗淨。番茄及薯仔切塊。', '鍋中加入適量清水及薑片,放入排骨大火煲滾。', '轉中小火煲30分鐘,加入番茄及薯仔。', '繼續煲45分鐘,最後加鹽調味即成。']
}
},
{
id:'satay_beef_noodles', type:'tea', name:'沙嗲牛肉麵', required:['beef','noodles','satay'],
recipe: {
ingredients: ['牛肉 150g', '即食麵 1包', '沙嗲醬 2湯匙', '花生醬 1茶匙', '生抽、糖 (醃料)'],
steps: ['牛肉醃15分鐘。', '燒熱油,爆香沙嗲醬及花生醬,加入牛肉炒熟,加少許水煮成濃汁。', '另起鍋煮熟即食麵及湯底。', '將沙嗲牛肉連汁鋪在麵上即成。']
}
}
];
// 補全剩餘食譜(簡單版)以防報錯
const allDishes = dishes.concat([
{ id:'blackbean_clam', type:'main', name:'豉椒炒蜆', required:['clam'], recipe: { ingredients:['蜆 1斤','豆豉 1湯匙','辣椒 適量'], steps:['蜆吐沙飛水','爆香豆豉辣椒','落蜆炒勻'] } },
{ id:'ketchup_shrimp', type:'main', name:'茄汁蝦碌', required:['shrimp','tomato'], recipe: { ingredients:['蝦 半斤','茄汁 3湯匙'], steps:['蝦剪鬚煎香','落茄汁煮勻'] } },
{ id:'cold_eggplant', type:'main', name:'涼拌茄子', required:['eggplant'], recipe: { ingredients:['茄子 2條','蒜蓉醋汁'], steps:['茄子蒸熟撕條','淋上醬汁'] } },
{ id:'garlic_cucumber', type:'main', name:'手拍青瓜', required:['cucumber'], recipe: { ingredients:['青瓜 2條','蒜蓉、醋、麻油'], steps:['青瓜拍碎切塊','加入調味料拌勻'] } },
{ id:'corn_pork_soup', type:'soup', name:'粟米排骨湯', required:['corn','pork'], recipe: { ingredients:['粟米 2條','排骨 300g'], steps:['排骨飛水','全部材料煲1.5小時'] } },
{ id:'tomato_beef_macaroni', type:'tea', name:'番茄牛肉通粉', required:['beef','tomato','macaroni'], recipe: { ingredients:['牛肉 100g','番茄 2個','通粉 1碗'], steps:['煮熟通粉','番茄煮成湯底','灼熟牛肉放上面'] } },
{ id:'luncheon_egg_noodles', type:'tea', name:'餐肉煎蛋麵', required:['luncheon_meat','eggs','noodles'], recipe: { ingredients:['午餐肉 2片','雞蛋 1隻','即食麵 1包'], steps:['煎香餐肉及蛋','煮熟麵條放上面'] } },
{ id:'soy_sauce_noodles', type:'tea', name:'豉油皇炒麵', required:['noodles','onion'], recipe: { ingredients:['炒麵 1個','洋蔥絲','生抽、老抽'], steps:['麵條飛水瀝乾','爆香洋蔥,加入麵條及豉油快炒'] } }
]);
function getHistory() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch (e) { return []; }
}
function saveHistory(dishIds) {
try {
const hist = getHistory();
hist.unshift({ ts: Date.now(), dish_ids: dishIds });
localStorage.setItem(STORAGE_KEY, JSON.stringify(hist.slice(0, MAX_HISTORY)));
} catch (e) {}
}
function getUsedDishIds() {
const hist = getHistory();
const used = new Set();
hist.forEach(entry => { (entry.dish_ids || []).forEach(id => used.add(id)); });
return used;
}
function getDiscountedIngredients() {
const keys = Object.keys(ingredientCatalog);
return keys.sort(() => 0.5 - Math.random()).slice(0, 2);
}
function scoreDish(dish, discounts) {
let score = 10;
let hasDiscount = false;
dish.required.forEach(req => {
if (discounts.includes(req)) { score += 20; hasDiscount = true; }
});
score += Math.random() * 5;
return { score, hasDiscount };
}
const selectCity = document.getElementById('city');
const selectMealType = document.getElementById('mealType');
const result = document.getElementById('result');
(function buildCityOptions() {
Object.entries(cities).forEach(([key, city]) => {
const op = document.createElement('option');
op.value = key; op.textContent = city.label;
selectCity.appendChild(op);
});
})();
function render() {
const mealType = selectMealType.value;
const cityKey = selectCity.value;
const currentCity = cities[cityKey];
const usedIds = getUsedDishIds();
const discounts = getDiscountedIngredients();
let availableDishes = allDishes.filter(d => !usedIds.has(d.id));
if (availableDishes.length < 5) {
localStorage.removeItem(STORAGE_KEY);
availableDishes = allDishes;
}
let selectedDishes = [];
let soup = null;
if (mealType === 'tea') {
let teaOptions = availableDishes.filter(d => d.type === 'tea');
if(teaOptions.length === 0) teaOptions = allDishes.filter(d => d.type === 'tea');
teaOptions = teaOptions.map(d => ({ ...d, ...scoreDish(d, discounts) })).sort((a,b) => b.score - a.score);
selectedDishes = [teaOptions[0]];
} else {
let mains = availableDishes.filter(d => d.type === 'main');
let soups = availableDishes.filter(d => d.type === 'soup');
if(mains.length < 3) mains = allDishes.filter(d => d.type === 'main');
if(soups.length < 1) soups = allDishes.filter(d => d.type === 'soup');
mains = mains.map(d => ({ ...d, ...scoreDish(d, discounts) })).sort((a,b) => b.score - a.score);
soups = soups.map(d => ({ ...d, ...scoreDish(d, discounts) })).sort((a,b) => b.score - a.score);
selectedDishes = mains.slice(0, 3);
soup = soups[0];
}
const finalIds = selectedDishes.map(d => d.id);
if (soup) finalIds.push(soup.id);
saveHistory(finalIds);
const colors = [...pastelColors].sort(() => Math.random() - 0.5);
const discountLabels = discounts.map(k => ingredientCatalog[k].label).join('、');
const generateDishHtml = (dish, idx, isSoup = false) => {
const ytUrl = `https://www.youtube.com/results?search_query=${encodeURIComponent(dish.name + ' 食譜 教學')}`;
const recipeId = `recipe-${dish.id}-${Date.now()}`;
const ingredientsHtml = dish.recipe.ingredients.map(i => `<li>${i}</li>`).join('');
const stepsHtml = dish.recipe.steps.map(s => `<li>${s}</li>`).join('');
return `
<li style="background-color: ${colors[idx % colors.length]}">
<div class="dish-header">
<strong>${isSoup ? '湯:' : (idx + 1 + '. ')}${dish.name}</strong>
${dish.hasDiscount ? `<span class="pill pill-discount">用咗特價食材</span>` : ''}
</div>
<div class="links">
<button class="link-btn" onclick="toggleRecipe('${recipeId}')">📝 睇食譜 (食材及步驟)</button>
<a href="${ytUrl}" target="_blank" class="link link-yt">📺 睇教學影片</a>
</div>
<div id="${recipeId}" class="recipe-content">
<h4>🛒 準備食材:</h4>
<ul>${ingredientsHtml}</ul>
<h4>🍳 烹飪步驟:</h4>
<ol>${stepsHtml}</ol>
</div>
</li>
`;
};
let menuHtml = selectedDishes.map((dish, idx) => generateDishHtml(dish, idx)).join('');
if (soup) { menuHtml += generateDishHtml(soup, 3, true); }
const allReqs = new Set();
[...selectedDishes, ...(soup ? [soup] : [])].forEach(d => {
d.required.forEach(r => allReqs.add(r));
});
const shopHtml = Array.from(allReqs).map(k => {
const isDiscount = discounts.includes(k);
return `<span class="pill ${isDiscount ? 'pill-discount' : ''}">${ingredientCatalog[k].label} ${isDiscount ? '(特價)' : ''}</span>`;
}).join('');
const storeLinksHtml = currentCity.shops.map(shop =>
`<a href="${shop.url}" target="_blank" class="link link-shop">🛒 去 ${shop.name} 買</a>`
).join('');
result.innerHTML = `
<div class="panel">
<div class="badge">${mealType === 'tea' ? '港式茶餐' : '三餸一湯'}</div>
<div class="badge badge-discount">🛒 模擬今日特價:${discountLabels}</div>
<h2>今餐食乜餸</h2>
<ul class="menu">${menuHtml}</ul>
<div class="section">
<h3>買餸清單 (${currentCity.label})</h3>
<div class="pills" style="margin-bottom: 12px;">${shopHtml}</div>
<div class="links">${storeLinksHtml}</div>
</div>
<div class="note">已嚴格過濾,保證以上菜式喺最近 10 次都未出現過。</div>
</div>
`;
document.getElementById('status').textContent = '已生成新一餐建議!';
}
document.getElementById('generate').addEventListener('click', render);
document.getElementById('reset').addEventListener('click', () => {
localStorage.removeItem(STORAGE_KEY);
document.getElementById('status').textContent = '已清除紀錄。';
});
render();
})();
</script>
</body>
</html>