From f65abdef0f9661a51439ceeb3aa66260a2134bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B0=20Jie=20Zhao=20=EF=BC=88=E9=9B=84?= =?UTF-8?q?=E7=8B=AE=E6=B1=BD=E8=BD=A6=E7=A7=91=E6=8A=80=EF=BC=89?= <00061074@chery.local> Date: Sun, 2 Nov 2025 22:23:10 +0800 Subject: [PATCH] feat: complete web app features and fix encoding --- static/css/analysis.css | 95 ++++++++ static/css/data_collection.css | 77 +++++++ static/css/recitation.css | 28 +++ static/css/recommendation.css | 99 +++++++++ static/css/style.css | 4 +- static/js/analysis.js | 133 ++++++++++++ static/js/data_collection.js | 193 +++++++++++++++++ static/js/recitation.js | 115 ++++++++++ static/js/recommendation.js | 135 ++++++++++++ templates/analysis.html | 66 ++++++ templates/data_collection.html | 120 ++++++++++ templates/index.html | 21 ++ templates/recitation.html | 5 + templates/recommendation.html | 70 ++++++ web_app.py | 386 ++++++++++++++++++++++++++++++++- 15 files changed, 1542 insertions(+), 5 deletions(-) create mode 100644 static/css/analysis.css create mode 100644 static/css/data_collection.css create mode 100644 static/css/recommendation.css create mode 100644 static/js/analysis.js create mode 100644 static/js/data_collection.js create mode 100644 static/js/recommendation.js create mode 100644 templates/analysis.html create mode 100644 templates/data_collection.html create mode 100644 templates/recommendation.html diff --git a/static/css/analysis.css b/static/css/analysis.css new file mode 100644 index 0000000..01b8955 --- /dev/null +++ b/static/css/analysis.css @@ -0,0 +1,95 @@ +.analysis-container { + max-width: 900px; + margin: 0 auto; +} + +.section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.section h2 { + font-size: 1.8em; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #555; +} + +.form-input, .form-textarea { + width: 100%; + padding: 12px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: #667eea; +} + +.form-textarea { + resize: vertical; + min-height: 120px; +} + +.analysis-result { + background: white; + padding: 25px; + border-radius: 10px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.analysis-result h3 { + margin-top: 0; + color: #667eea; +} + +.analysis-result .analysis-section { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; +} + +.analysis-result .analysis-section h4 { + color: #555; + margin-top: 0; +} + +.message-area { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + display: none; +} + +.message-area.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message-area.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + diff --git a/static/css/data_collection.css b/static/css/data_collection.css new file mode 100644 index 0000000..b7900ee --- /dev/null +++ b/static/css/data_collection.css @@ -0,0 +1,77 @@ +.data-collection-container { + max-width: 800px; + margin: 0 auto; +} + +.section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.section h2 { + font-size: 1.8em; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #555; +} + +.form-input, .form-textarea { + width: 100%; + padding: 12px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: #667eea; +} + +.form-textarea { + resize: vertical; + min-height: 100px; +} + +.message-area { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + display: none; +} + +.message-area.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message-area.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + +.message-area.info { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; + display: block; +} + diff --git a/static/css/recitation.css b/static/css/recitation.css index a67aca2..e4ca83d 100644 --- a/static/css/recitation.css +++ b/static/css/recitation.css @@ -191,6 +191,34 @@ color: #333; } +/* 导出按钮样式 */ +.export-buttons { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.btn-export { + padding: 10px 20px; + background: #28a745; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.9em; + transition: all 0.3s; +} + +.btn-export:hover { + background: #218838; + transform: translateY(-2px); +} + +.btn-export:active { + transform: translateY(0); +} + /* 响应式设计 */ @media (max-width: 768px) { .wheel-container { diff --git a/static/css/recommendation.css b/static/css/recommendation.css new file mode 100644 index 0000000..4f416c5 --- /dev/null +++ b/static/css/recommendation.css @@ -0,0 +1,99 @@ +.recommendation-container { + max-width: 900px; + margin: 0 auto; +} + +.section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.section h2 { + font-size: 1.8em; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #555; +} + +.form-input { + width: 100%; + padding: 12px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-input:focus { + outline: none; + border-color: #667eea; +} + +.recommendations-list { + display: grid; + gap: 20px; +} + +.recommendation-card { + background: white; + padding: 20px; + border-radius: 10px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.recommendation-card h3 { + margin-top: 0; + color: #667eea; +} + +.recommendation-card .food-list { + margin: 15px 0; +} + +.recommendation-card .food-item { + padding: 8px; + margin: 5px 0; + background: #f8f9fa; + border-radius: 5px; +} + +.recommendation-card .confidence { + color: #28a745; + font-weight: bold; +} + +.message-area { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + display: none; +} + +.message-area.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message-area.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + diff --git a/static/css/style.css b/static/css/style.css index de45c28..c4a705b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -89,8 +89,8 @@ body { .feature-cards { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 30px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 25px; margin-top: 40px; } diff --git a/static/js/analysis.js b/static/js/analysis.js new file mode 100644 index 0000000..6b368b4 --- /dev/null +++ b/static/js/analysis.js @@ -0,0 +1,133 @@ +// 分析功能脚本 + +let currentUserId = null; + +// DOM元素 +const loginSection = document.getElementById('loginSection'); +const requestSection = document.getElementById('requestSection'); +const analysisSection = document.getElementById('analysisSection'); +const messageArea = document.getElementById('messageArea'); + +const loginBtn = document.getElementById('loginBtn'); +const userIdInput = document.getElementById('userId'); +const analyzeBtn = document.getElementById('analyzeBtn'); +const mealDataTextarea = document.getElementById('mealData'); +const analysisResult = document.getElementById('analysisResult'); + +// 显示消息 +function showMessage(message, type = 'info') { + messageArea.textContent = message; + messageArea.className = `message-area ${type}`; + setTimeout(() => { + messageArea.className = 'message-area'; + messageArea.style.display = 'none'; + }, 3000); +} + +// 用户登录 +loginBtn.addEventListener('click', async () => { + const userId = userIdInput.value.trim(); + + if (!userId) { + showMessage('请输入用户ID', 'error'); + return; + } + + loginBtn.disabled = true; + loginBtn.textContent = '登录中...'; + + try { + const response = await fetch('/api/user/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: userId + }) + }); + + const data = await response.json(); + + if (data.success) { + currentUserId = userId; + loginSection.style.display = 'none'; + requestSection.style.display = 'block'; + showMessage(`欢迎,${data.name || userId}!`, 'success'); + } else { + showMessage(data.message || '登录失败', 'error'); + } + } catch (error) { + console.error('登录失败:', error); + showMessage('登录失败,请检查网络连接', 'error'); + } finally { + loginBtn.disabled = false; + loginBtn.textContent = '登录'; + } +}); + +// 开始分析 +analyzeBtn.addEventListener('click', async () => { + const mealDataText = mealDataTextarea.value.trim(); + + if (!mealDataText) { + showMessage('请输入餐食信息', 'error'); + return; + } + + analyzeBtn.disabled = true; + analyzeBtn.textContent = '分析中...'; + + try { + const response = await fetch('/api/analysis/nutrition', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: currentUserId, + meal_data: { + text: mealDataText + } + }) + }); + + const data = await response.json(); + + if (data.success && data.analysis) { + displayAnalysis(data.analysis); + analysisSection.style.display = 'block'; + showMessage('分析完成!', 'success'); + } else { + showMessage(data.message || '分析失败', 'error'); + } + } catch (error) { + console.error('分析失败:', error); + showMessage('分析失败,请检查网络连接', 'error'); + } finally { + analyzeBtn.disabled = false; + analyzeBtn.textContent = '开始分析'; + } +}); + +// 显示分析结果 +function displayAnalysis(analysis) { + analysisResult.innerHTML = ''; + + if (typeof analysis === 'string') { + // 如果是字符串,直接显示 + analysisResult.innerHTML = `
${analysis.replace(/\n/g, '
')}
${analysis.analysis.replace(/\n/g, '
')}
${typeof value === 'string' ? value.replace(/\n/g, '
') : JSON.stringify(value, null, 2)}
AI智能营养分析与建议
+建立你的个人饮食档案
+杩欐槸涓涓泦鎴愪簡鏅鸿兘楗鎺ㄨ崘鍜岃儗璇垫帓搴忓姛鑳界殑缃戦〉搴旂敤銆
基于AI的个性化餐食推荐
+