Files
kitchen/docs/api/eat.html
Developer 8d27c67d3a api实现
2026-04-09 08:54:36 +08:00

1586 lines
61 KiB
HTML
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.
<!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>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #007AFF;
--bg-color: #F2F2F7;
--card-bg: #FFFFFF;
--text-primary: #1C1C1E;
--text-secondary: #8E8E93;
--border-radius: 16px;
--shadow: 0 2px 20px rgba(0, 0, 0, 0.08);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif;
background: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 480px;
margin: 0 auto;
}
.header {
text-align: center;
padding: 30px 0;
}
.header h1 {
font-size: 28px;
font-weight: 700;
margin-bottom: 8px;
}
.header p {
color: var(--text-secondary);
font-size: 14px;
}
.mode-selector {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.mode-btn {
flex: 1;
padding: 14px;
border: none;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
background: var(--card-bg);
color: var(--text-primary);
}
.mode-btn.active {
background: var(--primary-color);
color: white;
}
.filter-panel {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 20px;
margin-bottom: 20px;
display: none;
}
.filter-panel.show {
display: block;
}
.filter-section {
margin-bottom: 16px;
}
.filter-section:last-child {
margin-bottom: 0;
}
.filter-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 8px;
display: block;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.filter-header .filter-label {
margin-bottom: 0;
}
.refresh-btn {
background: var(--bg-color);
border: none;
border-radius: 8px;
padding: 4px 10px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.refresh-btn:hover {
background: var(--primary-color);
transform: rotate(180deg);
}
.refresh-btn:active {
transform: rotate(360deg);
}
.filter-tag.expandable {
background: linear-gradient(135deg, var(--primary-color), #5856D6);
color: white;
}
.filter-tag.expandable.active {
box-shadow: 0 0 0 2px var(--primary-color);
}
.subcategory-container {
margin-top: 12px;
padding: 12px;
background: var(--bg-color);
border-radius: 12px;
border: 1px solid rgba(0, 122, 255, 0.2);
}
.subcategory-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.subcategory-title {
font-size: 13px;
font-weight: 600;
color: var(--primary-color);
}
.subcategory-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.subcategory-tag {
padding: 6px 12px;
border-radius: 16px;
font-size: 12px;
background: var(--card-bg);
border: 1px solid var(--primary-color);
color: var(--primary-color);
cursor: pointer;
transition: all 0.2s ease;
}
.subcategory-tag:hover {
background: var(--primary-color);
color: white;
}
.subcategory-tag.selected {
background: var(--primary-color);
color: white;
}
.filter-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.filter-tag {
padding: 8px 14px;
border-radius: 20px;
font-size: 13px;
background: var(--bg-color);
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.filter-tag.selected {
background: rgba(0, 122, 255, 0.1);
color: var(--primary-color);
border-color: var(--primary-color);
}
.wheel-container {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 40px 20px;
text-align: center;
margin-bottom: 20px;
box-shadow: var(--shadow);
}
.wheel {
width: 200px;
height: 200px;
margin: 0 auto 30px;
border-radius: 50%;
background: linear-gradient(135deg, #FF6B6B, #FF8E53, #FFCD56, #4BC0C0, #36A2EB, #9966FF);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: transform 0.1s ease;
}
.wheel-inner {
width: 160px;
height: 160px;
background: var(--card-bg);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 60px;
}
.wheel.spinning {
animation: spin 0.1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.start-btn {
padding: 16px 48px;
border: none;
border-radius: 25px;
font-size: 17px;
font-weight: 600;
background: var(--primary-color);
color: white;
cursor: pointer;
transition: all 0.3s ease;
}
.start-btn:active {
transform: scale(0.95);
}
.start-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.speed-selector {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 20px;
}
.speed-btn {
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 13px;
background: var(--bg-color);
cursor: pointer;
transition: all 0.2s ease;
}
.speed-btn.active {
background: var(--primary-color);
color: white;
}
.result-card {
background: var(--card-bg);
border-radius: var(--border-radius);
overflow: hidden;
margin-bottom: 20px;
box-shadow: var(--shadow);
display: none;
}
.result-card.show {
display: block;
animation: slideUp 0.5s ease;
}
.match-info {
display: flex;
justify-content: center;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, #E8F5E9, #E3F2FD);
border-bottom: 1px solid rgba(0, 122, 255, 0.1);
}
.match-item {
display: flex;
align-items: center;
gap: 6px;
}
.match-icon {
font-size: 16px;
}
.match-label {
font-size: 13px;
color: var(--text-secondary);
}
.match-value {
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
}
.match-unit {
font-size: 12px;
color: var(--text-secondary);
}
.match-divider {
width: 1px;
height: 24px;
background: rgba(0, 122, 255, 0.2);
margin: 0 20px;
}
.candidates-container {
padding: 16px;
background: var(--bg-color);
}
.candidates-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.candidate-card {
background: var(--card-bg);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.2s ease;
}
.candidate-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.15);
}
.candidate-cover {
width: 100%;
height: 100px;
object-fit: cover;
background: var(--bg-color);
}
.candidate-info {
padding: 10px;
}
.candidate-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
}
.candidate-meta {
font-size: 11px;
color: var(--text-secondary);
}
.candidates-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.refresh-candidates-btn {
padding: 10px 20px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.refresh-candidates-btn:hover {
background: #0056CC;
transform: scale(1.02);
}
.candidates-hint {
font-size: 12px;
color: var(--text-secondary);
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-cover {
width: 100%;
height: 200px;
object-fit: cover;
background: var(--bg-color);
}
.result-content {
padding: 20px;
}
.result-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.result-intro {
color: var(--text-secondary);
font-size: 14px;
line-height: 1.5;
margin-bottom: 16px;
}
.result-meta {
display: flex;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.meta-tag {
padding: 6px 12px;
background: var(--bg-color);
border-radius: 8px;
font-size: 13px;
}
.result-section {
margin-bottom: 16px;
}
.result-section h3 {
font-size: 15px;
font-weight: 600;
margin-bottom: 10px;
}
.ingredient-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.ingredient-item {
padding: 10px;
background: var(--bg-color);
border-radius: 8px;
font-size: 13px;
}
.ingredient-item.has-detail {
cursor: pointer;
transition: all 0.2s ease;
}
.ingredient-item.has-detail:hover {
background: rgba(0, 122, 255, 0.1);
}
.ingredient-item .expand-icon {
float: right;
color: var(--primary-color);
font-size: 12px;
transition: transform 0.2s ease;
}
.ingredient-item.expanded .expand-icon {
transform: rotate(180deg);
}
.ingredient-detail {
margin-top: 8px;
padding: 12px;
background: white;
border-radius: 8px;
border: 1px solid rgba(0, 122, 255, 0.1);
font-size: 12px;
line-height: 1.6;
}
.detail-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-row {
padding: 4px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
font-weight: 600;
color: var(--text-secondary);
}
.ingredient-item .name {
font-weight: 600;
}
.ingredient-item .amount {
color: var(--text-secondary);
margin-left: 4px;
}
.nutrition-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.nutrition-item {
text-align: center;
padding: 10px;
background: var(--bg-color);
border-radius: 8px;
}
.nutrition-item .value {
font-size: 18px;
font-weight: 700;
color: var(--primary-color);
}
.nutrition-item .label {
font-size: 11px;
color: var(--text-secondary);
margin-top: 4px;
}
.result-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 16px;
padding: 16px;
background: var(--bg-color);
border-radius: 12px;
}
.stat-item {
text-align: center;
}
.stat-item .icon {
font-size: 24px;
}
.stat-item .value {
font-size: 18px;
font-weight: 700;
}
.stat-item .label {
font-size: 12px;
color: var(--text-secondary);
}
.result-actions {
display: flex;
gap: 8px;
margin-top: 20px;
flex-wrap: wrap;
}
.action-btn {
flex: 1;
min-width: 80px;
padding: 14px;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.action-btn.primary {
background: var(--primary-color);
color: white;
}
.action-btn.secondary {
background: var(--bg-color);
color: var(--text-primary);
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.loading.show {
display: block;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--bg-color);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎯 今天吃什么?</h1>
<p>让命运来决定你的下一餐</p>
</div>
<div class="mode-selector">
<button class="mode-btn active" data-mode="random">🎲 完全随机</button>
<button class="mode-btn" data-mode="smart">🎯 智能推荐</button>
</div>
<div class="filter-panel" id="filterPanel">
<div class="filter-info" id="recipeCount" style="display: none; padding: 12px; background: rgba(0, 122, 255, 0.1); border-radius: 8px; margin-bottom: 12px; text-align: center; font-weight: 600; color: var(--primary-color);">
符合条件: 0 个菜谱
</div>
<div class="filter-section">
<div class="filter-header">
<label class="filter-label">屏蔽过敏原</label>
<button class="refresh-btn" data-type="allergen" title="刷新">🔄</button>
</div>
<div class="filter-tags" id="allergenTags"></div>
</div>
<div class="filter-section">
<div class="filter-header">
<label class="filter-label">需要的分类</label>
<button class="refresh-btn" data-type="category" title="刷新">🔄</button>
</div>
<div class="filter-tags" id="categoryTags"></div>
</div>
<div class="filter-section">
<div class="filter-header">
<label class="filter-label">👅 口味</label>
<button class="refresh-btn" data-type="taste" title="刷新">🔄</button>
</div>
<div class="filter-tags" id="tasteTags"></div>
</div>
<div class="filter-section">
<div class="filter-header">
<label class="filter-label">👨‍🍳 工艺</label>
<button class="refresh-btn" data-type="craft" title="刷新">🔄</button>
</div>
<div class="filter-tags" id="craftTags"></div>
</div>
</div>
<div class="wheel-container">
<div class="wheel" id="wheel">
<div class="wheel-inner">🍽️</div>
</div>
<button class="start-btn" id="startBtn">开始选择</button>
<div class="speed-selector">
<button class="speed-btn" data-speed="fast"></button>
<button class="speed-btn active" data-speed="medium"></button>
<button class="speed-btn" data-speed="slow"></button>
<button class="speed-btn" data-speed="skip">跳过</button>
</div>
</div>
<div class="loading" id="loading">
<div class="loading-spinner"></div>
<p>正在为你选择...</p>
</div>
<div class="result-card" id="resultCard">
<div class="match-info" id="matchInfo" style="display: none;">
<div class="match-item">
<span class="match-icon">🎯</span>
<span class="match-label">符合条件</span>
<span class="match-value" id="candidatesCount">0</span>
<span class="match-unit"></span>
</div>
<div class="match-divider"></div>
<div class="match-item">
<span class="match-icon"></span>
<span class="match-label">最佳匹配</span>
<span class="match-value" id="bestMatchCount">0</span>
<span class="match-unit"></span>
</div>
</div>
<img class="result-cover" id="resultCover" src="" alt="">
<div class="result-content">
<h2 class="result-title" id="resultTitle"></h2>
<p class="result-intro" id="resultIntro"></p>
<div class="result-meta" id="resultMeta"></div>
<div class="result-section">
<h3>🥬 食材清单</h3>
<div class="ingredient-list" id="ingredientList"></div>
</div>
<div class="result-section" id="nutritionSection" style="display:none;">
<h3>📊 营养成分</h3>
<div class="nutrition-grid" id="nutritionGrid"></div>
</div>
<div class="result-stats" id="resultStats">
<div class="stat-item">
<div class="icon">👁️</div>
<div class="value" id="statViews">0</div>
<div class="label">浏览</div>
</div>
<div class="stat-item">
<div class="icon">❤️</div>
<div class="value" id="statLikes">0</div>
<div class="label">点赞</div>
</div>
<div class="stat-item">
<div class="icon"></div>
<div class="value" id="statRecommends">0</div>
<div class="label">推荐</div>
</div>
</div>
<div class="result-actions">
<button class="action-btn secondary" id="likeBtn">❤️ 点赞</button>
<button class="action-btn secondary" id="recommendBtn">⭐ 推荐</button>
<button class="action-btn secondary" id="retryBtn">🔄 再来</button>
<button class="action-btn primary" id="detailBtn">📖 详情</button>
</div>
</div>
<div class="candidates-container" id="candidatesContainer" style="display: none;"></div>
</div>
</div>
<script>
let configData = null;
let currentMode = 'random';
let currentSpeed = 'medium';
let selectedFilters = {
allergens: [],
categories: [],
tags: []
};
let currentCandidates = [];
let candidatesCount = 0;
let bestMatchCount = 0;
const API_BASE = '';
document.addEventListener('DOMContentLoaded', function() {
loadConfig();
bindEvents();
});
async function loadConfig() {
try {
const response = await fetch(API_BASE + 'api_what_to_eat.php?act=config');
const data = await response.json();
console.log('配置数据:', data);
if (data.code === 200) {
configData = data.data;
renderFilters();
} else {
console.error('配置加载失败:', data.message);
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
function renderFilters() {
if (!configData) {
console.warn('configData 为空');
return;
}
console.log('渲染过滤器, categories:', configData.categories);
renderAllergens();
renderCategories();
renderTasteTags();
renderCraftTags();
}
function shuffleArray(array) {
const shuffled = [...array];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
function renderAllergens() {
const allergenContainer = document.getElementById('allergenTags');
if (configData.allergen_types && configData.allergen_types.length > 0) {
const items = shuffleArray(configData.allergen_types).slice(0, 20);
let html = '';
items.forEach(item => {
html += '<span class="filter-tag" data-type="allergen" data-value="' + item.type + '">';
html += item.icon + ' ' + item.name;
html += '</span>';
});
allergenContainer.innerHTML = html;
} else {
allergenContainer.innerHTML = '<span style="color: var(--text-secondary);">暂无过敏原选项</span>';
}
}
function renderCategories() {
const categoryContainer = document.getElementById('categoryTags');
if (configData.categories && configData.categories.length > 0) {
const recipeCategories = shuffleArray(configData.categories.filter(c => c.parent_id === 11)).slice(0, 10);
const ingredientCategories = shuffleArray(configData.categories.filter(c => c.parent_id === 1000)).slice(0, 10);
let categoryHtml = '';
if (recipeCategories.length > 0) {
categoryHtml += '<div style="margin-bottom: 8px; font-size: 12px; color: var(--text-secondary);">📖 菜谱分类</div>';
recipeCategories.forEach(item => {
categoryHtml += '<span class="filter-tag expandable" data-type="category" data-value="' + item.id + '" data-name="' + item.name + '" onclick="toggleSubcategories(this, ' + item.id + ')">';
categoryHtml += item.name + ' ▾';
categoryHtml += '</span>';
});
}
if (ingredientCategories.length > 0) {
categoryHtml += '<div style="margin: 12px 0 8px 0; font-size: 12px; color: var(--text-secondary);">🥬 食材分类</div>';
ingredientCategories.forEach(item => {
categoryHtml += '<span class="filter-tag expandable" data-type="category" data-value="' + item.id + '" data-name="' + item.name + '" onclick="toggleSubcategories(this, ' + item.id + ')">';
categoryHtml += item.name + ' ▾';
categoryHtml += '</span>';
});
}
categoryHtml += '<div id="subcategoryContainer" class="subcategory-container" style="display: none;"></div>';
categoryContainer.innerHTML = categoryHtml || '<span style="color: var(--text-secondary);">暂无分类</span>';
} else {
categoryContainer.innerHTML = '<span style="color: var(--text-secondary);">暂无分类</span>';
}
}
function renderTasteTags() {
const tasteContainer = document.getElementById('tasteTags');
if (configData.tags && configData.tags.taste && configData.tags.taste.length > 0) {
const items = shuffleArray(configData.tags.taste).slice(0, 20);
let html = '';
items.forEach(item => {
html += '<span class="filter-tag" data-type="tag" data-value="' + item.id + '">';
html += item.name;
html += '</span>';
});
tasteContainer.innerHTML = html;
} else {
tasteContainer.innerHTML = '<span style="color: var(--text-secondary);">暂无口味选项</span>';
}
}
function renderCraftTags() {
const craftContainer = document.getElementById('craftTags');
if (configData.tags && configData.tags.craft && configData.tags.craft.length > 0) {
const items = shuffleArray(configData.tags.craft).slice(0, 20);
let html = '';
items.forEach(item => {
html += '<span class="filter-tag" data-type="tag" data-value="' + item.id + '">';
html += item.name;
html += '</span>';
});
craftContainer.innerHTML = html;
} else {
craftContainer.innerHTML = '<span style="color: var(--text-secondary);">暂无工艺选项</span>';
}
}
let currentExpandedCategory = null;
async function toggleSubcategories(element, parentId) {
const container = document.getElementById('subcategoryContainer');
const categoryName = element.dataset.name;
document.querySelectorAll('.filter-tag.expandable').forEach(tag => {
tag.classList.remove('active');
});
if (currentExpandedCategory === parentId) {
container.style.display = 'none';
currentExpandedCategory = null;
return;
}
element.classList.add('active');
currentExpandedCategory = parentId;
container.innerHTML = '<div class="subcategory-header">' +
'<span class="subcategory-title">📁 ' + categoryName + ' 子分类</span>' +
'<button class="refresh-btn" onclick="loadSubcategories(' + parentId + ', \'' + categoryName + '\')">🔄 刷新</button>' +
'</div>' +
'<div class="subcategory-tags" id="subcategoryTags">' +
'<span style="color: var(--text-secondary);">加载中...</span>' +
'</div>';
container.style.display = 'block';
await loadSubcategories(parentId, categoryName);
}
async function loadSubcategories(parentId, categoryName) {
const tagsContainer = document.getElementById('subcategoryTags');
try {
const selectedTags = selectedFilters.tags.join(',');
if (selectedTags) {
const url = API_BASE + 'api_what_to_eat.php?act=available_filters&parent_category_id=' + parentId + '&selected_tags=' + selectedTags;
const response = await fetch(url);
const data = await response.json();
if (data.code === 200 && data.data.available_subcategories && data.data.available_subcategories.length > 0) {
let html = '';
data.data.available_subcategories.forEach(item => {
const isSelected = selectedFilters.categories.includes(String(item.id)) ? 'selected' : '';
html += '<span class="subcategory-tag ' + isSelected + '" ';
html += 'data-type="category" data-value="' + item.id + '" ';
html += 'onclick="selectSubcategory(this)">';
html += item.name + ' (' + item.count + ')';
html += '</span>';
});
tagsContainer.innerHTML = html;
} else {
tagsContainer.innerHTML = '<span style="color: var(--text-secondary);">暂无符合条件的子分类</span>';
}
} else {
const response = await fetch(API_BASE + 'api_what_to_eat.php?act=subcategories&parent_id=' + parentId);
const data = await response.json();
if (data.code === 200 && data.data.subcategories.length > 0) {
let html = '';
data.data.subcategories.forEach(item => {
const isSelected = selectedFilters.categories.includes(String(item.id)) ? 'selected' : '';
html += '<span class="subcategory-tag ' + isSelected + '" ';
html += 'data-type="category" data-value="' + item.id + '" ';
html += 'onclick="selectSubcategory(this)">';
html += item.name;
html += '</span>';
});
tagsContainer.innerHTML = html;
} else {
tagsContainer.innerHTML = '<span style="color: var(--text-secondary);">暂无子分类</span>';
}
}
} catch (error) {
console.error('加载子分类失败:', error);
tagsContainer.innerHTML = '<span style="color: red;">加载失败</span>';
}
}
function selectSubcategory(element) {
element.classList.toggle('selected');
const value = element.dataset.value;
if (element.classList.contains('selected')) {
if (!selectedFilters.categories.includes(value)) {
selectedFilters.categories.push(value);
}
} else {
const index = selectedFilters.categories.indexOf(value);
if (index > -1) {
selectedFilters.categories.splice(index, 1);
}
}
updateAvailableFilters();
}
async function updateAvailableFilters() {
const selectedCategories = selectedFilters.categories.join(',');
const selectedTags = selectedFilters.tags.join(',');
const parentCategoryId = currentExpandedCategory || 0;
if (!selectedCategories && !selectedTags && !parentCategoryId) {
return;
}
let url = API_BASE + 'api_what_to_eat.php?act=available_filters';
if (selectedCategories) {
url += '&selected_categories=' + selectedCategories;
}
if (selectedTags) {
url += '&selected_tags=' + selectedTags;
}
if (parentCategoryId) {
url += '&parent_category_id=' + parentCategoryId;
}
try {
const response = await fetch(url);
const data = await response.json();
if (data.code === 200) {
updateFilterUI(data.data);
}
} catch (error) {
console.error('更新筛选选项失败:', error);
}
}
function updateFilterUI(data) {
const recipeCountEl = document.getElementById('recipeCount');
if (recipeCountEl) {
recipeCountEl.textContent = '符合条件: ' + data.total_recipes + ' 个菜谱';
recipeCountEl.style.display = 'block';
}
if (data.available_subcategories && data.available_subcategories.length > 0) {
const tagsContainer = document.getElementById('subcategoryTags');
if (tagsContainer) {
let html = '';
data.available_subcategories.forEach(item => {
const isSelected = selectedFilters.categories.includes(String(item.id)) ? 'selected' : '';
html += '<span class="subcategory-tag ' + isSelected + '" ';
html += 'data-type="category" data-value="' + item.id + '" ';
html += 'onclick="selectSubcategory(this)">';
html += item.name + ' (' + item.count + ')';
html += '</span>';
});
tagsContainer.innerHTML = html;
}
}
if (data.available_tags) {
if (data.available_tags.taste && data.available_tags.taste.length > 0) {
const tasteContainer = document.getElementById('tasteTags');
if (tasteContainer) {
let html = '';
data.available_tags.taste.forEach(item => {
const isSelected = selectedFilters.tags.includes(String(item.id)) ? 'selected' : '';
html += '<span class="filter-tag ' + isSelected + '" ';
html += 'data-type="tag" data-value="' + item.id + '">';
html += item.name + ' (' + item.count + ')';
html += '</span>';
});
tasteContainer.innerHTML = html;
}
}
if (data.available_tags.craft && data.available_tags.craft.length > 0) {
const craftContainer = document.getElementById('craftTags');
if (craftContainer) {
let html = '';
data.available_tags.craft.forEach(item => {
const isSelected = selectedFilters.tags.includes(String(item.id)) ? 'selected' : '';
html += '<span class="filter-tag ' + isSelected + '" ';
html += 'data-type="tag" data-value="' + item.id + '">';
html += item.name + ' (' + item.count + ')';
html += '</span>';
});
craftContainer.innerHTML = html;
}
}
}
}
function toggleIngredientDetail(index) {
const detailEl = document.getElementById('ingredientDetail' + index);
const itemEl = detailEl.previousElementSibling;
if (detailEl.style.display === 'none') {
detailEl.style.display = 'block';
itemEl.classList.add('expanded');
} else {
detailEl.style.display = 'none';
itemEl.classList.remove('expanded');
}
}
function bindEvents() {
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentMode = this.dataset.mode;
document.getElementById('filterPanel').classList.toggle('show', currentMode === 'smart');
});
});
document.querySelectorAll('.speed-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.speed-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentSpeed = this.dataset.speed;
});
});
// 刷新按钮事件
document.querySelectorAll('.refresh-btn').forEach(btn => {
btn.addEventListener('click', function() {
const type = this.dataset.type;
switch(type) {
case 'allergen':
renderAllergens();
break;
case 'category':
renderCategories();
break;
case 'taste':
renderTasteTags();
break;
case 'craft':
renderCraftTags();
break;
}
});
});
document.addEventListener('click', function(e) {
if (e.target.classList.contains('filter-tag')) {
e.target.classList.toggle('selected');
const type = e.target.dataset.type;
const value = e.target.dataset.value;
if (e.target.classList.contains('selected')) {
selectedFilters[type + 's'].push(value);
} else {
const index = selectedFilters[type + 's'].indexOf(value);
if (index > -1) {
selectedFilters[type + 's'].splice(index, 1);
}
}
updateAvailableFilters();
}
});
document.getElementById('startBtn').addEventListener('click', startSelection);
document.getElementById('retryBtn').addEventListener('click', function() {
const resultCard = document.getElementById('resultCard');
const candidatesContainer = document.getElementById('candidatesContainer');
if (currentCandidates.length > 0) {
document.querySelector('.result-content').style.display = 'none';
candidatesContainer.style.display = 'block';
showCandidatesList();
} else {
startSelection();
}
});
document.getElementById('likeBtn').addEventListener('click', function() {
const recipeId = this.dataset.id;
if (recipeId) {
doAction('like', recipeId);
}
});
document.getElementById('recommendBtn').addEventListener('click', function() {
const recipeId = this.dataset.id;
if (recipeId) {
doAction('recommend', recipeId);
}
});
document.getElementById('detailBtn').addEventListener('click', function() {
const recipeId = this.dataset.id;
if (recipeId) {
window.open(API_BASE + 'api.php?act=detail&id=' + recipeId, '_blank');
}
});
}
async function startSelection() {
console.log('===== startSelection 开始 =====');
const startBtn = document.getElementById('startBtn');
const wheel = document.getElementById('wheel');
const loading = document.getElementById('loading');
const resultCard = document.getElementById('resultCard');
console.log('startBtn:', startBtn);
console.log('wheel:', wheel);
console.log('loading:', loading);
console.log('resultCard:', resultCard);
startBtn.disabled = true;
resultCard.classList.remove('show');
loading.classList.add('show');
if (currentSpeed !== 'skip') {
wheel.classList.add('spinning');
}
try {
let url = API_BASE + 'api_what_to_eat.php?act=' + currentMode;
if (currentMode === 'smart') {
if (selectedFilters.allergens.length > 0) {
url += '&exclude_allergens=' + selectedFilters.allergens.join(',');
}
if (selectedFilters.categories.length > 0) {
url += '&include_categories=' + selectedFilters.categories.join(',');
}
if (selectedFilters.tags.length > 0) {
url += '&include_tags=' + selectedFilters.tags.join(',');
}
}
console.log('请求URL:', url);
const response = await fetch(url);
console.log('Response:', response);
const data = await response.json();
console.log('API返回数据:', data);
console.log('data.code:', data.code);
console.log('data.data:', data.data);
const durations = { fast: 2000, medium: 5000, slow: 8000, skip: 0 };
const duration = durations[currentSpeed];
if (currentSpeed !== 'skip') {
await new Promise(resolve => setTimeout(resolve, duration));
wheel.classList.remove('spinning');
}
loading.classList.remove('show');
if (data.code === 200) {
console.log('data.data.candidates:', data.data?.candidates);
currentCandidates = data.data?.candidates || [];
candidatesCount = data.data?.candidates_count || 0;
bestMatchCount = data.data?.best_match_count || 0;
console.log('currentCandidates:', currentCandidates);
console.log('candidatesCount:', candidatesCount);
console.log('调用 showCandidatesList');
showCandidatesList();
} else {
alert(data.message || '选择失败,请重试');
}
} catch (error) {
console.error('请求失败:', error);
loading.classList.remove('show');
wheel.classList.remove('spinning');
alert('网络错误,请重试');
} finally {
startBtn.disabled = false;
}
}
function showCandidatesList() {
console.log('===== showCandidatesList 开始 =====');
console.log('currentCandidates:', currentCandidates);
console.log('currentCandidates.length:', currentCandidates.length);
const matchInfo = document.getElementById('matchInfo');
const candidatesContainer = document.getElementById('candidatesContainer');
const resultContent = document.querySelector('.result-content');
console.log('matchInfo:', matchInfo);
console.log('candidatesContainer:', candidatesContainer);
console.log('resultContent:', resultContent);
resultContent.style.display = 'none';
document.getElementById('candidatesCount').textContent = candidatesCount;
document.getElementById('bestMatchCount').textContent = bestMatchCount;
matchInfo.style.display = 'flex';
if (currentCandidates.length === 0) {
console.log('没有候选菜品');
candidatesContainer.innerHTML = '<p style="text-align: center; color: var(--text-secondary);">暂无候选菜品</p>';
return;
}
console.log('开始渲染候选菜品');
let html = '<div class="candidates-grid">';
currentCandidates.forEach((recipe, index) => {
console.log('渲染菜谱 ' + index + ':', recipe);
const coverSrc = recipe.cover || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect fill="%23f2f2f7" width="100" height="100"/><text x="50" y="55" text-anchor="middle" font-size="30">🍽️</text></svg>';
const recipeTitle = recipe.title || '未知菜谱';
const categoryName = (recipe.category && recipe.category.name) ? recipe.category.name : '未分类';
html += '<div class="candidate-card" onclick="selectCandidate(' + index + ')">';
html += '<img class="candidate-cover" src="' + coverSrc + '" alt="">';
html += '<div class="candidate-info">';
html += '<div class="candidate-title">' + recipeTitle + '</div>';
html += '<div class="candidate-meta">' + categoryName + '</div>';
html += '</div></div>';
});
html += '</div>';
html += '<div class="candidates-actions">';
html += '<button class="refresh-candidates-btn" onclick="refreshCandidates()">🔄 换一批</button>';
html += '<span class="candidates-hint">点击菜品查看详情</span>';
html += '</div>';
console.log('HTML生成完成长度:', html.length);
candidatesContainer.innerHTML = html;
candidatesContainer.style.display = 'block';
console.log('===== showCandidatesList 完成 =====');
}
function selectCandidate(index) {
if (index >= 0 && index < currentCandidates.length) {
showResult(currentCandidates[index]);
}
}
async function refreshCandidates() {
const candidatesContainer = document.getElementById('candidatesContainer');
candidatesContainer.innerHTML = '<p style="text-align: center; padding: 20px;">🔄 加载中...</p>';
await startSelection();
}
function showResult(recipe) {
console.log('显示结果:', recipe);
const resultCard = document.getElementById('resultCard');
const candidatesContainer = document.getElementById('candidatesContainer');
const resultContent = document.querySelector('.result-content');
candidatesContainer.style.display = 'none';
resultContent.style.display = 'block';
document.getElementById('resultCover').src = recipe.cover || 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect fill="%23f2f2f7" width="100" height="100"/><text x="50" y="55" text-anchor="middle" font-size="40">🍽️</text></svg>';
document.getElementById('resultTitle').textContent = recipe.title || '未知菜谱';
document.getElementById('resultIntro').textContent = recipe.intro || '暂无简介';
const categoryName = (recipe.category && recipe.category.name) ? recipe.category.name : '未分类';
const tags = recipe.tags || [];
let metaHtml = '<span class="meta-tag">' + categoryName + '</span>';
tags.forEach(t => {
metaHtml += '<span class="meta-tag">' + (t.name || '') + '</span>';
});
document.getElementById('resultMeta').innerHTML = metaHtml;
const ingredients = recipe.ingredients || {};
const mainIngredients = ingredients.main || [];
const auxiliaryIngredients = ingredients.auxiliary || [];
const seasoningIngredients = ingredients.seasoning || [];
let ingredientHtml = '';
function renderIngredientItem(ing, index) {
const hasDetail = ing.detail && ing.detail.id;
let onclickAttr = '';
if (hasDetail) {
onclickAttr = ' onclick="toggleIngredientDetail(' + index + ')"';
}
let html = '<div class="ingredient-item' + (hasDetail ? ' has-detail' : '') + '"' + onclickAttr + '>';
html += '<span class="name">' + (ing.name || '') + '</span>';
html += '<span class="amount">' + (ing.amount || '') + '</span>';
if (hasDetail) {
html += '<span class="expand-icon">▾</span>';
}
html += '</div>';
if (hasDetail) {
html += '<div class="ingredient-detail" id="ingredientDetail' + index + '" style="display: none;">';
html += '<div class="detail-section">';
if (ing.detail.alias && ing.detail.alias.length > 0) {
html += '<div class="detail-row"><span class="detail-label">别名:</span>' + ing.detail.alias.join('、') + '</div>';
}
if (ing.detail.intro) {
html += '<div class="detail-row"><span class="detail-label">简介:</span>' + (ing.detail.intro.substring(0, 100) || '') + '...</div>';
}
if (ing.detail.efficacy) {
html += '<div class="detail-row"><span class="detail-label">功效:</span>' + (ing.detail.efficacy.substring(0, 100) || '') + '...</div>';
}
if (ing.detail.suitable_crowd && ing.detail.suitable_crowd.length > 0) {
html += '<div class="detail-row"><span class="detail-label" style="color: #34C759;">✅ 适宜:</span>' + ing.detail.suitable_crowd.join('') + '</div>';
}
if (ing.detail.unsuitable_crowd && ing.detail.unsuitable_crowd.length > 0) {
html += '<div class="detail-row"><span class="detail-label" style="color: #FF3B30;">❌ 不宜:</span>' + ing.detail.unsuitable_crowd.join('') + '</div>';
}
if (ing.detail.allergen_type && ing.detail.allergen_type.length > 0) {
html += '<div class="detail-row"><span class="detail-label" style="color: #FF9500;">⚠️ 过敏原:</span>' + ing.detail.allergen_type.join('、') + '</div>';
}
if (ing.detail.category_names && ing.detail.category_names.length > 0) {
html += '<div class="detail-row"><span class="detail-label">分类:</span>' + ing.detail.category_names.join('、') + '</div>';
}
html += '</div></div>';
}
return html;
}
let ingredientIndex = 0;
if (mainIngredients.length > 0) {
ingredientHtml += '<div style="margin-bottom: 12px;"><strong style="color: var(--primary-color);">🍖 主料</strong></div>';
ingredientHtml += '<div class="ingredient-list" style="margin-bottom: 16px;">';
mainIngredients.forEach(ing => {
ingredientHtml += renderIngredientItem(ing, ingredientIndex++);
});
ingredientHtml += '</div>';
}
if (auxiliaryIngredients.length > 0) {
ingredientHtml += '<div style="margin-bottom: 12px;"><strong style="color: var(--primary-color);">🥬 辅料</strong></div>';
ingredientHtml += '<div class="ingredient-list" style="margin-bottom: 16px;">';
auxiliaryIngredients.forEach(ing => {
ingredientHtml += renderIngredientItem(ing, ingredientIndex++);
});
ingredientHtml += '</div>';
}
if (seasoningIngredients.length > 0) {
ingredientHtml += '<div style="margin-bottom: 12px;"><strong style="color: var(--primary-color);">🧂 调料</strong></div>';
ingredientHtml += '<div class="ingredient-list">';
seasoningIngredients.forEach(ing => {
ingredientHtml += renderIngredientItem(ing, ingredientIndex++);
});
ingredientHtml += '</div>';
}
document.getElementById('ingredientList').innerHTML = ingredientHtml || '<p>暂无食材信息</p>';
if (recipe.nutrition && (recipe.nutrition.calories || recipe.nutrition.protein || (recipe.nutrition.all && recipe.nutrition.all.length > 0))) {
document.getElementById('nutritionSection').style.display = 'block';
let nutritionHtml = '';
if (recipe.nutrition.calories) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.calories + '</div>';
nutritionHtml += '<div class="label">能量</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.protein) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.protein + '</div>';
nutritionHtml += '<div class="label">蛋白质</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.fat) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.fat + '</div>';
nutritionHtml += '<div class="label">脂肪</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.carbs) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.carbs + '</div>';
nutritionHtml += '<div class="label">碳水</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.fiber) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.fiber + '</div>';
nutritionHtml += '<div class="label">纤维</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.sodium) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.sodium + '</div>';
nutritionHtml += '<div class="label">钠</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.cholesterol) {
nutritionHtml += '<div class="nutrition-item">';
nutritionHtml += '<div class="value">' + recipe.nutrition.cholesterol + '</div>';
nutritionHtml += '<div class="label">胆固醇</div>';
nutritionHtml += '</div>';
}
if (recipe.nutrition.all && recipe.nutrition.all.length > 0) {
const mainNutrients = ['能量', '热量', '蛋白质', '脂肪', '碳水', '纤维', '钠', '胆固醇'];
const otherNutrients = recipe.nutrition.all.filter(n => !mainNutrients.some(m => n.name.includes(m)));
otherNutrients.slice(0, 4).forEach(n => {
nutritionHtml += '<div class="nutrition-item">' +
'<div class="value">' + n.value + n.unit + '</div>' +
'<div class="label">' + n.name + '</div>' +
'</div>';
});
}
document.getElementById('nutritionGrid').innerHTML = nutritionHtml || '<p style="color: var(--text-secondary);">暂无营养信息</p>';
} else {
document.getElementById('nutritionSection').style.display = 'none';
}
document.getElementById('detailBtn').dataset.id = recipe.id;
document.getElementById('likeBtn').dataset.id = recipe.id;
document.getElementById('recommendBtn').dataset.id = recipe.id;
const stats = recipe.statistics || {};
document.getElementById('statViews').textContent = stats.view_count || 0;
document.getElementById('statLikes').textContent = stats.like_count || 0;
document.getElementById('statRecommends').textContent = stats.recommend_count || 0;
resultCard.classList.add('show');
}
async function doAction(action, recipeId) {
try {
const response = await fetch(API_BASE + 'api_what_to_eat.php?act=' + action + '&id=' + recipeId);
const data = await response.json();
if (data.code === 200) {
if (action === 'like') {
document.getElementById('statLikes').textContent = data.data.like_count;
} else if (action === 'recommend') {
document.getElementById('statRecommends').textContent = data.data.recommend_count;
}
}
} catch (error) {
console.error('操作失败:', error);
}
}
</script>
</body>
</html>