效果预览
效果截图
源码展示
<!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>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<!-- 配置Tailwind自定义主题 -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'meat-red': '#FF6B6B',
'vegetable-green': '#4CD137',
'seasoning-gold': '#F7CA18',
'action-blue': '#3498DB',
'light-gray': '#ECF0F1',
'dark-text': '#2C3E50',
},
fontFamily: {
sans: ['"Noto Sans SC"', 'sans-serif'],
},
},
}
}
</script>
<style type="text/tailwindcss">
@layer utilities {
.content-auto {
content-visibility: auto;
}
.btn-hover {
@apply transition-all duration-300 hover:scale-105 active:scale-95;
}
.card-hover {
@apply transition-all duration-300 hover:shadow-lg hover:-translate-y-1;
}
.input-focus {
@apply focus:ring-2 focus:ring-action-blue focus:border-action-blue;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.dragging {
@apply opacity-50 scale-95;
}
.placeholder-gray-400::placeholder {
color: rgba(156, 163, 175, 0.5);
}
}
</style>
</head>
<body class="bg-light-gray font-sans text-dark-text min-h-screen">
<div id="app" class="container mx-auto px-4 py-6 max-w-5xl">
<!-- 顶部栏 -->
<header class="flex justify-between items-center mb-8">
<h1 class="text-[clamp(1.5rem,3vw,2rem)] font-bold text-center flex-1">美食食材配料表生成器</h1>
</header>
<!-- 主界面内容 -->
<main class="flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">
<!-- 已选食材计数 -->
<div id="selection-count" class="w-full mb-6 bg-white p-4 rounded-lg shadow-md text-center">
<span class="text-lg">已选:<span id="meat-count" class="text-meat-red font-bold">0</span> 肉类,
<span id="vegetable-count" class="text-vegetable-green font-bold">0</span> 素菜,
<span id="seasoning-count" class="text-seasoning-gold font-bold">0</span> 配料</span>
</div>
<!-- 核心按钮区 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-md mb-12">
<button id="meat-btn" class="bg-meat-red text-white rounded-xl p-6 shadow-lg btn-hover flex flex-col items-center">
<i class="fa fa-cutlery text-3xl mb-2"></i>
<span class="text-xl font-bold">肉类</span>
</button>
<button id="vegetable-btn" class="bg-vegetable-green text-white rounded-xl p-6 shadow-lg btn-hover flex flex-col items-center">
<i class="fa fa-leaf text-3xl mb-2"></i>
<span class="text-xl font-bold">素菜</span>
</button>
<button id="seasoning-btn" class="bg-seasoning-gold text-white rounded-xl p-6 shadow-lg btn-hover flex flex-col items-center">
<i class="fa fa-flask text-3xl mb-2"></i>
<span class="text-xl font-bold">配料</span>
</button>
</div>
<!-- 生成列表按钮 -->
<button id="generate-btn" class="bg-gradient-to-r from-action-blue to-blue-600 text-white text-xl font-bold py-4 px-12 rounded-full shadow-lg btn-hover w-full max-w-md">
生成配料表
</button>
</main>
<!-- 生成的配料表区域 -->
<div id="ingredients-list" class="hidden mt-8 bg-white rounded-lg shadow-lg p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">食材配料表</h2>
<button id="reset-btn" class="text-red-500 hover:text-red-700 btn-hover">
<i class="fa fa-refresh mr-2"></i>清空重置
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="border-b">
<th class="text-left py-2 px-4">分类</th>
<th class="text-left py-2 px-4">食材名称</th>
<th class="text-left py-2 px-4">数量</th>
<th class="text-left py-2 px-4">排序</th>
<th class="text-left py-2 px-4">操作</th>
</tr>
</thead>
<tbody id="ingredients-table-body">
<!-- 动态生成的表格内容 -->
</tbody>
</table>
</div>
<div class="flex justify-between mt-6">
<button id="sort-btn" class="bg-light-gray text-dark-text py-2 px-4 rounded-lg shadow btn-hover">
<i class="fa fa-sort mr-2"></i>一键排序
</button>
<button id="copy-btn" class="bg-action-blue text-white py-2 px-4 rounded-lg shadow btn-hover">
<i class="fa fa-copy mr-2"></i>复制列表
</button>
</div>
</div>
<!-- 历史记录面板 -->
<div id="history-section" class="mt-8 bg-white rounded-lg shadow-lg p-6">
<h2 class="text-xl font-bold mb-4 flex justify-between items-center">
历史记录
<button id="clear-history-btn" class="text-sm text-red-500 hover:text-red-700 btn-hover">
<i class="fa fa-trash mr-1"></i>清空历史
</button>
</h2>
<div id="history-list" class="space-y-3 max-h-[300px] overflow-y-auto">
<!-- 历史记录将动态生成 -->
</div>
</div>
<!-- 肉类选择弹窗 -->
<div id="meat-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-t-lg rounded-b-2xl shadow-xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="text-xl font-bold">选择肉类</h3>
<button id="close-meat-btn" class="text-gray-500 hover:text-dark-text">
<i class="fa fa-times"></i>
</button>
</div>
<div class="p-4">
<div class="relative mb-4">
<input type="text" id="meat-search" placeholder="搜索肉类..." class="w-full py-2 px-4 rounded-lg border shadow-sm input-focus">
<i class="fa fa-search absolute right-4 top-3 text-gray-400"></i>
</div>
<div class="grid grid-cols-3 gap-3 overflow-y-auto max-h-[40vh] scrollbar-hide">
<!-- 肉类选项将动态生成 -->
</div>
</div>
</div>
</div>
<!-- 素菜选择弹窗 -->
<div id="vegetable-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-t-lg rounded-b-2xl shadow-xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="text-xl font-bold">选择素菜</h3>
<button id="close-vegetable-btn" class="text-gray-500 hover:text-dark-text">
<i class="fa fa-times"></i>
</button>
</div>
<div class="p-4">
<div class="relative mb-4">
<input type="text" id="vegetable-search" placeholder="搜索素菜..." class="w-full py-2 px-4 rounded-lg border shadow-sm input-focus">
<i class="fa fa-search absolute right-4 top-3 text-gray-400"></i>
</div>
<div class="grid grid-cols-3 gap-3 overflow-y-auto max-h-[40vh] scrollbar-hide">
<!-- 素菜选项将动态生成 -->
</div>
</div>
</div>
</div>
<!-- 配料选择弹窗 -->
<div id="seasoning-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-t-lg rounded-b-2xl shadow-xl w-full max-w-md max-h-[80vh] flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h3 class="text-xl font-bold">选择配料</h3>
<button id="close-seasoning-btn" class="text-gray-500 hover:text-dark-text">
<i class="fa fa-times"></i>
</button>
</div>
<div class="p-4">
<!-- 新增搜索框 -->
<div class="relative mb-4">
<input type="text" id="seasoning-search" placeholder="搜索配料..." class="w-full py-2 px-4 rounded-lg border shadow-sm input-focus">
<i class="fa fa-search absolute right-4 top-3 text-gray-400"></i>
</div>
<!-- 配料分类标签 -->
<div class="flex mb-4 overflow-x-auto scrollbar-hide pb-2">
<button class="seasoning-tab active bg-seasoning-gold text-white py-2 px-4 rounded-full mr-2 btn-hover whitespace-nowrap" data-category="基础配料">基础配料</button>
<button class="seasoning-tab bg-light-gray text-dark-text py-2 px-4 rounded-full mr-2 btn-hover whitespace-nowrap" data-category="酱汁配料">酱汁配料</button>
<button class="seasoning-tab bg-light-gray text-dark-text py-2 px-4 rounded-full btn-hover whitespace-nowrap" data-category="特殊配料">特殊配料</button>
</div>
<div class="grid grid-cols-2 gap-3 overflow-y-auto max-h-[30vh] scrollbar-hide" id="seasoning-items">
<!-- 配料选项将动态生成 -->
</div>
</div>
</div>
</div>
<!-- 数量选择弹窗 -->
<div id="quantity-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-xl font-bold mb-4">选择数量</h3>
<div class="mb-6">
<p class="text-lg mb-2">食材:<span id="quantity-food-name" class="font-bold"></span></p>
<div class="flex items-center justify-between mb-4">
<button id="decrease-btn" class="bg-light-gray text-dark-text w-12 h-12 rounded-full flex items-center justify-center btn-hover">
<i class="fa fa-minus"></i>
</button>
<input type="number" id="quantity-input" min="1" class="text-2xl font-bold w-24 text-center border-b-2 border-gray-300 focus:outline-none focus:border-action-blue placeholder-gray-400" placeholder="100">
<button id="increase-btn" class="bg-light-gray text-dark-text w-12 h-12 rounded-full flex items-center justify-center btn-hover">
<i class="fa fa-plus"></i>
</button>
</div>
<div class="grid grid-cols-4 gap-2">
<button class="unit-btn active bg-action-blue text-white py-2 rounded-lg btn-hover" data-unit="克">克</button>
<button class="unit-btn bg-light-gray text-dark-text py-2 rounded-lg btn-hover" data-unit="斤">斤</button>
<button class="unit-btn bg-light-gray text-dark-text py-2 rounded-lg btn-hover" data-unit="片">片</button>
<button class="unit-btn bg-light-gray text-dark-text py-2 rounded-lg btn-hover" data-unit="勺">勺</button>
</div>
</div>
<div class="flex justify-between">
<button id="cancel-quantity-btn" class="bg-light-gray text-dark-text py-3 px-6 rounded-lg shadow btn-hover">取消</button>
<button id="confirm-quantity-btn" class="bg-action-blue text-white py-3 px-6 rounded-lg shadow btn-hover">确认</button>
</div>
</div>
</div>
<!-- 复制列表时的菜品名称弹窗 -->
<div id="dish-name-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl shadow-xl w-full max-w-md p-6">
<h3 class="text-xl font-bold mb-4">请输入菜品名称</h3>
<div class="mb-6">
<input type="text" id="dish-name-input" placeholder="例如:宫保鸡丁" class="w-full py-3 px-4 rounded-lg border shadow-sm input-focus">
</div>
<div class="flex justify-between">
<button id="cancel-dish-name-btn" class="bg-light-gray text-dark-text py-3 px-6 rounded-lg shadow btn-hover">取消</button>
<button id="confirm-dish-name-btn" class="bg-action-blue text-white py-3 px-6 rounded-lg shadow btn-hover">确认</button>
</div>
</div>
</div>
<!-- 通知提示 -->
<div id="notification" class="fixed top-4 right-4 bg-dark-text text-white p-4 rounded-lg shadow-lg transform translate-x-full transition-transform duration-300 z-50 flex items-center">
<i class="fa fa-check-circle mr-2 text-action-blue"></i>
<span id="notification-text">操作成功</span>
</div>
</div>
<script>
// 数据模型
const state = {
selectedFoods: [], // 已选择的食材
currentFood: null, // 当前正在操作的食材
currentCategory: '基础配料', // 当前配料分类
history: JSON.parse(localStorage.getItem('foodGeneratorHistory')) || [] // 历史记录
};
// 食材数据 - 添加肉类对应符号表情
const foodData = {
meats: [
{ id: 'm01', name: '猪肉', icon: '🐷' },
{ id: 'm02', name: '腊肉', icon: '🥓' },
{ id: 'm03', name: '猪肝', icon: '🐷' },
{ id: 'm04', name: '鸡蛋', icon: '🥚' },
{ id: 'm05', name: '牛肉', icon: '🐂' },
{ id: 'm06', name: '鸭肉', icon: '🦆' },
{ id: 'm07', name: '鸡肉', icon: '🐔' },
{ id: 'm08', name: '鱼肉', icon: '🐟' },
{ id: 'm09', name: '排骨', icon: '🍖' },
{ id: 'm10', name: '五花肉', icon: '🐷' },
{ id: 'm11', name: '卤肉', icon: '🍖' },
{ id: 'm12', name: '扣肉', icon: '🍖' },
{ id: 'm13', name: '羊肉', icon: '🐑' },
{ id: 'm14', name: '火腿', icon: '🍖' },
{ id: 'm15', name: '肉肠', icon: '🌭' },
{ id: 'm16', name: '猪蹄', icon: '🐷' },
{ id: 'm17', name: '虾', icon: '🦐' },
{ id: 'm18', name: '鸡腿', icon: '🍗' },
{ id: 'm19', name: '鸭腿', icon: '🦆' },
{ id: 'm20', name: '鸡翅根', icon: '🍗' }
],
vegetables: [
{ id: 'v01', name: '青椒', icon: 'fa-leaf' },
{ id: 'v02', name: '螺丝椒', icon: 'fa-fire' },
{ id: 'v03', name: '土豆', icon: 'fa-leaf' },
{ id: 'v04', name: '苦瓜', icon: 'fa-leaf' },
{ id: 'v05', name: '丝瓜', icon: 'fa-leaf' },
{ id: 'v06', name: '豆角', icon: 'fa-leaf' },
{ id: 'v07', name: '荷兰豆', icon: 'fa-leaf' },
{ id: 'v08', name: '毛豆', icon: 'fa-leaf' },
{ id: 'v09', name: '韭菜', icon: 'fa-leaf' },
{ id: 'v10', name: '西兰花', icon: 'fa-leaf' },
{ id: 'v11', name: '红椒', icon: 'fa-circle' },
{ id: 'v12', name: '灯笼椒', icon: 'fa-circle' },
{ id: 'v13', name: '黄瓜', icon: 'fa-leaf' },
{ id: 'v14', name: '南瓜', icon: 'fa-leaf' },
{ id: 'v15', name: '生菜', icon: 'fa-leaf' },
{ id: 'v16', name: '莲藕', icon: 'fa-leaf' },
{ id: 'v17', name: '包菜', icon: 'fa-leaf' },
{ id: 'v18', name: '娃娃菜', icon: 'fa-leaf' },
{ id: 'v19', name: '大白菜', icon: 'fa-leaf' },
{ id: 'v20', name: '小白菜', icon: 'fa-leaf' },
{ id: 'v21', name: '菠菜', icon: 'fa-leaf' },
{ id: 'v22', name: '油麦菜', icon: 'fa-leaf' },
{ id: 'v23', name: '空心菜', icon: 'fa-leaf' },
{ id: 'v24', name: '茼蒿', icon: 'fa-leaf' },
{ id: 'v25', name: '苋菜', icon: 'fa-leaf' },
{ id: 'v26', name: '芹菜', icon: 'fa-leaf' },
{ id: 'v27', name: '白菜', icon: 'fa-leaf' },
{ id: 'v28', name: '莴笋', icon: 'fa-leaf' },
{ id: 'v29', name: '竹笋', icon: 'fa-leaf' },
{ id: 'v30', name: '胡萝卜', icon: 'fa-fire' }
],
seasonings: {
'基础配料': [
{ id: 's01', name: '调和油', icon: 'fa-flask' },
{ id: 's02', name: '蒜米', icon: 'fa-flask' },
{ id: 's03', name: '酱汁', icon: 'fa-flask' },
{ id: 's04', name: '葱段', icon: 'fa-flask' },
{ id: 's05', name: '生粉水', icon: 'fa-flask' },
{ id: 's06', name: '过滤水', icon: 'fa-tint' },
{ id: 's07', name: '红美人椒', icon: 'fa-fire' },
{ id: 's08', name: '小炒辣椒', icon: 'fa-fire' },
{ id: 's09', name: '胡椒粉', icon: 'fa-flask' },
{ id: 's10', name: '番茄酱', icon: 'fa-flask' },
{ id: 's11', name: '姜片', icon: 'fa-flask' },
{ id: 's12', name: '小米辣', icon: 'fa-fire' },
{ id: 's13', name: '盐', icon: 'fa-flask' },
{ id: 's14', name: '糖', icon: 'fa-flask' },
{ id: 's15', name: '辣椒', icon: 'fa-fire' },
{ id: 's16', name: '花椒', icon: 'fa-flask' },
{ id: 's17', name: '八角', icon: 'fa-flask' },
{ id: 's18', name: '桂皮', icon: 'fa-flask' },
{ id: 's19', name: '豆豉', icon: 'fa-leaf' }
],
'酱汁配料': [
{ id: 's20', name: '生抽', icon: 'fa-flask' },
{ id: 's21', name: '老抽', icon: 'fa-flask' },
{ id: 's22', name: '料酒', icon: 'fa-flask' },
{ id: 's23', name: '醋', icon: 'fa-flask' },
{ id: 's24', name: '蚝油', icon: 'fa-flask' },
{ id: 's25', name: '辣椒酱', icon: 'fa-fire' },
{ id: 's26', name: '甜面酱', icon: 'fa-flask' },
{ id: 's27', name: '淀粉', icon: 'fa-flask' }
],
'特殊配料': [
{ id: 's28', name: '花椒粉', icon: 'fa-flask' },
{ id: 's29', name: '孜然粉', icon: 'fa-flask' },
{ id: 's30', name: '五香粉', icon: 'fa-flask' },
{ id: 's31', name: '咖喱粉', icon: 'fa-flask' },
{ id: 's32', name: '老干妈', icon: 'fa-fire' },
{ id: 's33', name: '豆瓣酱', icon: 'fa-flask' }
]
}
};
// DOM元素
const elements = {
// 主界面
meatBtn: document.getElementById('meat-btn'),
vegetableBtn: document.getElementById('vegetable-btn'),
seasoningBtn: document.getElementById('seasoning-btn'),
generateBtn: document.getElementById('generate-btn'),
ingredientsList: document.getElementById('ingredients-list'),
ingredientsTableBody: document.getElementById('ingredients-table-body'),
meatCount: document.getElementById('meat-count'),
vegetableCount: document.getElementById('vegetable-count'),
seasoningCount: document.getElementById('seasoning-count'),
sortBtn: document.getElementById('sort-btn'),
copyBtn: document.getElementById('copy-btn'),
resetBtn: document.getElementById('reset-btn'),
// 历史记录
historyList: document.getElementById('history-list'),
clearHistoryBtn: document.getElementById('clear-history-btn'),
// 肉类弹窗
meatModal: document.getElementById('meat-modal'),
closeMeatBtn: document.getElementById('close-meat-btn'),
meatSearch: document.getElementById('meat-search'),
// 素菜弹窗
vegetableModal: document.getElementById('vegetable-modal'),
closeVegetableBtn: document.getElementById('close-vegetable-btn'),
vegetableSearch: document.getElementById('vegetable-search'),
// 配料弹窗
seasoningModal: document.getElementById('seasoning-modal'),
seasoningTabs: document.querySelectorAll('.seasoning-tab'),
closeSeasoningBtn: document.getElementById('close-seasoning-btn'),
seasoningSearch: document.getElementById('seasoning-search'),
// 数量弹窗
quantityModal: document.getElementById('quantity-modal'),
closeQuantityBtn: document.getElementById('cancel-quantity-btn'),
confirmQuantityBtn: document.getElementById('confirm-quantity-btn'),
quantityFoodName: document.getElementById('quantity-food-name'),
quantityInput: document.getElementById('quantity-input'),
decreaseBtn: document.getElementById('decrease-btn'),
increaseBtn: document.getElementById('increase-btn'),
unitBtns: document.querySelectorAll('.unit-btn'),
// 菜品名称弹窗
dishNameModal: document.getElementById('dish-name-modal'),
dishNameInput: document.getElementById('dish-name-input'),
cancelDishNameBtn: document.getElementById('cancel-dish-name-btn'),
confirmDishNameBtn: document.getElementById('confirm-dish-name-btn'),
// 通知
notification: document.getElementById('notification'),
notificationText: document.getElementById('notification-text')
};
// 初始化应用
function initApp() {
// 渲染肉类选项
renderFoodOptions('meats', foodData.meats);
// 渲染素菜选项
renderFoodOptions('vegetables', foodData.vegetables);
// 渲染配料选项
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
// 初始化事件监听器
initEventListeners();
// 更新计数
updateSelectionCount();
// 渲染历史记录
renderHistory();
// 初始化拖拽排序
initDragSort();
}
// 初始化事件监听器
function initEventListeners() {
// 主界面按钮
elements.meatBtn.addEventListener('click', () => showModal('meat'));
elements.vegetableBtn.addEventListener('click', () => showModal('vegetable'));
elements.seasoningBtn.addEventListener('click', () => showModal('seasoning'));
elements.generateBtn.addEventListener('click', generateIngredientsList);
elements.sortBtn.addEventListener('click', sortIngredients);
elements.copyBtn.addEventListener('click', () => {
if (state.selectedFoods.length === 0) {
showNotification('请先生成配料表', 'error');
return;
}
elements.dishNameModal.classList.remove('hidden');
});
elements.resetBtn.addEventListener('click', resetAll);
// 历史记录面板
elements.clearHistoryBtn.addEventListener('click', clearHistory);
// 肉类弹窗
elements.closeMeatBtn.addEventListener('click', () => elements.meatModal.classList.add('hidden'));
elements.meatSearch.addEventListener('input', filterMeats);
// 素菜弹窗
elements.closeVegetableBtn.addEventListener('click', () => elements.vegetableModal.classList.add('hidden'));
elements.vegetableSearch.addEventListener('input', filterVegetables);
// 配料弹窗
elements.closeSeasoningBtn.addEventListener('click', () => elements.seasoningModal.classList.add('hidden'));
elements.seasoningSearch.addEventListener('input', filterSeasonings);
// 配料分类标签
elements.seasoningTabs.forEach(tab => {
tab.addEventListener('click', () => {
// 切换标签样式
elements.seasoningTabs.forEach(t => {
t.classList.remove('active', 'bg-seasoning-gold', 'text-white');
t.classList.add('bg-light-gray', 'text-dark-text');
});
tab.classList.add('active', 'bg-seasoning-gold', 'text-white');
tab.classList.remove('bg-light-gray', 'text-dark-text');
// 更新当前分类并渲染配料
state.currentCategory = tab.dataset.category;
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
// 清空搜索框
elements.seasoningSearch.value = '';
});
});
// 数量弹窗
elements.closeQuantityBtn.addEventListener('click', () => elements.quantityModal.classList.add('hidden'));
elements.confirmQuantityBtn.addEventListener('click', confirmQuantity);
elements.decreaseBtn.addEventListener('click', decreaseQuantity);
elements.increaseBtn.addEventListener('click', increaseQuantity);
// 单位按钮
elements.unitBtns.forEach(btn => {
btn.addEventListener('click', () => {
elements.unitBtns.forEach(b => {
b.classList.remove('active', 'bg-action-blue', 'text-white');
b.classList.add('bg-light-gray', 'text-dark-text');
});
btn.classList.add('active', 'bg-action-blue', 'text-white');
btn.classList.remove('bg-light-gray', 'text-dark-text');
});
});
// 菜品名称弹窗
elements.cancelDishNameBtn.addEventListener('click', () => {
elements.dishNameModal.classList.add('hidden');
});
elements.confirmDishNameBtn.addEventListener('click', async () => {
const dishName = elements.dishNameInput.value.trim();
if (!dishName) {
showNotification('请输入菜品名称', 'error');
return;
}
try {
await copyIngredientsList(dishName);
elements.dishNameModal.classList.add('hidden');
elements.dishNameInput.value = '';
} catch (err) {
showNotification('复制失败,请重试(请允许剪贴板权限)', 'error');
console.error('复制失败:', err);
}
});
// ESC键关闭弹窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
['meat-modal', 'vegetable-modal', 'seasoning-modal', 'quantity-modal', 'dish-name-modal'].forEach(modalId => {
document.getElementById(modalId)?.classList.add('hidden');
});
}
});
}
// 初始化拖拽排序
function initDragSort() {
new Sortable(elements.ingredientsTableBody, {
handle: '.drag-handle',
animation: 150,
ghostClass: 'bg-light-gray',
onEnd: function(evt) {
// 更新数据顺序
const rows = Array.from(elements.ingredientsTableBody.querySelectorAll('tr'));
const newOrder = rows.map(row => {
return state.selectedFoods.find(food => food.id === row.dataset.id);
});
state.selectedFoods = newOrder;
showNotification('排序已更新');
}
});
}
// 渲染食物选项
function renderFoodOptions(type, foods) {
const container = type === 'meats'
? elements.meatModal.querySelector('.grid')
: elements.vegetableModal.querySelector('.grid');
container.innerHTML = '';
foods.forEach(food => {
const isSelected = state.selectedFoods.some(item => item.id === food.id);
const card = document.createElement('div');
card.className = `card-hover rounded-lg shadow p-3 flex flex-col items-center relative cursor-pointer transition-all duration-300 ${
isSelected
? type === 'meats'
? 'bg-meat-red/10 border-2 border-meat-red'
: 'bg-vegetable-green/10 border-2 border-vegetable-green'
: 'bg-white'
}`;
card.dataset.id = food.id;
card.dataset.name = food.name;
card.dataset.type = type === 'meats' ? '肉类' : '素菜';
// 检查是否是肉类并使用表情符号
const iconHtml = type === 'meats'
? `<span class="text-2xl mb-2">${food.icon}</span>`
: `<i class="fa ${food.icon} text-2xl mb-2 ${type === 'meats' ? 'text-meat-red' : 'text-vegetable-green'}"></i>`;
card.innerHTML += `
${iconHtml}
<span class="text-center text-sm">${food.name}</span>
`;
card.addEventListener('click', () => {
if (isSelected) {
// 取消选择
const index = state.selectedFoods.findIndex(item => item.id === food.id);
if (index !== -1) {
state.selectedFoods.splice(index, 1);
updateSelectionCount();
renderFoodOptions('meats', foodData.meats);
renderFoodOptions('vegetables', foodData.vegetables);
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
showNotification(`${food.name} 已从配料表中移除`);
}
} else {
// 添加选择
state.currentFood = {
id: food.id,
name: food.name,
type: type === 'meats' ? '肉类' : '素菜',
quantity: 100,
unit: '克'
};
elements.quantityFoodName.textContent = food.name;
elements.quantityInput.value = ''; // 设置为空
elements.unitBtns[0].click(); // 默认选择"克"
elements.quantityModal.classList.remove('hidden');
}
});
container.appendChild(card);
});
}
// 渲染配料选项
function renderSeasoningOptions(seasonings) {
const container = document.getElementById('seasoning-items');
container.innerHTML = '';
seasonings.forEach(seasoning => {
const isSelected = state.selectedFoods.some(item => item.id === seasoning.id);
const card = document.createElement('div');
card.className = `card-hover rounded-lg shadow p-3 flex items-center justify-between cursor-pointer transition-all duration-300 ${
isSelected
? 'bg-seasoning-gold/10 border-2 border-seasoning-gold'
: 'bg-white'
}`;
card.dataset.id = seasoning.id;
card.dataset.name = seasoning.name;
card.dataset.type = '配料';
card.innerHTML += `
<div class="flex items-center">
<i class="fa ${seasoning.icon} text-xl mr-2 text-seasoning-gold"></i>
<span>${seasoning.name}</span>
</div>
${isSelected ? '<i class="fa fa-check text-seasoning-gold"></i>' : ''}
`;
card.addEventListener('click', () => {
if (isSelected) {
// 取消选择
const index = state.selectedFoods.findIndex(item => item.id === seasoning.id);
if (index !== -1) {
state.selectedFoods.splice(index, 1);
updateSelectionCount();
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
renderFoodOptions('meats', foodData.meats);
renderFoodOptions('vegetables', foodData.vegetables);
showNotification(`${seasoning.name} 已从配料表中移除`);
}
} else {
// 添加选择
state.currentFood = {
id: seasoning.id,
name: seasoning.name,
type: '配料',
quantity: 10,
unit: '克'
};
elements.quantityFoodName.textContent = seasoning.name;
elements.quantityInput.value = ''; // 设置为空
elements.unitBtns[0].click(); // 默认选择"克"
elements.quantityModal.classList.remove('hidden');
}
});
container.appendChild(card);
});
}
// 过滤肉类
function filterMeats() {
const searchTerm = elements.meatSearch.value.toLowerCase();
const filteredMeats = foodData.meats.filter(meat =>
meat.name.toLowerCase().includes(searchTerm)
);
renderFoodOptions('meats', filteredMeats);
}
// 过滤素菜
function filterVegetables() {
const searchTerm = elements.vegetableSearch.value.toLowerCase();
const filteredVegetables = foodData.vegetables.filter(vegetable =>
vegetable.name.toLowerCase().includes(searchTerm)
);
renderFoodOptions('vegetables', filteredVegetables);
}
// 过滤配料
function filterSeasonings() {
const searchTerm = elements.seasoningSearch.value.toLowerCase();
const filteredSeasonings = foodData.seasonings[state.currentCategory].filter(seasoning =>
seasoning.name.toLowerCase().includes(searchTerm)
);
renderSeasoningOptions(filteredSeasonings);
}
// 显示弹窗
function showModal(type) {
if (type === 'meat') {
elements.meatModal.classList.remove('hidden');
elements.meatSearch.focus();
} else if (type === 'vegetable') {
elements.vegetableModal.classList.remove('hidden');
elements.vegetableSearch.focus();
} else if (type === 'seasoning') {
elements.seasoningModal.classList.remove('hidden');
elements.seasoningSearch.focus();
}
}
// 确认数量
function confirmQuantity() {
if (!state.currentFood) return;
// 获取输入值,如果为空则使用默认值
const inputValue = elements.quantityInput.value.trim();
state.currentFood.quantity = inputValue ? parseInt(inputValue) : (state.currentFood.type === '配料' ? 10 : 100);
// 获取选中的单位
const activeUnitBtn = Array.from(elements.unitBtns).find(btn =>
btn.classList.contains('active')
);
state.currentFood.unit = activeUnitBtn ? activeUnitBtn.dataset.unit : '克';
// 检查是否已选择相同食材
const existingIndex = state.selectedFoods.findIndex(
item => item.id === state.currentFood.id
);
if (existingIndex !== -1) {
// 更新已选择的食材
state.selectedFoods[existingIndex] = state.currentFood;
} else {
// 添加新选择的食材
state.selectedFoods.push(state.currentFood);
}
// 关闭弹窗
elements.quantityModal.classList.add('hidden');
// 更新UI
updateSelectionCount();
renderFoodOptions('meats', foodData.meats);
renderFoodOptions('vegetables', foodData.vegetables);
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
showNotification(`${state.currentFood.name} 已添加到配料表`);
// 重置当前食材
state.currentFood = null;
}
// 减少数量
function decreaseQuantity() {
const currentValue = elements.quantityInput.value.trim()
? parseInt(elements.quantityInput.value)
: (state.currentFood.type === '配料' ? 10 : 100);
if (currentValue > 1) {
elements.quantityInput.value = currentValue - 1;
}
}
// 增加数量
function increaseQuantity() {
const currentValue = elements.quantityInput.value.trim()
? parseInt(elements.quantityInput.value)
: (state.currentFood.type === '配料' ? 10 : 100);
elements.quantityInput.value = currentValue + 1;
}
// 更新选择计数
function updateSelectionCount() {
const meatCount = state.selectedFoods.filter(food => food.type === '肉类').length;
const vegetableCount = state.selectedFoods.filter(food => food.type === '素菜').length;
const seasoningCount = state.selectedFoods.filter(food => food.type === '配料').length;
elements.meatCount.textContent = meatCount;
elements.vegetableCount.textContent = vegetableCount;
elements.seasoningCount.textContent = seasoningCount;
// 控制生成按钮状态
if (meatCount + vegetableCount + seasoningCount > 0) {
elements.generateBtn.removeAttribute('disabled');
elements.generateBtn.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
elements.generateBtn.setAttribute('disabled', 'true');
elements.generateBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
}
// 生成配料表
function generateIngredientsList() {
if (state.selectedFoods.length === 0) {
showNotification('请先选择食材', 'error');
return;
}
// 按类别排序:肉类 > 素菜 > 配料
state.selectedFoods.sort((a, b) => {
const categoryOrder = { '肉类': 1, '素菜': 2, '配料': 3 };
return categoryOrder[a.type] - categoryOrder[b.type];
});
// 渲染配料表
elements.ingredientsTableBody.innerHTML = '';
state.selectedFoods.forEach(food => {
const row = document.createElement('tr');
row.className = 'border-b hover:bg-light-gray/50 transition-colors duration-200';
row.dataset.id = food.id;
const categoryClass =
food.type === '肉类' ? 'text-meat-red' :
food.type === '素菜' ? 'text-vegetable-green' : 'text-seasoning-gold';
// 为肉类添加表情符号
const nameWithIcon = food.type === '肉类'
? `<span class="mr-1">${foodData.meats.find(m => m.id === food.id)?.icon || ''}</span>${food.name}`
: food.name;
row.innerHTML = `
<td class="py-3 px-4 ${categoryClass}">${food.type}</td>
<td class="py-3 px-4">${nameWithIcon}</td>
<td class="py-3 px-4">${food.quantity}${food.unit}</td>
<td class="py-3 px-4">
<button class="drag-handle bg-light-gray p-1 rounded cursor-move hover:bg-gray-200">
<i class="fa fa-bars"></i>
</button>
</td>
<td class="py-3 px-4">
<button class="delete-btn text-red-500 hover:text-red-700 transition-colors duration-200" data-id="${food.id}">
<i class="fa fa-trash"></i>
</button>
</td>
`;
elements.ingredientsTableBody.appendChild(row);
});
// 显示配料表
elements.ingredientsList.classList.remove('hidden');
// 滚动到配料表
elements.ingredientsList.scrollIntoView({ behavior: 'smooth' });
// 添加删除按钮事件
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const foodId = e.currentTarget.dataset.id;
const foodIndex = state.selectedFoods.findIndex(food => food.id === foodId);
if (foodIndex !== -1) {
const foodName = state.selectedFoods[foodIndex].name;
state.selectedFoods.splice(foodIndex, 1);
// 更新UI
updateSelectionCount();
renderFoodOptions('meats', foodData.meats);
renderFoodOptions('vegetables', foodData.vegetables);
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
generateIngredientsList();
showNotification(`${foodName} 已从配料表中移除`);
}
});
});
}
// 一键排序
function sortIngredients() {
if (state.selectedFoods.length === 0) return;
// 按类别和名称排序
state.selectedFoods.sort((a, b) => {
const categoryOrder = { '肉类': 1, '素菜': 2, '配料': 3 };
const categoryDiff = categoryOrder[a.type] - categoryOrder[b.type];
if (categoryDiff !== 0) {
return categoryDiff;
}
return a.name.localeCompare(b.name);
});
// 重新生成配料表
generateIngredientsList();
showNotification('配料表已按类别和名称排序');
}
// 复制配料表
async function copyIngredientsList(dishName) {
if (state.selectedFoods.length === 0) return;
// 构建复制内容
let content = `${dishName} 食材配料表\n\n`;
// 按类别分组
const groupedFoods = {
'肉类': [],
'素菜': [],
'配料': []
};
state.selectedFoods.forEach(food => {
groupedFoods[food.type].push(food);
});
// 按类别添加到内容
Object.keys(groupedFoods).forEach(category => {
if (groupedFoods[category].length > 0) {
content += `${category}:\n`;
groupedFoods[category].forEach(food => {
// 不复制表情符号,只复制名称
content += `- ${food.name} ${food.quantity}${food.unit}\n`;
});
content += '\n';
}
});
try {
// 复制到剪贴板
await navigator.clipboard.writeText(content);
// 保存到历史记录
const historyItem = {
id: Date.now(),
name: dishName,
ingredients: state.selectedFoods,
timestamp: new Date().toLocaleString()
};
state.history.unshift(historyItem);
// 限制历史记录数量为10
if (state.history.length > 10) {
state.history.pop();
}
// 保存到本地存储
localStorage.setItem('foodGeneratorHistory', JSON.stringify(state.history));
// 渲染历史记录
renderHistory();
showNotification(`"${dishName}" 的配料表已复制到剪贴板`);
} catch (err) {
console.error('复制失败:', err);
throw err; // 抛出错误,由调用者处理
}
}
// 渲染历史记录
function renderHistory() {
elements.historyList.innerHTML = '';
if (state.history.length === 0) {
elements.historyList.innerHTML = '<p class="text-center text-gray-500">暂无历史记录</p>';
return;
}
state.history.forEach(item => {
const historyCard = document.createElement('div');
historyCard.className = 'bg-white border rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow duration-300';
historyCard.innerHTML = `
<div class="flex justify-between items-start mb-2">
<h4 class="font-bold text-lg">${item.name}</h4>
<span class="text-sm text-gray-500">${item.timestamp}</span>
</div>
<div class="text-sm text-gray-600 mb-3">
${item.ingredients.length} 种食材 ·
${item.ingredients.filter(i => i.type === '肉类').length} 肉类 ·
${item.ingredients.filter(i => i.type === '素菜').length} 素菜 ·
${item.ingredients.filter(i => i.type === '配料').length} 配料
</div>
<div class="flex justify-between">
<button class="load-btn text-action-blue hover:text-blue-700 transition-colors duration-200 text-sm" data-id="${item.id}">
<i class="fa fa-refresh mr-1"></i>加载
</button>
<button class="delete-history-btn text-red-500 hover:text-red-700 transition-colors duration-200 text-sm" data-id="${item.id}">
<i class="fa fa-trash mr-1"></i>删除
</button>
</div>
</div>
`;
elements.historyList.appendChild(historyCard);
});
// 添加历史记录按钮事件
document.querySelectorAll('.load-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const itemId = parseInt(e.currentTarget.dataset.id);
const historyItem = state.history.find(item => item.id === itemId);
if (historyItem) {
state.selectedFoods = historyItem.ingredients;
updateSelectionCount();
renderFoodOptions('meats', foodData.meats);
renderFoodOptions('vegetables', foodData.vegetables);
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
generateIngredientsList();
showNotification(`已加载"${historyItem.name}"的配料表`);
}
});
});
document.querySelectorAll('.delete-history-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const itemId = parseInt(e.currentTarget.dataset.id);
const itemIndex = state.history.findIndex(item => item.id === itemId);
if (itemIndex !== -1) {
const itemName = state.history[itemIndex].name;
state.history.splice(itemIndex, 1);
// 保存到本地存储
localStorage.setItem('foodGeneratorHistory', JSON.stringify(state.history));
// 重新渲染
renderHistory();
showNotification(`已删除"${itemName}"的历史记录`);
}
});
});
}
// 清空历史记录
function clearHistory() {
if (state.history.length === 0) {
showNotification('历史记录已为空', 'info');
return;
}
if (confirm('确定要清空所有历史记录吗?')) {
state.history = [];
localStorage.setItem('foodGeneratorHistory', JSON.stringify(state.history));
renderHistory();
showNotification('历史记录已清空');
}
}
// 重置所有
function resetAll() {
if (state.selectedFoods.length === 0) {
showNotification('配料表已为空', 'info');
return;
}
if (confirm('确定要清空所有已选食材吗?')) {
state.selectedFoods = [];
updateSelectionCount();
renderFoodOptions('meats', foodData.meats);
renderFoodOptions('vegetables', foodData.vegetables);
renderSeasoningOptions(foodData.seasonings[state.currentCategory]);
elements.ingredientsList.classList.add('hidden');
showNotification('已清空所有已选食材');
}
}
// 显示通知
function showNotification(message, type = 'success') {
elements.notificationText.textContent = message;
// 设置通知类型
if (type === 'error') {
elements.notification.classList.remove('bg-dark-text');
elements.notification.classList.add('bg-red-600');
elements.notification.querySelector('i').className = 'fa fa-exclamation-circle mr-2 text-white';
} else if (type === 'info') {
elements.notification.classList.remove('bg-dark-text');
elements.notification.classList.add('bg-action-blue');
elements.notification.querySelector('i').className = 'fa fa-info-circle mr-2 text-white';
} else {
elements.notification.classList.remove('bg-red-600', 'bg-action-blue');
elements.notification.classList.add('bg-dark-text');
elements.notification.querySelector('i').className = 'fa fa-check-circle mr-2 text-action-blue';
}
// 显示通知
elements.notification.classList.remove('translate-x-full');
// 3秒后隐藏通知
setTimeout(() => {
elements.notification.classList.add('translate-x-full');
}, 3000);
}
// 初始化应用
document.addEventListener('DOMContentLoaded', initApp);
</script>
</body>
</html>
闲言碎语
本次使用豆包AI生成由Trae CN 源码
本来第一版本是长这样的(直接生成)
后面是先让他生成一个设计框架
然后再复制设计框架给它生成源码
然后就不断给它出修改建议
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END
暂无评论内容