Spaces:
Sleeping
Sleeping
<html lang="zh-TW"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AI食物營養分析器 - 個人化版本</title> | |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
<style> | |
/* 全局樣式 */ | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
/* 追蹤紀錄專用樣式 */ | |
.tracking-container { | |
padding: 1.5rem; | |
max-width: 1200px; | |
margin: 0 auto; | |
} | |
/* 統計卡片網格 */ | |
.stats-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
gap: 1rem; | |
margin-bottom: 2rem; | |
} | |
.stat-card { | |
background: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
display: flex; | |
align-items: center; | |
transition: transform 0.2s, box-shadow 0.2s; | |
} | |
.stat-card:hover { | |
transform: translateY(-3px); | |
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); | |
} | |
.stat-icon { | |
font-size: 2rem; | |
margin-right: 1rem; | |
width: 50px; | |
height: 50px; | |
border-radius: 50%; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
background: #f5f7fa; | |
} | |
.stat-info { | |
flex: 1; | |
} | |
.stat-value { | |
font-size: 1.5rem; | |
font-weight: 700; | |
color: #2c3e50; | |
margin-bottom: 0.25rem; | |
} | |
.stat-label { | |
font-size: 0.875rem; | |
color: #7f8c8d; | |
} | |
/* 圖表卡片 */ | |
.chart-card { | |
background: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
margin-bottom: 1.5rem; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
} | |
.chart-card.full-width { | |
grid-column: 1 / -1; | |
} | |
.chart-header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
} | |
.chart-header h3 { | |
font-size: 1.25rem; | |
color: #2c3e50; | |
margin: 0; | |
} | |
/* 進度條樣式 */ | |
.progress-bars { | |
display: grid; | |
gap: 1.25rem; | |
} | |
.progress-item { | |
margin-bottom: 1rem; | |
} | |
.progress-header { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 0.5rem; | |
font-size: 0.875rem; | |
} | |
.progress-label { | |
font-weight: 600; | |
color: #2c3e50; | |
} | |
.progress-value { | |
color: #7f8c8d; | |
} | |
.progress-bar-container { | |
height: 8px; | |
background: #f0f2f5; | |
border-radius: 4px; | |
overflow: hidden; | |
} | |
.progress-bar-fill { | |
height: 100%; | |
border-radius: 4px; | |
transition: width 0.5s ease; | |
} | |
/* 食物記錄 */ | |
.meals-card { | |
background: white; | |
border-radius: 12px; | |
padding: 1.5rem; | |
margin-bottom: 1.5rem; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); | |
} | |
.meals-header { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
} | |
.meals-header h3 { | |
margin: 0; | |
font-size: 1.25rem; | |
color: #2c3e50; | |
} | |
.btn-small { | |
background: #3498db; | |
color: white; | |
border: none; | |
padding: 0.5rem 1rem; | |
border-radius: 6px; | |
font-size: 0.875rem; | |
font-weight: 500; | |
cursor: pointer; | |
transition: background 0.2s; | |
display: inline-flex; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.btn-small:hover { | |
background: #2980b9; | |
} | |
.meals-list { | |
display: grid; | |
gap: 1rem; | |
} | |
.empty-state { | |
text-align: center; | |
padding: 3rem 1rem; | |
color: #7f8c8d; | |
} | |
.empty-icon { | |
font-size: 3rem; | |
margin-bottom: 1rem; | |
opacity: 0.7; | |
} | |
.empty-state p { | |
margin: 0.5rem 0; | |
} | |
.text-muted { | |
color: #bdc3c7; | |
font-size: 0.875rem; | |
} | |
/* 主體樣式 */ | |
body { | |
font-family: 'Arial', sans-serif; | |
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
padding: 1rem; | |
} | |
.app-container { | |
background: rgba(255, 255, 255, 0.95); | |
border-radius: 20px; | |
padding: 2rem; | |
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); | |
max-width: 500px; | |
width: 100%; | |
max-height: 90vh; | |
overflow-y: auto; | |
} | |
.header { | |
text-align: center; | |
margin-bottom: 2rem; | |
} | |
.title { | |
color: #333; | |
font-size: 1.8rem; | |
font-weight: bold; | |
margin-bottom: 0.5rem; | |
} | |
.subtitle { | |
color: #666; | |
font-size: 0.9rem; | |
} | |
.tab-nav { | |
display: flex; | |
background: rgba(102, 126, 234, 0.1); | |
border-radius: 15px; | |
margin-bottom: 2rem; | |
padding: 0.3rem; | |
} | |
.tab-btn { | |
flex: 1; | |
padding: 0.8rem; | |
border: none; | |
background: transparent; | |
border-radius: 12px; | |
font-weight: bold; | |
cursor: pointer; | |
transition: all 0.3s; | |
color: #666; | |
} | |
.tab-btn.active { | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
color: white; | |
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); | |
} | |
.tab-content { | |
display: none; | |
} | |
.tab-content.active { | |
display: block; | |
} | |
/* 個人資料表單 */ | |
.profile-form { | |
display: grid; | |
gap: 1.5rem; | |
} | |
.input-group { | |
display: flex; | |
flex-direction: column; | |
} | |
.input-group label { | |
font-weight: bold; | |
color: #333; | |
margin-bottom: 0.5rem; | |
font-size: 0.9rem; | |
} | |
.input-group input, .input-group select { | |
padding: 12px; | |
border: 2px solid #e0e0e0; | |
border-radius: 10px; | |
font-size: 1rem; | |
transition: border-color 0.3s; | |
} | |
.input-group input:focus, .input-group select:focus { | |
outline: none; | |
border-color: #667eea; | |
} | |
.input-row { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 1rem; | |
} | |
.save-profile-btn { | |
background: linear-gradient(45deg, #43cea2, #185a9d); | |
color: white; | |
border: none; | |
padding: 15px; | |
border-radius: 15px; | |
font-weight: bold; | |
cursor: pointer; | |
font-size: 1rem; | |
transition: transform 0.2s; | |
} | |
.save-profile-btn:hover { | |
transform: translateY(-2px); | |
} | |
/* 食物分析區域 */ | |
.camera-container { | |
position: relative; | |
margin-bottom: 1.5rem; | |
text-align: center; | |
} | |
#video { | |
width: 100%; | |
max-width: 300px; | |
border-radius: 15px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); | |
background-color: #000; | |
} | |
#canvas { | |
display: none; | |
} | |
.controls { | |
display: flex; | |
gap: 1rem; | |
justify-content: center; | |
margin: 1.5rem 0; | |
flex-wrap: wrap; | |
} | |
button { | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
color: white; | |
border: none; | |
padding: 12px 20px; | |
border-radius: 25px; | |
cursor: pointer; | |
font-weight: bold; | |
transition: transform 0.2s; | |
font-size: 0.9rem; | |
} | |
button:hover { | |
transform: translateY(-2px); | |
} | |
button:disabled { | |
opacity: 0.6; | |
cursor: not-allowed; | |
} | |
.upload-btn { | |
background: linear-gradient(45deg, #43cea2, #185a9d); | |
} | |
.file-input { | |
display: none; | |
} | |
.loading { | |
display: none; | |
flex-direction: column; | |
align-items: center; | |
justify-content: center; | |
margin: 1rem 0; | |
text-align: center; | |
} | |
.spinner { | |
border: 3px solid #f3f3f3; | |
border-top: 3px solid #667eea; | |
border-radius: 50%; | |
width: 30px; | |
height: 30px; | |
animation: spin 1s linear infinite; | |
margin: 0 auto 1rem; | |
} | |
@keyframes spin { | |
0% { transform: rotate(0deg); } | |
100% { transform: rotate(360deg); } | |
} | |
.result { | |
margin-top: 2rem; | |
padding: 1.5rem; | |
background: rgba(102, 126, 234, 0.1); | |
border-radius: 15px; | |
display: none; | |
} | |
.food-info { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
} | |
.food-name { | |
font-size: 1.3rem; | |
font-weight: bold; | |
color: #333; | |
} | |
.add-to-diary { | |
background: linear-gradient(45deg, #ff6b6b, #ee5a24); | |
padding: 8px 16px; | |
border-radius: 20px; | |
font-size: 0.8rem; | |
} | |
.nutrition-grid { | |
display: grid; | |
grid-template-columns: 1fr 1fr; | |
gap: 0.8rem; | |
margin-bottom: 1.5rem; | |
} | |
.nutrition-item { | |
background: white; | |
padding: 0.8rem; | |
border-radius: 10px; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
text-align: center; | |
} | |
.nutrition-label { | |
font-size: 0.9rem; | |
color: #666; | |
margin-bottom: 0.3rem; | |
} | |
.nutrition-value { | |
font-size: 1.1rem; | |
font-weight: bold; | |
color: #333; | |
} | |
.daily-recommendation { | |
background: linear-gradient(45deg, rgba(255, 107, 107, 0.1), rgba(238, 90, 36, 0.1)); | |
padding: 1rem; | |
border-radius: 10px; | |
border-left: 4px solid #ff6b6b; | |
margin-top: 1.5rem; | |
} | |
.recommendation-title { | |
font-weight: bold; | |
color: #333; | |
margin-bottom: 0.5rem; | |
font-size: 0.9rem; | |
} | |
.recommendation-text { | |
font-size: 0.8rem; | |
color: #666; | |
line-height: 1.4; | |
} | |
/* 健康指數樣式 */ | |
.health-indices { | |
display: flex; | |
gap: 1rem; | |
margin: 1rem 0 1.5rem; | |
} | |
.health-index { | |
flex: 1; | |
background: white; | |
padding: 0.8rem; | |
border-radius: 10px; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
} | |
.index-label { | |
font-size: 0.8rem; | |
color: #666; | |
margin-bottom: 0.5rem; | |
} | |
.index-meter { | |
height: 6px; | |
background: #f0f0f0; | |
border-radius: 3px; | |
margin-bottom: 0.5rem; | |
overflow: hidden; | |
} | |
.meter-fill { | |
height: 100%; | |
width: 0%; | |
border-radius: 3px; | |
transition: width 0.5s ease; | |
} | |
#healthIndexFill { | |
background: linear-gradient(45deg, #43cea2, #185a9d); | |
} | |
#glycemicIndexFill { | |
background: linear-gradient(45deg, #ff9a9e, #fad0c4); | |
} | |
.index-value { | |
font-size: 0.8rem; | |
font-weight: bold; | |
color: #333; | |
text-align: right; | |
} | |
.food-description { | |
background: rgba(102, 126, 234, 0.1); | |
padding: 1rem; | |
border-radius: 10px; | |
font-size: 0.9rem; | |
color: #555; | |
line-height: 1.5; | |
margin-bottom: 1rem; | |
} | |
.benefits-tags { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 0.5rem; | |
margin-bottom: 1.5rem; | |
} | |
.benefit-tag { | |
background: rgba(67, 206, 162, 0.1); | |
color: #43cea2; | |
padding: 0.3rem 0.8rem; | |
border-radius: 20px; | |
font-size: 0.8rem; | |
font-weight: 500; | |
display: inline-block; | |
} | |
.nutrition-section-title { | |
color: #333; | |
font-size: 1rem; | |
margin: 1.5rem 0 0.8rem; | |
} | |
.nutrition-details { | |
display: grid; | |
grid-template-columns: 1fr; | |
gap: 1rem; | |
margin-top: 1.5rem; | |
} | |
.nutrition-section { | |
background: white; | |
padding: 1rem; | |
border-radius: 10px; | |
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
} | |
/* 統計卡片樣式修正 */ | |
.water-card { | |
background: linear-gradient(135deg, #e0f7fa, #b2ebf2); | |
} | |
.water-card .stat-number { | |
display: flex; | |
flex-direction: column; | |
align-items: center; | |
gap: 0.5rem; | |
} | |
.water-controls { | |
display: flex; | |
gap: 0.5rem; | |
margin-top: 0.5rem; | |
justify-content: center; | |
} | |
.water-btn { | |
background: rgba(255, 255, 255, 0.7); | |
border: none; | |
border-radius: 5px; | |
padding: 3px 8px; | |
font-size: 0.75rem; | |
color: #0288d1; | |
cursor: pointer; | |
transition: all 0.2s; | |
} | |
.water-btn:hover { | |
background: #0288d1; | |
color: white; | |
} | |
.stat-number { | |
font-size: 2rem; | |
font-weight: bold; | |
color: #667eea; | |
margin-bottom: 0.5rem; | |
} | |
/* 圖表容器 */ | |
.chart-container { | |
background: white; | |
padding: 1.5rem; | |
border-radius: 15px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
margin-bottom: 2rem; | |
} | |
.chart-tabs { | |
display: flex; | |
background: #f5f5f5; | |
border-radius: 20px; | |
padding: 0.3rem; | |
} | |
.chart-tab { | |
background: transparent; | |
border: none; | |
padding: 0.5rem 1rem; | |
border-radius: 15px; | |
font-size: 0.8rem; | |
color: #666; | |
cursor: pointer; | |
transition: all 0.3s; | |
} | |
.chart-tab.active { | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
color: white; | |
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); | |
} | |
.chart-view { | |
display: none; | |
height: 250px; | |
} | |
.chart-view.active { | |
display: block; | |
} | |
/* 目標達成進度樣式 */ | |
.goals-container { | |
background: white; | |
padding: 1.5rem; | |
border-radius: 15px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
margin-bottom: 2rem; | |
} | |
.goal-items { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 1.2rem; | |
} | |
.goal-item { | |
background: #f8f9fa; | |
padding: 1rem; | |
border-radius: 10px; | |
} | |
.goal-info { | |
display: flex; | |
justify-content: space-between; | |
margin-bottom: 0.8rem; | |
} | |
.goal-label { | |
font-weight: 500; | |
color: #333; | |
} | |
.goal-value { | |
color: #666; | |
font-size: 0.9rem; | |
} | |
.goal-progress { | |
height: 8px; | |
background: #e9ecef; | |
border-radius: 4px; | |
overflow: hidden; | |
} | |
.progress-bar { | |
height: 100%; | |
width: 0%; | |
border-radius: 4px; | |
transition: width 0.5s ease; | |
} | |
#caloriesProgressBar { | |
background: linear-gradient(45deg, #ff9a9e, #fad0c4); | |
} | |
#proteinProgressBar { | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
} | |
#waterProgressBar { | |
background: linear-gradient(45deg, #4facfe, #00f2fe); | |
} | |
#fiberProgressBar { | |
background: linear-gradient(45deg, #43cea2, #185a9d); | |
} | |
.today-meals { | |
background: white; | |
padding: 1.5rem; | |
border-radius: 15px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
} | |
.meal-item { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
padding: 1rem; | |
border-bottom: 1px solid #f0f0f0; | |
} | |
.meal-item:last-child { | |
border-bottom: none; | |
} | |
.meal-info h4 { | |
color: #333; | |
margin-bottom: 0.3rem; | |
} | |
.meal-info span { | |
color: #666; | |
font-size: 0.8rem; | |
} | |
.meal-calories { | |
font-weight: bold; | |
color: #667eea; | |
} | |
.welcome-message { | |
background: linear-gradient(45deg, rgba(67, 206, 162, 0.1), rgba(24, 90, 157, 0.1)); | |
padding: 1rem; | |
border-radius: 10px; | |
margin-bottom: 2rem; | |
border-left: 4px solid #43cea2; | |
} | |
.no-profile { | |
text-align: center; | |
color: #666; | |
padding: 2rem; | |
} | |
/* 每日總結樣式 */ | |
.daily-summary { | |
background: white; | |
padding: 1.5rem; | |
border-radius: 15px; | |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
margin-bottom: 2rem; | |
} | |
.daily-summary h3 { | |
color: #333; | |
margin-bottom: 1.2rem; | |
font-size: 1.2rem; | |
} | |
.nutrition-balance { | |
display: flex; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
padding-bottom: 1.5rem; | |
border-bottom: 1px solid #f0f0f0; | |
} | |
.balance-chart { | |
flex: 0 0 150px; | |
} | |
.balance-stats { | |
flex: 1; | |
padding-left: 1.5rem; | |
} | |
.balance-item { | |
display: flex; | |
align-items: center; | |
margin-bottom: 0.8rem; | |
} | |
.balance-label { | |
flex: 1; | |
color: #666; | |
font-size: 0.9rem; | |
} | |
.balance-value { | |
font-weight: bold; | |
color: #333; | |
margin-right: 1rem; | |
} | |
.balance-percent { | |
background: #f0f0f0; | |
padding: 0.2rem 0.5rem; | |
border-radius: 10px; | |
font-size: 0.8rem; | |
color: #666; | |
min-width: 45px; | |
text-align: center; | |
} | |
.nutrition-score { | |
display: flex; | |
align-items: center; | |
} | |
.score-circle { | |
width: 80px; | |
height: 80px; | |
border-radius: 50%; | |
background: linear-gradient(45deg, #667eea, #764ba2); | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
margin-right: 1.5rem; | |
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.3); | |
} | |
.score-number { | |
color: white; | |
font-size: 2rem; | |
font-weight: bold; | |
} | |
.score-details { | |
flex: 1; | |
} | |
.score-details h4 { | |
color: #333; | |
margin-bottom: 0.3rem; | |
font-size: 1.1rem; | |
} | |
.score-details p { | |
color: #666; | |
font-size: 0.85rem; | |
line-height: 1.4; | |
} | |
.summary-icon { | |
font-size: 1.5rem; | |
margin-right: 0.5rem; | |
} | |
.summary-item { | |
display: flex; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
} | |
.summary-title { | |
font-size: 1rem; | |
color: #333; | |
margin-bottom: 0.2rem; | |
} | |
.summary-value { | |
font-size: 1.2rem; | |
font-weight: bold; | |
color: #667eea; | |
} | |
.water-input { | |
display: flex; | |
flex-direction: column; | |
align-items: flex-start; | |
} | |
/* 響應式設計 */ | |
@media (max-width: 768px) { | |
.chart-header { | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 1rem; | |
} | |
.meals-header { | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 1rem; | |
} | |
.btn-small { | |
width: 100%; | |
justify-content: center; | |
} | |
.nutrition-balance { | |
flex-direction: column; | |
text-align: center; | |
} | |
.nutrition-details { | |
grid-template-columns: 1fr; | |
} | |
.balance-stats { | |
padding-left: 0; | |
padding-top: 1rem; | |
} | |
} | |
@media (max-width: 480px) { | |
.stats-grid { | |
grid-template-columns: 1fr; | |
} | |
.tracking-container { | |
padding: 1rem; | |
} | |
.chart-card, .meals-card, .goals-container { | |
padding: 1rem; | |
} | |
.nutrition-details { | |
grid-template-columns: 1fr; | |
} | |
.goal-items { | |
grid-template-columns: 1fr; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="app-container"> | |
<div class="header"> | |
<h1 class="title">🍎 AI食物營養分析器</h1> | |
<p class="subtitle">智能分析您的飲食營養</p> | |
</div> | |
<div class="tab-nav"> | |
<button class="tab-btn active" data-tab="analyzer" onclick="switchTab('analyzer')">分析食物</button> | |
<button class="tab-btn" data-tab="profile" onclick="switchTab('profile')">個人資料</button> | |
<button class="tab-btn" data-tab="tracking" onclick="switchTab('tracking')">營養追蹤</button> | |
</div> | |
<!-- 食物分析頁面 --> | |
<div id="analyzer" class="tab-content active"> | |
<div id="profileCheck" class="no-profile" style="display: none;"> | |
<p>請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆</p> | |
</div> | |
<div id="analyzerContent"> | |
<div class="camera-container"> | |
<video id="video" autoplay playsinline></video> | |
<canvas id="canvas"></canvas> | |
</div> | |
<div class="controls"> | |
<button onclick="startCamera()">📷 開啟相機</button> | |
<button onclick="capturePhoto()">📸 拍照分析</button> | |
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">📁 上傳圖片</button> | |
<input type="file" id="fileInput" class="file-input" accept="image/*" onchange="handleFileUpload(event)"> | |
</div> | |
<div class="loading" id="loading"> | |
<div class="spinner"></div> | |
<p>AI正在分析食物中...</p> | |
</div> | |
<div class="result" id="result"> | |
<div class="food-info"> | |
<h3 class="food-name" id="foodName">識別的食物</h3> | |
<button class="add-to-diary" onclick="addToFoodDiary()">加入飲食記錄</button> | |
</div> | |
<div class="health-indices"> | |
<div class="health-index"> | |
<div class="index-label">健康指數</div> | |
<div class="index-meter"> | |
<div class="meter-fill" id="healthIndexFill"></div> | |
</div> | |
<div class="index-value" id="healthIndexValue">--/100</div> | |
</div> | |
<div class="health-index"> | |
<div class="index-label">升糖指數</div> | |
<div class="index-meter"> | |
<div class="meter-fill" id="glycemicIndexFill"></div> | |
</div> | |
<div class="index-value" id="glycemicIndexValue">--/100</div> | |
</div> | |
</div> | |
<p class="food-description" id="foodDescription">這裡會顯示食物的詳細描述和營養價值。</p> | |
<div class="benefits-tags" id="benefitsTags"></div> | |
<div class="nutrition-details"> | |
<div class="nutrition-section"> | |
<h4 class="nutrition-section-title">基本營養素</h4> | |
<div class="nutrition-grid" id="nutritionGrid"></div> | |
</div> | |
<div class="nutrition-section"> | |
<h4 class="nutrition-section-title">維生素</h4> | |
<div class="nutrition-grid" id="vitaminsGrid"></div> | |
</div> | |
<div class="nutrition-section"> | |
<h4 class="nutrition-section-title">礦物質</h4> | |
<div class="nutrition-grid" id="mineralsGrid"></div> | |
</div> | |
</div> | |
<div class="daily-recommendation" id="recommendation"></div> | |
</div> | |
</div> | |
</div> | |
<!-- 個人資料頁面 --> | |
<div id="profile" class="tab-content"> | |
<div class="profile-form"> | |
<div class="input-group"> | |
<label for="userName">姓名</label> | |
<input type="text" id="userName" placeholder="請輸入您的姓名"> | |
</div> | |
<div class="input-row"> | |
<div class="input-group"> | |
<label for="userAge">年齡</label> | |
<input type="number" id="userAge" placeholder="歲" min="1" max="120"> | |
</div> | |
<div class="input-group"> | |
<label for="userGender">性別</label> | |
<select id="userGender"> | |
<option value="">請選擇</option> | |
<option value="male">男性</option> | |
<option value="female">女性</option> | |
</select> | |
</div> | |
</div> | |
<div class="input-row"> | |
<div class="input-group"> | |
<label for="userHeight">身高 (cm)</label> | |
<input type="number" id="userHeight" placeholder="公分" min="100" max="250"> | |
</div> | |
<div class="input-group"> | |
<label for="userWeight">體重 (kg)</label> | |
<input type="number" id="userWeight" placeholder="公斤" min="30" max="200"> | |
</div> | |
</div> | |
<div class="input-group"> | |
<label for="activityLevel">活動量</label> | |
<select id="activityLevel"> | |
<option value="sedentary">久坐少動 (辦公室工作)</option> | |
<option value="light">輕度活動 (每週運動1-3次)</option> | |
<option value="moderate">中度活動 (每週運動3-5次)</option> | |
<option value="active">高度活動 (每週運動6-7次)</option> | |
<option value="extra">超高活動 (體力勞動+運動)</option> | |
</select> | |
</div> | |
<div class="input-group"> | |
<label for="healthGoal">健康目標</label> | |
<select id="healthGoal"> | |
<option value="lose">減重</option> | |
<option value="maintain">維持體重</option> | |
<option value="gain">增重</option> | |
<option value="muscle">增肌</option> | |
<option value="health">保持健康</option> | |
</select> | |
</div> | |
<button class="save-profile-btn" onclick="saveUserProfile()">💾 儲存資料</button> | |
</div> | |
</div> | |
<!-- 營養追蹤頁面 --> | |
<div id="tracking" class="tab-content"> | |
<div id="trackingCheck" class="no-profile" style="display: none;"> | |
<p>請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆</p> | |
</div> | |
<div class="tracking-container" id="trackingContent"> | |
<div class="welcome-message" id="welcomeMessage"></div> | |
<div class="stats-grid" id="statsGrid"> | |
<div class="stat-card"> | |
<div class="stat-icon">🔥</div> | |
<div class="stat-info"> | |
<div class="stat-value" id="todayCalories">0</div> | |
<div class="stat-label">今日攝取熱量</div> | |
</div> | |
</div> | |
<div class="stat-card"> | |
<div class="stat-icon">🎯</div> | |
<div class="stat-info"> | |
<div class="stat-value" id="calorieGoalText">0</div> | |
<div class="stat-label">每日目標熱量</div> | |
</div> | |
</div> | |
</div> | |
<div class="goals-container" id="goalsContainer"> | |
<h3>今日目標進度</h3> | |
<div class="goal-items" id="goalItems"></div> | |
</div> | |
<div class="meals-card"> | |
<div class="meals-header"> | |
<h3>今日食物記錄</h3> | |
<button class="btn-small" onclick="switchTab('analyzer')">+ 新增餐點</button> | |
</div> | |
<div id="mealsList" class="meals-list"></div> | |
</div> | |
<div class="chart-card full-width"> | |
<div class="chart-header"> | |
<h3>本週熱量趨勢</h3> | |
</div> | |
<div class="chart-container"> | |
<canvas id="weeklyChart"></canvas> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// ================================================================================= | |
// Application State and Data | |
// ================================================================================= | |
const appState = { | |
userProfile: null, | |
foodDiary: [], | |
currentAnalysis: null, | |
charts: {} | |
}; | |
const API_BASE_URL = 'http://127.0.0.1:8000'; // Backend server address | |
// ================================================================================= | |
// Initialization | |
// ================================================================================= | |
document.addEventListener('DOMContentLoaded', initializeApp); | |
function initializeApp() { | |
loadUserProfile(); | |
loadFoodDiary(); | |
switchTab('analyzer'); // Start on analyzer tab | |
} | |
// ================================================================================= | |
// Tab & UI Management | |
// ================================================================================= | |
function switchTab(tabId) { | |
// Hide all tab contents | |
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); | |
// Deactivate all tab buttons | |
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); | |
// Activate the selected tab and button | |
document.getElementById(tabId).classList.add('active'); | |
document.querySelector(`.tab-btn[data-tab="${tabId}"]`).classList.add('active'); | |
// Conditional UI updates based on tab and profile status | |
const hasProfile = !!appState.userProfile; | |
document.getElementById('profileCheck').style.display = !hasProfile && tabId === 'analyzer' ? 'block' : 'none'; | |
document.getElementById('analyzerContent').style.display = hasProfile && tabId === 'analyzer' ? 'block' : 'none'; | |
document.getElementById('trackingCheck').style.display = !hasProfile && tabId === 'tracking' ? 'block' : 'none'; | |
document.getElementById('trackingContent').style.display = hasProfile && tabId === 'tracking' ? 'block' : 'none'; | |
if (hasProfile) { | |
if (tabId === 'profile') { | |
populateProfileForm(); | |
} else if (tabId === 'tracking') { | |
updateTrackingPage(); | |
} | |
} | |
} | |
function showLoading(isLoading) { | |
document.getElementById('loading').style.display = isLoading ? 'flex' : 'none'; | |
} | |
function showNotification(message, type = 'info') { | |
alert(`[${type.toUpperCase()}] ${message}`); | |
} | |
// ================================================================================= | |
// Profile Management | |
// ================================================================================= | |
function saveUserProfile() { | |
const profile = { | |
name: document.getElementById('userName').value, | |
age: parseInt(document.getElementById('userAge').value), | |
gender: document.getElementById('userGender').value, | |
height: parseInt(document.getElementById('userHeight').value), | |
weight: parseInt(document.getElementById('userWeight').value), | |
activityLevel: document.getElementById('activityLevel').value, | |
healthGoal: document.getElementById('healthGoal').value | |
}; | |
if (Object.values(profile).some(v => !v)) { | |
return showNotification('請填寫所有欄位!', 'error'); | |
} | |
// Calculate BMR and daily calorie needs | |
const bmr = calculateBMR(profile); | |
profile.dailyCalories = calculateDailyCalories(bmr, profile.activityLevel, profile.healthGoal); | |
profile.proteinGoal = Math.round(profile.dailyCalories * 0.25 / 4); // 25% from protein | |
profile.fiberGoal = 25; // g | |
profile.waterGoal = 2000; // ml | |
appState.userProfile = profile; | |
localStorage.setItem('userProfile', JSON.stringify(profile)); | |
showNotification('個人資料已儲存!', 'success'); | |
switchTab('analyzer'); // Switch to analyzer after saving | |
} | |
function loadUserProfile() { | |
const storedProfile = localStorage.getItem('userProfile'); | |
if (storedProfile) { | |
appState.userProfile = JSON.parse(storedProfile); | |
} | |
} | |
function populateProfileForm() { | |
if (!appState.userProfile) return; | |
for (const key in appState.userProfile) { | |
const element = document.getElementById(key); | |
if (element) { | |
element.value = appState.userProfile[key]; | |
} | |
} | |
} | |
function calculateBMR({ gender, weight, height, age }) { | |
if (gender === 'male') { | |
return 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age); | |
} | |
return 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age); | |
} | |
function calculateDailyCalories(bmr, activityLevel, healthGoal) { | |
const activityMultipliers = { sedentary: 1.2, light: 1.375, moderate: 1.55, active: 1.725, extra: 1.9 }; | |
let calories = bmr * (activityMultipliers[activityLevel] || 1.2); | |
const goalAdjustments = { lose: -300, gain: 300, muscle: 200 }; | |
calories += goalAdjustments[healthGoal] || 0; | |
return Math.round(calories); | |
} | |
// ================================================================================= | |
// Camera and Image Analysis | |
// ================================================================================= | |
async function startCamera() { | |
try { | |
const stream = await navigator.mediaDevices.getUserMedia({ video: true }); | |
const video = document.getElementById('video'); | |
video.srcObject = stream; | |
video.style.display = 'block'; | |
} catch (err) { | |
showNotification('無法開啟相機,請檢查權限。', 'error'); | |
} | |
} | |
function capturePhoto() { | |
const video = document.getElementById('video'); | |
const canvas = document.getElementById('canvas'); | |
canvas.width = video.videoWidth; | |
canvas.height = video.videoHeight; | |
const context = canvas.getContext('2d'); | |
context.drawImage(video, 0, 0, canvas.width, canvas.height); | |
canvas.toBlob(blob => { | |
if (blob) { | |
processImage(blob); | |
} else { | |
showNotification('無法擷取圖片,請再試一次', 'error'); | |
} | |
}, 'image/jpeg'); | |
} | |
function handleFileUpload(event) { | |
const file = event.target.files[0]; | |
if (file) { | |
processImage(file); | |
// 重置文件輸入,允許重複上傳相同文件 | |
event.target.value = ''; | |
} | |
} | |
async function processImage(imageSource) { | |
showLoading(true); | |
document.getElementById('result').style.display = 'none'; | |
const formData = new FormData(); | |
formData.append('file', imageSource); | |
try { | |
// 調用 AI 端點獲取食物名稱 | |
const aiResponse = await fetch(`${API_BASE_URL}/ai/analyze-food-image/`, { | |
method: 'POST', | |
body: formData, | |
}); | |
if (!aiResponse.ok) { | |
const errorData = await aiResponse.json(); | |
throw new Error(errorData.detail || `AI辨識失敗 (狀態碼: ${aiResponse.status})`); | |
} | |
const aiData = await aiResponse.json(); | |
const foodName = aiData.food_name; | |
if (!foodName || foodName === "Unknown") { | |
throw new Error('AI無法辨識出食物名稱。'); | |
} | |
// 創建基本分析結果(基於 AI 辨識結果) | |
// 由於營養端點可能不存在,我們先創建一個基本版本 | |
appState.currentAnalysis = { | |
foodName: foodName, | |
description: `AI 辨識結果:${foodName}`, | |
healthIndex: 75, // 默認值 | |
glycemicIndex: 50, // 默認值 | |
benefits: [`含有 ${foodName} 的營養成分`], | |
nutrition: { | |
calories: 150, // 默認估計值 | |
protein: 8, | |
carbs: 20, | |
fat: 5, | |
fiber: 3, | |
sugar: 2 | |
}, | |
vitamins: { | |
'Vitamin C': 15, | |
'Vitamin A': 10 | |
}, | |
minerals: { | |
'Iron': 2, | |
'Calcium': 50 | |
} | |
}; | |
// 嘗試獲取詳細營養信息(可選) | |
try { | |
const nutritionResponse = await fetch(`${API_BASE_URL}/api/analyze-nutrition/${encodeURIComponent(foodName)}`); | |
if (nutritionResponse.ok) { | |
const nutritionData = await nutritionResponse.json(); | |
if (nutritionData.success) { | |
// 如果營養端點存在且成功,合併數據 | |
appState.currentAnalysis = { | |
foodName: foodName, | |
...nutritionData | |
}; | |
} | |
} | |
} catch (nutritionError) { | |
console.log('營養信息端點不可用,使用基本信息'); | |
} | |
displayAnalysisResults(appState.currentAnalysis); | |
} catch (error) { | |
showNotification(`分析失敗: ${error.message}`, 'error'); | |
} finally { | |
showLoading(false); | |
} | |
} | |
function displayAnalysisResults(analysis) { | |
console.log('顯示分析結果:', analysis); // 調試用 | |
// 顯示結果區域 | |
document.getElementById('result').style.display = 'block'; | |
// 基本信息 | |
document.getElementById('foodName').textContent = analysis.foodName; | |
document.getElementById('foodDescription').textContent = analysis.description || `這是 ${analysis.foodName}`; | |
// 健康指數 | |
const healthIndex = analysis.healthIndex || 75; | |
const glycemicIndex = analysis.glycemicIndex || 50; | |
document.getElementById('healthIndexValue').textContent = `${healthIndex}/100`; | |
document.getElementById('healthIndexFill').style.width = `${healthIndex}%`; | |
document.getElementById('glycemicIndexValue').textContent = `${glycemicIndex}/100`; | |
document.getElementById('glycemicIndexFill').style.width = `${glycemicIndex}%`; | |
// 營養益處 | |
const benefitsContainer = document.getElementById('benefitsTags'); | |
benefitsContainer.innerHTML = ''; | |
if (analysis.benefits && Array.isArray(analysis.benefits)) { | |
analysis.benefits.forEach(tag => { | |
const tagEl = document.createElement('span'); | |
tagEl.className = 'benefit-tag'; | |
tagEl.textContent = tag; | |
benefitsContainer.appendChild(tagEl); | |
}); | |
} else { | |
const tagEl = document.createElement('span'); | |
tagEl.className = 'benefit-tag'; | |
tagEl.textContent = `${analysis.foodName} 的營養價值`; | |
benefitsContainer.appendChild(tagEl); | |
} | |
// 營養信息 | |
const nutrition = analysis.nutrition || {}; | |
const nutritionData = { | |
calories: nutrition.calories || 150, | |
protein: nutrition.protein || 8, | |
carbs: nutrition.carbs || 20, | |
fat: nutrition.fat || 5, | |
fiber: nutrition.fiber || 3, | |
sugar: nutrition.sugar || 2 | |
}; | |
const labels = { | |
calories: '熱量', | |
protein: '蛋白質', | |
carbs: '碳水化合物', | |
fat: '脂肪', | |
fiber: '纖維', | |
sugar: '糖分' | |
}; | |
const units = { | |
calories: '卡', | |
protein: 'g', | |
carbs: 'g', | |
fat: 'g', | |
fiber: 'g', | |
sugar: 'g' | |
}; | |
// 填充營養網格 | |
const nutritionGrid = document.getElementById('nutritionGrid'); | |
nutritionGrid.innerHTML = ''; | |
for (const [key, value] of Object.entries(nutritionData)) { | |
const itemDiv = document.createElement('div'); | |
itemDiv.className = 'nutrition-item'; | |
itemDiv.innerHTML = ` | |
<div class="nutrition-label">${labels[key]}</div> | |
<div class="nutrition-value">${value} ${units[key]}</div> | |
`; | |
nutritionGrid.appendChild(itemDiv); | |
} | |
// 維生素和礦物質 | |
const vitaminsGrid = document.getElementById('vitaminsGrid'); | |
const mineralsGrid = document.getElementById('mineralsGrid'); | |
vitaminsGrid.innerHTML = ''; | |
mineralsGrid.innerHTML = ''; | |
if (analysis.vitamins) { | |
for (const [key, value] of Object.entries(analysis.vitamins)) { | |
const itemDiv = document.createElement('div'); | |
itemDiv.className = 'nutrition-item'; | |
itemDiv.innerHTML = ` | |
<div class="nutrition-label">${key}</div> | |
<div class="nutrition-value">${value} mg</div> | |
`; | |
vitaminsGrid.appendChild(itemDiv); | |
} | |
} | |
if (analysis.minerals) { | |
for (const [key, value] of Object.entries(analysis.minerals)) { | |
const itemDiv = document.createElement('div'); | |
itemDiv.className = 'nutrition-item'; | |
itemDiv.innerHTML = ` | |
<div class="nutrition-label">${key}</div> | |
<div class="nutrition-value">${value} mg</div> | |
`; | |
mineralsGrid.appendChild(itemDiv); | |
} | |
} | |
// 個人化建議 | |
if (appState.userProfile) { | |
const recoEl = document.getElementById('recommendation'); | |
const caloriePercentage = ((nutritionData.calories / appState.userProfile.dailyCalories) * 100).toFixed(0); | |
recoEl.innerHTML = ` | |
<div class="recommendation-title">💡 個人化建議</div> | |
<div class="recommendation-text">這份食物約佔您每日熱量建議的 ${caloriePercentage}%。</div> | |
`; | |
} | |
} | |
// ================================================================================= | |
// Food Diary | |
// ================================================================================= | |
function addToFoodDiary() { | |
if (!appState.currentAnalysis) { | |
return showNotification('沒有可加入的分析結果。', 'error'); | |
} | |
const meal = { | |
...appState.currentAnalysis, | |
id: Date.now(), | |
timestamp: new Date().toISOString() | |
}; | |
appState.foodDiary.push(meal); | |
saveFoodDiary(); | |
showNotification(`${meal.foodName} 已加入記錄!`, 'success'); | |
updateTrackingPage(); // Refresh tracking page data | |
} | |
function loadFoodDiary() { | |
const today = new Date().toISOString().slice(0, 10); | |
const storedDiary = localStorage.getItem(`foodDiary_${today}`); | |
appState.foodDiary = storedDiary ? JSON.parse(storedDiary) : []; | |
} | |
function saveFoodDiary() { | |
const today = new Date().toISOString().slice(0, 10); | |
localStorage.setItem(`foodDiary_${today}`, JSON.stringify(appState.foodDiary)); | |
} | |
// ================================================================================= | |
// Tracking Page | |
// ================================================================================= | |
function updateTrackingPage() { | |
if (!appState.userProfile) return; | |
updateWelcomeMessage(); | |
updateTodayStats(); | |
updateGoalsProgress(); | |
updateMealsList(); | |
renderWeeklyChart(); | |
} | |
function updateWelcomeMessage() { | |
document.getElementById('welcomeMessage').innerHTML = `<h3>👋 你好, ${appState.userProfile.name}!</h3><p>這是您今天的營養總覽。</p>`; | |
} | |
function updateTodayStats() { | |
const todayTotals = appState.foodDiary.reduce((totals, meal) => { | |
totals.calories += meal.nutrition.calories || 0; | |
totals.protein += meal.nutrition.protein || 0; | |
totals.fiber += meal.nutrition.fiber || 0; | |
return totals; | |
}, { calories: 0, protein: 0, fiber: 0 }); | |
document.getElementById('todayCalories').textContent = Math.round(todayTotals.calories); | |
document.getElementById('calorieGoalText').textContent = appState.userProfile.dailyCalories; | |
} | |
function updateGoalsProgress() { | |
const goals = { | |
calories: { label: '熱量', unit: '卡', goal: appState.userProfile.dailyCalories, key: 'calories' }, | |
protein: { label: '蛋白質', unit: 'g', goal: appState.userProfile.proteinGoal, key: 'protein' }, | |
fiber: { label: '纖維', unit: 'g', goal: appState.userProfile.fiberGoal, key: 'fiber' } | |
}; | |
const todayTotals = appState.foodDiary.reduce((totals, meal) => { | |
totals.calories += meal.nutrition.calories || 0; | |
totals.protein += meal.nutrition.protein || 0; | |
totals.fiber += meal.nutrition.fiber || 0; | |
return totals; | |
}, { calories: 0, protein: 0, fiber: 0 }); | |
const container = document.getElementById('goalItems'); | |
container.innerHTML = ''; | |
for (const g in goals) { | |
const { label, unit, goal, key } = goals[g]; | |
const current = Math.round(todayTotals[key]); | |
const progress = goal > 0 ? Math.min(100, (current / goal) * 100) : 0; | |
const item = document.createElement('div'); | |
item.className = 'progress-item'; | |
item.innerHTML = ` | |
<div class="progress-header"> | |
<span class="progress-label">${label}</span> | |
<span class="progress-value">${current} / ${goal} ${unit}</span> | |
</div> | |
<div class="progress-bar-container"> | |
<div class="progress-bar-fill" style="width: ${progress}%; background-color: #${g === 'calories' ? 'ff9a9e' : g === 'protein' ? '667eea' : '43cea2'};"></div> | |
</div>`; | |
container.appendChild(item); | |
} | |
} | |
function updateMealsList() { | |
const listEl = document.getElementById('mealsList'); | |
if (appState.foodDiary.length === 0) { | |
listEl.innerHTML = `<div class="empty-state"> | |
<div class="empty-icon">🍽️</div><p>今天尚未記錄任何食物</p> | |
</div>`; | |
return; | |
} | |
listEl.innerHTML = appState.foodDiary.map(meal => ` | |
<div class="meal-item"> | |
<div class="meal-info"> | |
<h4>${meal.foodName}</h4> | |
<span>${new Date(meal.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span> | |
</div> | |
<div class="meal-calories">${Math.round(meal.nutrition.calories)} 卡</div> | |
</div> | |
`).join(''); | |
} | |
function renderWeeklyChart() { | |
const ctx = document.getElementById('weeklyChart').getContext('2d'); | |
if (appState.charts.weekly) { | |
appState.charts.weekly.destroy(); | |
} | |
// Mock data for past 6 days + today's actual data | |
const labels = ['二', '三', '四', '五', '六', '日', '一'].slice(-7); // Last 7 days ending today | |
const data = Array(6).fill(0).map(() => Math.random() * (2200 - 1500) + 1500); | |
const todayCalories = appState.foodDiary.reduce((sum, meal) => sum + meal.nutrition.calories, 0); | |
data.push(todayCalories); | |
appState.charts.weekly = new Chart(ctx, { | |
type: 'line', | |
data: { | |
labels, | |
datasets: [{ | |
label: '每日熱量 (卡)', | |
data, | |
borderColor: '#667eea', | |
backgroundColor: 'rgba(102, 126, 234, 0.1)', | |
fill: true, | |
tension: 0.3 | |
}] | |
}, | |
options: { | |
responsive: true, | |
maintainAspectRatio: false, | |
scales: { y: { beginAtZero: true } }, | |
plugins: { legend: { display: false } } | |
} | |
}); | |
} | |
</script> | |
</body> | |
</html> |