feat: 新增菜品对比和食物相生相克功能

- 新增菜品对比功能,支持1v1左右和1vN上下布局切换
- 新增食物相生相克查询工具,包含API服务和详情页面
- 优化平台工具类,移除冗余的鸿蒙系统检测逻辑
- 更新版本号至1.5.1,修改更新日志和版本说明
- 修复多个页面列表分隔符构建器参数警告
- 新增雷达图组件,用于展示多维度对比数据
- 新增可编辑数值组件,支持双击编辑和范围验证
- 优化导出按钮,增加对比功能入口
- 新增搜索引擎枚举工具类
- 更新应用路由配置,添加对比和相生相克相关页面
This commit is contained in:
Developer
2026-05-01 09:13:27 +08:00
parent 4ec348b28e
commit 3a056ca676
69 changed files with 14597 additions and 13496 deletions

View File

@@ -0,0 +1,954 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>食物相生相克 - 查询</title>
<style>
:root {
--primary: #007AFF;
--primary-light: rgba(0,122,255,0.12);
--secondary: #FF9500;
--background: #F2F2F7;
--card: #FFFFFF;
--text1: #1C1C1E;
--text2: #8E8E93;
--text3: #C7C7CC;
--green: #34C759;
--red: #FF3B30;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-xl: 28px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-7: 32px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.1);
--font-xs: 11px;
--font-sm: 12px;
--font-md: 14px;
--font-lg: 16px;
--font-xl: 18px;
--font-xxl: 22px;
--glass-blur: 20px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', sans-serif;
background: var(--background);
color: var(--text1);
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.app-container {
max-width: 480px;
margin: 0 auto;
min-height: 100vh;
background: var(--background);
position: relative;
overflow-x: hidden;
}
.header {
position: sticky;
top: 0;
z-index: 100;
background: rgba(242,242,247,0.72);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border-bottom: 0.5px solid rgba(0,0,0,0.06);
padding: var(--space-3) var(--space-4);
}
.header-nav {
display: flex;
align-items: center;
gap: var(--space-3);
}
.back-btn {
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(0,0,0,0.04);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
font-size: 18px;
color: var(--primary);
transition: all 0.2s;
}
.back-btn:hover { background: rgba(0,0,0,0.08); }
.header-title {
font-size: var(--font-lg);
font-weight: 600;
flex: 1;
}
.header-action {
font-size: var(--font-sm);
color: var(--primary);
cursor: pointer;
font-weight: 500;
}
.content {
padding: var(--space-4);
padding-bottom: 100px;
}
.hero-card {
background: var(--card);
border-radius: var(--radius-xl);
padding: var(--space-6);
box-shadow: var(--shadow-md);
text-align: center;
margin-bottom: var(--space-4);
position: relative;
overflow: hidden;
}
.hero-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 120px;
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
}
.hero-card.xiangsheng::before {
background: linear-gradient(180deg, rgba(52,199,89,0.12), transparent);
}
.hero-card.xiangke::before {
background: linear-gradient(180deg, rgba(255,59,48,0.12), transparent);
}
.hero-emoji {
width: 88px;
height: 88px;
border-radius: var(--radius-xl);
display: flex;
align-items: center;
justify-content: center;
font-size: 44px;
margin: 0 auto var(--space-4);
position: relative;
}
.hero-emoji.xiangsheng {
background: linear-gradient(135deg, rgba(52,199,89,0.2), rgba(48,209,88,0.1));
box-shadow: 0 8px 24px rgba(52,199,89,0.2);
}
.hero-emoji.xiangke {
background: linear-gradient(135deg, rgba(255,59,48,0.2), rgba(255,107,107,0.1));
box-shadow: 0 8px 24px rgba(255,59,48,0.2);
}
.hero-name {
font-size: 28px;
font-weight: 700;
letter-spacing: -0.5px;
margin-bottom: var(--space-2);
position: relative;
}
.hero-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 14px;
font-size: var(--font-md);
font-weight: 600;
position: relative;
}
.hero-tag.xiangsheng { background: rgba(52,199,89,0.12); color: #34C759; }
.hero-tag.xiangke { background: rgba(255,59,48,0.12); color: #FF3B30; }
.hero-views {
margin-top: var(--space-3);
font-size: var(--font-sm);
color: var(--text3);
position: relative;
}
.info-section {
background: var(--card);
border-radius: var(--radius-lg);
padding: var(--space-5);
box-shadow: var(--shadow-sm);
margin-bottom: var(--space-4);
}
.info-section-title {
font-size: var(--font-md);
font-weight: 600;
color: var(--text2);
margin-bottom: var(--space-3);
display: flex;
align-items: center;
gap: var(--space-2);
}
.info-desc {
font-size: var(--font-md);
line-height: 1.8;
color: var(--text1);
padding: var(--space-4);
background: var(--background);
border-radius: var(--radius-md);
}
.related-list {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.related-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
background: var(--background);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
}
.related-item:hover { background: rgba(0,0,0,0.04); transform: translateX(4px); }
.related-emoji {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
background: var(--primary-light);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.related-info { flex: 1; min-width: 0; }
.related-name {
font-size: var(--font-md);
font-weight: 500;
}
.related-desc {
font-size: var(--font-xs);
color: var(--text2);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.related-arrow {
color: var(--text3);
font-size: 16px;
flex-shrink: 0;
}
.nav-bar {
display: flex;
gap: var(--space-3);
margin-top: var(--space-4);
}
.nav-btn {
flex: 1;
padding: var(--space-4);
border-radius: var(--radius-lg);
border: none;
background: var(--card);
box-shadow: var(--shadow-sm);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
font-size: var(--font-md);
font-weight: 500;
color: var(--primary);
transition: all 0.2s;
}
.nav-btn:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
.nav-btn:disabled { opacity: 0.4; cursor: default; transform: none; box-shadow: var(--shadow-sm); }
.action-bar {
display: flex;
gap: var(--space-3);
margin-top: var(--space-4);
}
.action-btn {
flex: 1;
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-lg);
border: none;
font-size: var(--font-md);
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
transition: all 0.2s;
}
.action-btn.copy {
background: var(--background);
color: var(--text1);
border: 1.5px solid rgba(0,0,0,0.06);
}
.action-btn.copy:hover { background: rgba(0,0,0,0.06); }
.action-btn.search {
background: linear-gradient(135deg, #007AFF, #5856D6);
color: white;
box-shadow: 0 4px 12px rgba(0,122,255,0.3);
}
.action-btn.search:hover { transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0,122,255,0.4); }
.action-btn.copied {
background: rgba(52,199,89,0.12);
color: #34C759;
border-color: rgba(52,199,89,0.2);
}
.engine-menu {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 300;
display: none;
align-items: flex-end;
justify-content: center;
}
.engine-menu.show { display: flex; }
.engine-sheet {
width: 100%;
max-width: 480px;
background: var(--card);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
padding: var(--space-2) 0 var(--space-7);
animation: slideUp 0.3s cubic-bezier(0.32,0.72,0,1);
}
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
.engine-handle { width: 36px; height: 5px; background: var(--text3); border-radius: 3px; margin: var(--space-2) auto var(--space-3); }
.engine-title { font-size: var(--font-lg); font-weight: 600; text-align: center; margin-bottom: var(--space-4); padding: 0 var(--space-4); }
.engine-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); padding: 0 var(--space-4); }
.engine-card { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-4); background: var(--background); border-radius: var(--radius-md); cursor: pointer; transition: all 0.2s; border: 1.5px solid transparent; }
.engine-card:hover { border-color: var(--primary); transform: scale(1.02); }
.engine-card-icon { width: 40px; height: 40px; border-radius: var(--radius-sm); background: var(--primary-light); display: flex; align-items: center; justify-content: center; font-size: 20px; }
.engine-card-name { font-size: var(--font-md); font-weight: 600; }
.engine-card-url { font-size: var(--font-xs); color: var(--text3); }
.search-section {
background: var(--card);
border-radius: var(--radius-lg);
padding: var(--space-5);
box-shadow: var(--shadow-sm);
margin-bottom: var(--space-4);
}
.search-row {
display: flex;
align-items: center;
gap: var(--space-3);
}
.search-input-wrap {
flex: 1;
display: flex;
align-items: center;
height: 44px;
background: rgba(0,0,0,0.04);
border-radius: var(--radius-md);
padding: 0 var(--space-3);
gap: var(--space-2);
}
.search-input-wrap input {
flex: 1;
border: none;
background: none;
font-size: var(--font-md);
color: var(--text1);
outline: none;
}
.search-input-wrap input::placeholder { color: var(--text3); }
.search-btn {
height: 44px;
padding: 0 var(--space-5);
border-radius: var(--radius-md);
border: none;
background: linear-gradient(135deg, #007AFF, #5856D6);
color: white;
font-size: var(--font-md);
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(0,122,255,0.3);
}
.search-btn:hover { transform: scale(1.03); }
.search-results {
margin-top: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.search-result-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3);
background: var(--background);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
}
.search-result-item:hover { background: rgba(0,0,0,0.04); }
.search-result-emoji {
width: 40px;
height: 40px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.search-result-emoji.xiangsheng { background: rgba(52,199,89,0.12); }
.search-result-emoji.xiangke { background: rgba(255,59,48,0.12); }
.search-result-info { flex: 1; min-width: 0; }
.search-result-name { font-size: var(--font-md); font-weight: 500; }
.search-result-desc { font-size: var(--font-xs); color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-result-arrow { color: var(--text3); font-size: 14px; }
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-7);
gap: var(--space-3);
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--text3);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-text { font-size: var(--font-sm); color: var(--text2); }
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-7);
gap: var(--space-3);
}
.empty-icon {
width: 80px;
height: 80px;
border-radius: var(--radius-xl);
background: var(--primary-light);
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
}
.empty-title { font-size: var(--font-lg); font-weight: 600; }
.empty-desc { font-size: var(--font-sm); color: var(--text2); text-align: center; line-height: 1.5; }
.param-hint {
background: var(--card);
border-radius: var(--radius-lg);
padding: var(--space-5);
box-shadow: var(--shadow-sm);
margin-bottom: var(--space-4);
}
.param-hint-title {
font-size: var(--font-md);
font-weight: 600;
margin-bottom: var(--space-3);
display: flex;
align-items: center;
gap: var(--space-2);
}
.param-hint-code {
background: var(--background);
border-radius: var(--radius-md);
padding: var(--space-3);
font-family: 'SF Mono', 'Menlo', monospace;
font-size: var(--font-sm);
line-height: 1.8;
color: var(--text2);
overflow-x: auto;
}
.param-hint-code .param { color: var(--primary); font-weight: 600; }
.param-hint-code .value { color: var(--green); }
@media (prefers-color-scheme: dark) {
:root {
--background: #000000;
--card: #1C1C1E;
--text1: #FFFFFF;
--text2: #EBEBF5;
--text3: #8E8E93;
--primary-light: rgba(10,132,255,0.18);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.2);
--shadow-md: 0 4px 12px rgba(0,0,0,0.3);
}
.header { background: rgba(0,0,0,0.72); border-bottom-color: rgba(255,255,255,0.06); }
.search-input-wrap { background: rgba(255,255,255,0.06); }
.info-desc, .related-item:hover, .search-result-item:hover { background: #2C2C2E; }
.param-hint-code { background: #2C2C2E; }
.engine-sheet { background: #1C1C1E; }
.engine-card { background: #2C2C2E; }
.action-btn.copy { background: #2C2C2E; border-color: rgba(255,255,255,0.06); color: var(--text1); }
.action-btn.copy:hover { background: #3A3A3C; }
}
</style>
</head>
<body>
<div class="app-container" id="app">
<div class="header">
<div class="header-nav">
<button class="back-btn" onclick="goBack()"></button>
<div class="header-title">食物相生相克查询</div>
<div class="header-action" onclick="goHome()">首页</div>
</div>
</div>
<div class="content" id="content">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</div>
<script>
const API_BASE = 'https://tools.wktyl.com/api/shiwu';
const foodEmojis = {
'苹果': '🍎', '香蕉': '🍌', '葡萄': '🍇', '草莓': '🍓', '西瓜': '🍉',
'橙子': '🍊', '柠檬': '🍋', '桃子': '🍑', '梨': '🍐', '芒果': '🥭',
'菠萝': '🍍', '樱桃': '🍒', '牛奶': '🥛', '鸡蛋': '🥚', '蜂蜜': '🍯',
'豆腐': '🧈', '萝卜': '🥕', '菠菜': '🥬', '番茄': '🍅', '土豆': '🥔',
'黄瓜': '🥒', '茄子': '🍆', '辣椒': '🌶️', '大蒜': '🧄', '洋葱': '🧅',
'蘑菇': '🍄', '芹菜': '🥦', '白菜': '🥬', '花生': '🥜', '核桃': '🌰',
'绿茶': '🍵', '咖啡': '☕', '啤酒': '🍺', '白酒': '🍶', '红酒': '🍷',
'虾': '🦐', '蟹': '🦀', '螃蟹': '🦀', '鱼': '🐟', '鸡肉': '🍗',
'牛肉': '🥩', '猪肉': '🥓', '羊肉': '🍖', '鸭肉': '🦆', '豆浆': '🥛',
'酸奶': '🥛', '奶酪': '🧀', '巧克力': '🍫', '醋': '🫗', '生姜': '🫚',
'红薯': '🍠', '山药': '🥔', '南瓜': '🎃', '木耳': '🍄', '银耳': '🍄',
'海带': '🌊', '芝麻': '🫘', '栗子': '🌰', '桂圆': '🔴', '山楂': '🔴',
'木瓜': '🟡', '榴莲': '🟡', '椰子': '🥥', '猕猴桃': '🥝', '火龙果': '🐉',
};
function getEmoji(name) {
for (const [key, val] of Object.entries(foodEmojis)) {
if (name.includes(key)) return val;
}
return '🍽️';
}
function getCategoryClass(name) { return name === '相生' ? 'xiangsheng' : 'xiangke'; }
function getCategoryIcon(name) { return name === '相生' ? '✅' : '⚠️'; }
async function fetchAPI(endpoint, params = {}) {
const url = new URL(`${API_BASE}/${endpoint}`);
Object.entries(params).forEach(([k, v]) => {
if (v !== null && v !== undefined && v !== '') url.searchParams.set(k, v);
});
try {
const resp = await fetch(url.toString());
const data = await resp.json();
if (data.code === 1) return data.data;
return null;
} catch (e) {
console.error('API Error:', e);
return null;
}
}
function goBack() {
if (document.referrer && document.referrer.includes(location.host)) {
history.back();
} else {
location.href = 'food_compatibility.html';
}
}
function goHome() {
location.href = 'food_compatibility.html';
}
function renderDetail(data) {
const catClass = getCategoryClass(data.category_name);
const content = document.getElementById('content');
content.innerHTML = `
<div class="hero-card ${catClass}">
<div class="hero-emoji ${catClass}">${getEmoji(data.sw)}</div>
<div class="hero-name">${data.sw}</div>
<div class="hero-tag ${catClass}">${getCategoryIcon(data.category_name)} ${data.category_name}</div>
<div class="hero-views">👁 ${data.views || 0} 次浏览</div>
</div>
<div class="info-section">
<div class="info-section-title">📝 搭配说明</div>
<div class="info-desc">${data.yh || '暂无说明'}</div>
</div>
${data.related && data.related.length > 0 ? `
<div class="info-section">
<div class="info-section-title">🔗 相关推荐</div>
<div class="related-list">
${data.related.map(r => `
<div class="related-item" onclick="loadById('${r.hash_id}')">
<div class="related-emoji">${getEmoji(r.sw)}</div>
<div class="related-info">
<div class="related-name">${r.sw}</div>
<div class="related-desc">${r.yh || ''}</div>
</div>
<div class="related-arrow"></div>
</div>
`).join('')}
</div>
</div>` : ''}
<div class="nav-bar">
<button class="nav-btn" ${!data.prev ? 'disabled' : ''} onclick="loadById('${data.prev ? data.prev.hash_id : ''}')">
${data.prev ? data.prev.sw : '无'}
</button>
<button class="nav-btn" ${!data.next ? 'disabled' : ''} onclick="loadById('${data.next ? data.next.hash_id : ''}')">
${data.next ? data.next.sw : '无'}
</button>
</div>
<div class="action-bar">
<button class="action-btn copy" id="copyBtn" onclick="copyFoodInfo('${data.sw}', \`${(data.yh || '').replace(/`/g, '\\`')}\`)">
📋 复制信息
</button>
<button class="action-btn search" onclick="showEngineMenu('${data.sw}')">
🔍 去搜索
</button>
</div>
<div class="search-section">
<div class="info-section-title">🔍 查询其他食物</div>
<div class="search-row">
<div class="search-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${getComputedStyle(document.documentElement).getPropertyValue('--text3').trim()}" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="searchInput" placeholder="输入食物名称..." />
</div>
<button class="search-btn" onclick="doSearch()">搜索</button>
</div>
<div class="search-results" id="searchResults"></div>
</div>
`;
document.getElementById('searchInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doSearch();
});
}
function renderSearchMode(keyword) {
const content = document.getElementById('content');
content.innerHTML = `
<div class="search-section">
<div class="info-section-title">🔍 搜索 "${keyword}"</div>
<div class="search-row">
<div class="search-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${getComputedStyle(document.documentElement).getPropertyValue('--text3').trim()}" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="searchInput" placeholder="输入食物名称..." value="${keyword}" />
</div>
<button class="search-btn" onclick="doSearch()">搜索</button>
</div>
<div class="search-results" id="searchResults">
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">搜索中...</div>
</div>
</div>
</div>
<div class="param-hint">
<div class="param-hint-title">💡 接口说明</div>
<div class="param-hint-code">
其他页面可通过 URL 参数传递食物名称:<br>
<span class="param">?keyword=</span><span class="value">苹果</span><br>
<span class="param">?keyword=</span><span class="value">牛奶+蜂蜜</span><br><br>
或通过食物 ID 直接查看详情:<br>
<span class="param">?id=</span><span class="value">16o7v5</span>
</div>
</div>
`;
document.getElementById('searchInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doSearch();
});
performSearch(keyword);
}
function renderEmpty() {
const content = document.getElementById('content');
content.innerHTML = `
<div class="empty-state">
<div class="empty-icon">🥗</div>
<div class="empty-title">食物相生相克查询</div>
<div class="empty-desc">请通过 URL 参数传递食物名称<br>或使用下方搜索框查询</div>
</div>
<div class="search-section">
<div class="info-section-title">🔍 搜索食物</div>
<div class="search-row">
<div class="search-input-wrap">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="${getComputedStyle(document.documentElement).getPropertyValue('--text3').trim()}" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
<input type="text" id="searchInput" placeholder="输入食物名称..." />
</div>
<button class="search-btn" onclick="doSearch()">搜索</button>
</div>
<div class="search-results" id="searchResults"></div>
</div>
<div class="param-hint">
<div class="param-hint-title">💡 接口说明</div>
<div class="param-hint-code">
其他页面可通过 URL 参数传递食物名称:<br>
<span class="param">?keyword=</span><span class="value">苹果</span><br>
<span class="param">?keyword=</span><span class="value">牛奶+蜂蜜</span><br><br>
或通过食物 ID 直接查看详情:<br>
<span class="param">?id=</span><span class="value">16o7v5</span><br><br>
Flutter 页面调用示例:<br>
Get.toNamed(<span class="value">'/tools/food-compat'</span>, parameters: {'keyword': '苹果'});
</div>
</div>
`;
document.getElementById('searchInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doSearch();
});
}
async function loadById(id) {
const content = document.getElementById('content');
content.innerHTML = `
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">加载详情...</div>
</div>`;
const data = await fetchAPI('detail', { id });
if (!data) {
content.innerHTML = `
<div class="empty-state">
<div class="empty-icon">😔</div>
<div class="empty-title">加载失败</div>
<div class="empty-desc">食物不存在或网络错误</div>
</div>`;
return;
}
history.pushState(null, '', `?id=${data.hash_id}`);
renderDetail(data);
}
async function performSearch(keyword) {
const resultsEl = document.getElementById('searchResults');
if (!resultsEl) return;
resultsEl.innerHTML = `
<div class="loading-container">
<div class="loading-spinner"></div>
<div class="loading-text">搜索中...</div>
</div>`;
const data = await fetchAPI('search', { keyword, limit: 20 });
if (!data || !data.list || data.list.length === 0) {
resultsEl.innerHTML = `
<div class="empty-state" style="padding: var(--space-5)">
<div class="empty-icon" style="width:56px;height:56px;font-size:24px">🔍</div>
<div class="empty-title" style="font-size:var(--font-md)">未找到"${keyword}"</div>
<div class="empty-desc">试试其他关键词</div>
</div>`;
return;
}
resultsEl.innerHTML = '';
data.list.forEach(item => {
const catClass = getCategoryClass(item.category_name);
const el = document.createElement('div');
el.className = 'search-result-item';
el.onclick = () => loadById(item.hash_id);
el.innerHTML = `
<div class="search-result-emoji ${catClass}">${getEmoji(item.sw)}</div>
<div class="search-result-info">
<div class="search-result-name">${item.sw}</div>
<div class="search-result-desc">${getCategoryIcon(item.category_name)} ${item.category_name} · ${item.yh || ''}</div>
</div>
<div class="search-result-arrow"></div>`;
resultsEl.appendChild(el);
});
}
function doSearch() {
const input = document.getElementById('searchInput');
if (!input || !input.value.trim()) return;
const keyword = input.value.trim();
history.pushState(null, '', `?keyword=${encodeURIComponent(keyword)}`);
performSearch(keyword);
}
async function init() {
const urlParams = new URLSearchParams(window.location.search);
const keyword = urlParams.get('keyword');
const id = urlParams.get('id');
if (id) {
await loadById(id);
} else if (keyword) {
renderSearchMode(keyword);
} else {
renderEmpty();
}
}
window.addEventListener('popstate', init);
init();
let _searchKeyword = '';
function copyFoodInfo(name, desc) {
const text = `${name}】食物相生相克\n${desc}`;
navigator.clipboard.writeText(text).then(() => {
const btn = document.getElementById('copyBtn');
if (btn) {
btn.classList.add('copied');
btn.innerHTML = '✅ 已复制';
setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = '📋 复制信息'; }, 2000);
}
}).catch(() => {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
const btn = document.getElementById('copyBtn');
if (btn) {
btn.classList.add('copied');
btn.innerHTML = '✅ 已复制';
setTimeout(() => { btn.classList.remove('copied'); btn.innerHTML = '📋 复制信息'; }, 2000);
}
});
}
function showEngineMenu(keyword) {
_searchKeyword = keyword;
document.getElementById('engineMenu').classList.add('show');
}
function closeEngineOutside(e) {
if (e.target === document.getElementById('engineMenu')) {
document.getElementById('engineMenu').classList.remove('show');
}
}
function launchEngine(engine) {
const keyword = _searchKeyword;
const encoded = encodeURIComponent(keyword + ' 相生相克');
const urls = {
baidu: `https://www.baidu.com/s?wd=${encoded}`,
bing: `https://www.bing.com/search?q=${encoded}`,
google: `https://www.google.com/search?q=${encoded}`,
sogou: `https://www.sogou.com/web?query=${encoded}`,
};
window.open(urls[engine] || urls.baidu, '_blank');
document.getElementById('engineMenu').classList.remove('show');
}
</script>
<!-- Engine Menu -->
<div class="engine-menu" id="engineMenu" onclick="closeEngineOutside(event)">
<div class="engine-sheet">
<div class="engine-handle"></div>
<div class="engine-title">🔍 选择搜索引擎</div>
<div class="engine-grid">
<div class="engine-card" onclick="launchEngine('baidu')">
<div class="engine-card-icon">🔍</div>
<div><div class="engine-card-name">百度</div><div class="engine-card-url">baidu.com</div></div>
</div>
<div class="engine-card" onclick="launchEngine('bing')">
<div class="engine-card-icon">🟦</div>
<div><div class="engine-card-name">Bing</div><div class="engine-card-url">bing.com</div></div>
</div>
<div class="engine-card" onclick="launchEngine('google')">
<div class="engine-card-icon">🌐</div>
<div><div class="engine-card-name">谷歌</div><div class="engine-card-url">google.com</div></div>
</div>
<div class="engine-card" onclick="launchEngine('sogou')">
<div class="engine-card-icon">🐕</div>
<div><div class="engine-card-name">搜狗</div><div class="engine-card-url">sogou.com</div></div>
</div>
</div>
</div>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff