Merge pull request 'feat: complete web app features and fix encoding' (#2) from testwith-login into master
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
95
static/css/analysis.css
Normal file
95
static/css/analysis.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
77
static/css/data_collection.css
Normal file
77
static/css/data_collection.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -191,6 +191,34 @@
|
|||||||
color: #333;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.wheel-container {
|
.wheel-container {
|
||||||
|
|||||||
99
static/css/recommendation.css
Normal file
99
static/css/recommendation.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -89,8 +89,8 @@ body {
|
|||||||
|
|
||||||
.feature-cards {
|
.feature-cards {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
gap: 30px;
|
gap: 25px;
|
||||||
margin-top: 40px;
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
133
static/js/analysis.js
Normal file
133
static/js/analysis.js
Normal file
@@ -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 = `<div class="analysis-section"><p>${analysis.replace(/\n/g, '<br>')}</p></div>`;
|
||||||
|
} else if (analysis.analysis) {
|
||||||
|
// 如果包含analysis字段
|
||||||
|
analysisResult.innerHTML = `<div class="analysis-section"><h4>分析结果</h4><p>${analysis.analysis.replace(/\n/g, '<br>')}</p></div>`;
|
||||||
|
} else {
|
||||||
|
// 如果是对象,格式化显示
|
||||||
|
for (const [key, value] of Object.entries(analysis)) {
|
||||||
|
const section = document.createElement('div');
|
||||||
|
section.className = 'analysis-section';
|
||||||
|
section.innerHTML = `<h4>${key}</h4><p>${typeof value === 'string' ? value.replace(/\n/g, '<br>') : JSON.stringify(value, null, 2)}</p>`;
|
||||||
|
analysisResult.appendChild(section);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
193
static/js/data_collection.js
Normal file
193
static/js/data_collection.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
// 数据采集功能脚本
|
||||||
|
|
||||||
|
let currentUserId = null;
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
const loginSection = document.getElementById('loginSection');
|
||||||
|
const basicQuestionnaire = document.getElementById('basicQuestionnaire');
|
||||||
|
const mealRecord = document.getElementById('mealRecord');
|
||||||
|
const messageArea = document.getElementById('messageArea');
|
||||||
|
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
|
const userIdInput = document.getElementById('userId');
|
||||||
|
const userNameInput = document.getElementById('userName');
|
||||||
|
|
||||||
|
const submitBasicBtn = document.getElementById('submitBasicBtn');
|
||||||
|
const submitMealBtn = document.getElementById('submitMealBtn');
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
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();
|
||||||
|
const userName = userNameInput.value.trim();
|
||||||
|
|
||||||
|
if (!userId || !userName) {
|
||||||
|
showMessage('请输入用户ID和姓名', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loginBtn.disabled = true;
|
||||||
|
loginBtn.textContent = '登录中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/user/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: userId,
|
||||||
|
name: userName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
currentUserId = userId;
|
||||||
|
loginSection.style.display = 'none';
|
||||||
|
basicQuestionnaire.style.display = 'block';
|
||||||
|
mealRecord.style.display = 'block';
|
||||||
|
showMessage('登录成功!', 'success');
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '登录失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
showMessage('登录失败,请检查网络连接', 'error');
|
||||||
|
} finally {
|
||||||
|
loginBtn.disabled = false;
|
||||||
|
loginBtn.textContent = '登录/注册';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 提交基础信息
|
||||||
|
submitBasicBtn.addEventListener('click', async () => {
|
||||||
|
const age = document.getElementById('age').value;
|
||||||
|
const gender = document.getElementById('gender').value;
|
||||||
|
const height = document.getElementById('height').value;
|
||||||
|
const weight = document.getElementById('weight').value;
|
||||||
|
const activityLevel = document.getElementById('activityLevel').value;
|
||||||
|
|
||||||
|
if (!age || !gender || !height || !weight || !activityLevel) {
|
||||||
|
showMessage('请填写完整的基础信息', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBasicBtn.disabled = true;
|
||||||
|
submitBasicBtn.textContent = '提交中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/questionnaire/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: currentUserId,
|
||||||
|
type: 'basic',
|
||||||
|
answers: {
|
||||||
|
age: parseInt(age),
|
||||||
|
gender: gender,
|
||||||
|
height: parseFloat(height),
|
||||||
|
weight: parseFloat(weight),
|
||||||
|
activity_level: activityLevel
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showMessage('基础信息提交成功!', 'success');
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '提交失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败:', error);
|
||||||
|
showMessage('提交失败,请检查网络连接', 'error');
|
||||||
|
} finally {
|
||||||
|
submitBasicBtn.disabled = false;
|
||||||
|
submitBasicBtn.textContent = '提交基础信息';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 记录餐食
|
||||||
|
submitMealBtn.addEventListener('click', async () => {
|
||||||
|
const mealDate = document.getElementById('mealDate').value || new Date().toISOString().split('T')[0];
|
||||||
|
const mealType = document.getElementById('mealType').value;
|
||||||
|
const foodsText = document.getElementById('foods').value;
|
||||||
|
const calories = document.getElementById('calories').value;
|
||||||
|
const satisfaction = document.getElementById('satisfaction').value;
|
||||||
|
|
||||||
|
if (!foodsText || !calories) {
|
||||||
|
showMessage('请填写完整的餐食信息', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析食物列表
|
||||||
|
const foods = foodsText.split('\n').filter(line => line.trim());
|
||||||
|
const quantities = foods.map(f => {
|
||||||
|
const parts = f.trim().split(/\s+/);
|
||||||
|
return parts.length > 1 ? parts.slice(1).join(' ') : '适量';
|
||||||
|
});
|
||||||
|
const foodNames = foods.map(f => f.trim().split(/\s+/)[0]);
|
||||||
|
|
||||||
|
submitMealBtn.disabled = true;
|
||||||
|
submitMealBtn.textContent = '记录中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/meal/record', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: currentUserId,
|
||||||
|
date: mealDate,
|
||||||
|
meal_type: mealType,
|
||||||
|
foods: foodNames,
|
||||||
|
quantities: quantities,
|
||||||
|
calories: parseFloat(calories),
|
||||||
|
satisfaction_score: parseInt(satisfaction),
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showMessage('餐食记录成功!', 'success');
|
||||||
|
// 清空表单
|
||||||
|
document.getElementById('foods').value = '';
|
||||||
|
document.getElementById('calories').value = '';
|
||||||
|
document.getElementById('satisfaction').value = '3';
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '记录失败', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('记录失败:', error);
|
||||||
|
showMessage('记录失败,请检查网络连接', 'error');
|
||||||
|
} finally {
|
||||||
|
submitMealBtn.disabled = false;
|
||||||
|
submitMealBtn.textContent = '记录餐食';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置默认日期为今天
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const mealDateInput = document.getElementById('mealDate');
|
||||||
|
if (mealDateInput) {
|
||||||
|
mealDateInput.value = new Date().toISOString().split('T')[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -26,6 +26,73 @@ const currentItem = document.getElementById('currentItem');
|
|||||||
const resultSection = document.getElementById('resultSection');
|
const resultSection = document.getElementById('resultSection');
|
||||||
const sortedList = document.getElementById('sortedList');
|
const sortedList = document.getElementById('sortedList');
|
||||||
const resetBtn = document.getElementById('resetBtn');
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
|
const exportTxtBtn = document.getElementById('exportTxtBtn');
|
||||||
|
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
||||||
|
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
||||||
|
|
||||||
|
// 本地存储键名
|
||||||
|
const STORAGE_KEY_EXTRACTED = 'recitation_extracted_items';
|
||||||
|
const STORAGE_KEY_SORTED = 'recitation_sorted_items';
|
||||||
|
const STORAGE_KEY_ORIGINAL_TEXT = 'recitation_original_text';
|
||||||
|
|
||||||
|
// 页面加载时恢复数据
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
restoreFromStorage();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
function saveToStorage() {
|
||||||
|
try {
|
||||||
|
if (extractedItems.length > 0) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_EXTRACTED, JSON.stringify(extractedItems));
|
||||||
|
}
|
||||||
|
if (sortedItems.length > 0) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_SORTED, JSON.stringify(sortedItems));
|
||||||
|
}
|
||||||
|
if (textInput.value.trim()) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_ORIGINAL_TEXT, textInput.value);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存到本地存储失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地存储恢复
|
||||||
|
function restoreFromStorage() {
|
||||||
|
try {
|
||||||
|
const savedExtracted = localStorage.getItem(STORAGE_KEY_EXTRACTED);
|
||||||
|
const savedSorted = localStorage.getItem(STORAGE_KEY_SORTED);
|
||||||
|
const savedText = localStorage.getItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||||
|
|
||||||
|
if (savedText) {
|
||||||
|
textInput.value = savedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedExtracted) {
|
||||||
|
extractedItems = JSON.parse(savedExtracted);
|
||||||
|
displayExtractedItems(extractedItems);
|
||||||
|
extractedSection.style.display = 'block';
|
||||||
|
textInput.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedSorted) {
|
||||||
|
sortedItems = JSON.parse(savedSorted);
|
||||||
|
displaySortedItems(sortedItems);
|
||||||
|
createWheel(sortedItems);
|
||||||
|
wheelSection.style.display = 'block';
|
||||||
|
resultSection.style.display = 'block';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('从本地存储恢复失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除本地存储
|
||||||
|
function clearStorage() {
|
||||||
|
localStorage.removeItem(STORAGE_KEY_EXTRACTED);
|
||||||
|
localStorage.removeItem(STORAGE_KEY_SORTED);
|
||||||
|
localStorage.removeItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||||
|
}
|
||||||
|
|
||||||
// 提取知识点
|
// 提取知识点
|
||||||
extractBtn.addEventListener('click', async () => {
|
extractBtn.addEventListener('click', async () => {
|
||||||
@@ -55,6 +122,7 @@ extractBtn.addEventListener('click', async () => {
|
|||||||
displayExtractedItems(extractedItems);
|
displayExtractedItems(extractedItems);
|
||||||
extractedSection.style.display = 'block';
|
extractedSection.style.display = 'block';
|
||||||
textInput.disabled = true;
|
textInput.disabled = true;
|
||||||
|
saveToStorage(); // 保存到本地存储
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '提取失败');
|
alert(data.message || '提取失败');
|
||||||
}
|
}
|
||||||
@@ -108,6 +176,7 @@ sortBtn.addEventListener('click', async () => {
|
|||||||
wheelSection.style.display = 'block';
|
wheelSection.style.display = 'block';
|
||||||
resultSection.style.display = 'block';
|
resultSection.style.display = 'block';
|
||||||
currentSpinIndex = 0;
|
currentSpinIndex = 0;
|
||||||
|
saveToStorage(); // 保存到本地存储
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '排序失败');
|
alert(data.message || '排序失败');
|
||||||
}
|
}
|
||||||
@@ -259,6 +328,50 @@ function displaySortedItems(items) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出功能
|
||||||
|
exportTxtBtn.addEventListener('click', () => exportData('txt'));
|
||||||
|
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
||||||
|
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
||||||
|
|
||||||
|
function exportData(format) {
|
||||||
|
if (sortedItems.length === 0) {
|
||||||
|
alert('没有可导出的数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/export/sorted', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
items: sortedItems,
|
||||||
|
format: format
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('导出失败');
|
||||||
|
}
|
||||||
|
return response.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
|
||||||
|
a.download = `背诵排序结果_${timestamp}.${format}`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('导出失败:', error);
|
||||||
|
alert('导出失败,请重试');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 重置
|
// 重置
|
||||||
resetBtn.addEventListener('click', () => {
|
resetBtn.addEventListener('click', () => {
|
||||||
extractedItems = [];
|
extractedItems = [];
|
||||||
@@ -281,5 +394,7 @@ resetBtn.addEventListener('click', () => {
|
|||||||
|
|
||||||
isSpinning = false;
|
isSpinning = false;
|
||||||
spinBtn.disabled = false;
|
spinBtn.disabled = false;
|
||||||
|
|
||||||
|
clearStorage(); // 清除本地存储
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
135
static/js/recommendation.js
Normal file
135
static/js/recommendation.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// 推荐功能脚本
|
||||||
|
|
||||||
|
let currentUserId = null;
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
const loginSection = document.getElementById('loginSection');
|
||||||
|
const requestSection = document.getElementById('requestSection');
|
||||||
|
const recommendationsSection = document.getElementById('recommendationsSection');
|
||||||
|
const messageArea = document.getElementById('messageArea');
|
||||||
|
|
||||||
|
const loginBtn = document.getElementById('loginBtn');
|
||||||
|
const userIdInput = document.getElementById('userId');
|
||||||
|
const getRecommendationBtn = document.getElementById('getRecommendationBtn');
|
||||||
|
const mealTypeSelect = document.getElementById('mealType');
|
||||||
|
const recommendationsList = document.getElementById('recommendationsList');
|
||||||
|
|
||||||
|
// 显示消息
|
||||||
|
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 = '登录';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取推荐
|
||||||
|
getRecommendationBtn.addEventListener('click', async () => {
|
||||||
|
const mealType = mealTypeSelect.value;
|
||||||
|
|
||||||
|
getRecommendationBtn.disabled = true;
|
||||||
|
getRecommendationBtn.textContent = '获取中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/recommendation/get', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
user_id: currentUserId,
|
||||||
|
meal_type: mealType,
|
||||||
|
preferences: {},
|
||||||
|
context: {}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.recommendations && data.recommendations.length > 0) {
|
||||||
|
displayRecommendations(data.recommendations);
|
||||||
|
recommendationsSection.style.display = 'block';
|
||||||
|
showMessage('推荐获取成功!', 'success');
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || '暂无推荐,请先完善个人数据', 'info');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取推荐失败:', error);
|
||||||
|
showMessage('获取推荐失败,请检查网络连接', 'error');
|
||||||
|
} finally {
|
||||||
|
getRecommendationBtn.disabled = false;
|
||||||
|
getRecommendationBtn.textContent = '获取推荐';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示推荐结果
|
||||||
|
function displayRecommendations(recommendations) {
|
||||||
|
recommendationsList.innerHTML = '';
|
||||||
|
|
||||||
|
recommendations.forEach((rec, index) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'recommendation-card';
|
||||||
|
|
||||||
|
let foods = [];
|
||||||
|
if (rec.foods && Array.isArray(rec.foods)) {
|
||||||
|
foods = rec.foods;
|
||||||
|
} else if (rec.food) {
|
||||||
|
foods = [rec.food];
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>推荐方案 ${index + 1}</h3>
|
||||||
|
<div class="food-list">
|
||||||
|
${foods.map(food => `<div class="food-item">${food}</div>`).join('')}
|
||||||
|
</div>
|
||||||
|
${rec.confidence ? `<div class="confidence">置信度: ${(rec.confidence * 100).toFixed(1)}%</div>` : ''}
|
||||||
|
${rec.reason ? `<div style="margin-top: 10px; color: #666;">推荐理由: ${rec.reason}</div>` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
recommendationsList.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
66
templates/analysis.html
Normal file
66
templates/analysis.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>营养分析 - 个性化饮食推荐助手</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/analysis.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>? 营养分析</h1>
|
||||||
|
<p class="subtitle">AI智能营养分析与建议</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-item">首页</a>
|
||||||
|
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||||
|
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||||
|
<a href="/analysis" class="nav-item active">营养分析</a>
|
||||||
|
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="analysis-container">
|
||||||
|
<!-- 用户登录区域 -->
|
||||||
|
<div id="loginSection" class="section">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="userId">用户ID:</label>
|
||||||
|
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||||
|
</div>
|
||||||
|
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分析请求区域 -->
|
||||||
|
<div id="requestSection" class="section" style="display: none;">
|
||||||
|
<h2>营养分析</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>餐食信息:</label>
|
||||||
|
<textarea id="mealData" class="form-textarea" rows="5" placeholder="请输入餐食信息,例如: 早餐:燕麦粥、香蕉、牛奶 热量:350大卡"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="analyzeBtn" class="btn btn-primary">开始分析</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分析结果显示 -->
|
||||||
|
<div id="analysisSection" class="section" style="display: none;">
|
||||||
|
<h2>分析结果</h2>
|
||||||
|
<div id="analysisResult" class="analysis-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作提示 -->
|
||||||
|
<div id="messageArea" class="message-area"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/analysis.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
120
templates/data_collection.html
Normal file
120
templates/data_collection.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>数据采集 - 个性化饮食推荐助手</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/data_collection.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>? 数据采集</h1>
|
||||||
|
<p class="subtitle">建立你的个人饮食档案</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-item">首页</a>
|
||||||
|
<a href="/data-collection" class="nav-item active">数据采集</a>
|
||||||
|
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||||
|
<a href="/analysis" class="nav-item">营养分析</a>
|
||||||
|
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="data-collection-container">
|
||||||
|
<!-- 用户登录区域 -->
|
||||||
|
<div id="loginSection" class="section">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="userId">用户ID:</label>
|
||||||
|
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="userName">姓名:</label>
|
||||||
|
<input type="text" id="userName" class="form-input" placeholder="请输入姓名">
|
||||||
|
</div>
|
||||||
|
<button id="loginBtn" class="btn btn-primary">登录/注册</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基础信息问卷 -->
|
||||||
|
<div id="basicQuestionnaire" class="section" style="display: none;">
|
||||||
|
<h2>基础信息问卷</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>年龄:</label>
|
||||||
|
<input type="number" id="age" class="form-input" placeholder="请输入年龄">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>性别:</label>
|
||||||
|
<select id="gender" class="form-input">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="男">男</option>
|
||||||
|
<option value="女">女</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>身高(cm):</label>
|
||||||
|
<input type="number" id="height" class="form-input" placeholder="请输入身高">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>体重(kg):</label>
|
||||||
|
<input type="number" id="weight" class="form-input" placeholder="请输入体重">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>活动水平:</label>
|
||||||
|
<select id="activityLevel" class="form-input">
|
||||||
|
<option value="">请选择</option>
|
||||||
|
<option value="久坐">久坐</option>
|
||||||
|
<option value="轻度活动">轻度活动</option>
|
||||||
|
<option value="中度活动">中度活动</option>
|
||||||
|
<option value="高度活动">高度活动</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="submitBasicBtn" class="btn btn-primary">提交基础信息</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 餐食记录 -->
|
||||||
|
<div id="mealRecord" class="section" style="display: none;">
|
||||||
|
<h2>记录餐食</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>日期:</label>
|
||||||
|
<input type="date" id="mealDate" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>餐次:</label>
|
||||||
|
<select id="mealType" class="form-input">
|
||||||
|
<option value="breakfast">早餐</option>
|
||||||
|
<option value="lunch">午餐</option>
|
||||||
|
<option value="dinner">晚餐</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>食物列表(每行一个):</label>
|
||||||
|
<textarea id="foods" class="form-textarea" rows="5" placeholder="例如: 燕麦粥 1碗 香蕉 1根 牛奶 200ml"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>热量(大卡):</label>
|
||||||
|
<input type="number" id="calories" class="form-input" placeholder="请输入热量">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>满意度(1-5分):</label>
|
||||||
|
<input type="number" id="satisfaction" class="form-input" min="1" max="5" value="3">
|
||||||
|
</div>
|
||||||
|
<button id="submitMealBtn" class="btn btn-primary">记录餐食</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作提示 -->
|
||||||
|
<div id="messageArea" class="message-area"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/data_collection.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/" class="nav-item active">首页</a>
|
<a href="/" class="nav-item active">首页</a>
|
||||||
|
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||||
|
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||||
|
<a href="/analysis" class="nav-item">营养分析</a>
|
||||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -24,6 +27,24 @@
|
|||||||
<p>这是一个集成了智能饮食推荐和背诵排序功能的网页应用。</p>
|
<p>这是一个集成了智能饮食推荐和背诵排序功能的网页应用。</p>
|
||||||
|
|
||||||
<div class="feature-cards">
|
<div class="feature-cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon">📝</div>
|
||||||
|
<h3>数据采集</h3>
|
||||||
|
<p>填写基础信息问卷,记录你的饮食习惯和偏好,建立个人档案。</p>
|
||||||
|
<a href="/data-collection" class="btn btn-primary">开始使用</a>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon">🤖</div>
|
||||||
|
<h3>智能推荐</h3>
|
||||||
|
<p>基于你的饮食习惯和偏好,AI为你推荐个性化的餐食搭配。</p>
|
||||||
|
<a href="/recommendation" class="btn btn-primary">开始使用</a>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon">🔬</div>
|
||||||
|
<h3>营养分析</h3>
|
||||||
|
<p>分析你的餐食营养状况,提供专业的营养建议和健康指导。</p>
|
||||||
|
<a href="/analysis" class="btn btn-primary">开始使用</a>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-icon">🎯</div>
|
<div class="card-icon">🎯</div>
|
||||||
<h3>背诵排序</h3>
|
<h3>背诵排序</h3>
|
||||||
|
|||||||
@@ -56,6 +56,11 @@
|
|||||||
<!-- 排序结果显示 -->
|
<!-- 排序结果显示 -->
|
||||||
<div id="resultSection" class="result-section" style="display: none;">
|
<div id="resultSection" class="result-section" style="display: none;">
|
||||||
<h2>随机排序结果</h2>
|
<h2>随机排序结果</h2>
|
||||||
|
<div class="export-buttons">
|
||||||
|
<button id="exportTxtBtn" class="btn btn-export">导出为TXT</button>
|
||||||
|
<button id="exportJsonBtn" class="btn btn-export">导出为JSON</button>
|
||||||
|
<button id="exportCsvBtn" class="btn btn-export">导出为CSV</button>
|
||||||
|
</div>
|
||||||
<div id="sortedList" class="sorted-list"></div>
|
<div id="sortedList" class="sorted-list"></div>
|
||||||
<button id="resetBtn" class="btn btn-secondary">重新开始</button>
|
<button id="resetBtn" class="btn btn-secondary">重新开始</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
70
templates/recommendation.html
Normal file
70
templates/recommendation.html
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>智能推荐 - 个性化饮食推荐助手</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/recommendation.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>? 智能推荐</h1>
|
||||||
|
<p class="subtitle">基于AI的个性化餐食推荐</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-item">首页</a>
|
||||||
|
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||||
|
<a href="/recommendation" class="nav-item active">智能推荐</a>
|
||||||
|
<a href="/analysis" class="nav-item">营养分析</a>
|
||||||
|
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="recommendation-container">
|
||||||
|
<!-- 用户登录区域 -->
|
||||||
|
<div id="loginSection" class="section">
|
||||||
|
<h2>用户登录</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="userId">用户ID:</label>
|
||||||
|
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||||
|
</div>
|
||||||
|
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 推荐请求区域 -->
|
||||||
|
<div id="requestSection" class="section" style="display: none;">
|
||||||
|
<h2>获取推荐</h2>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>餐次:</label>
|
||||||
|
<select id="mealType" class="form-input">
|
||||||
|
<option value="breakfast">早餐</option>
|
||||||
|
<option value="lunch">午餐</option>
|
||||||
|
<option value="dinner">晚餐</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="getRecommendationBtn" class="btn btn-primary">获取推荐</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 推荐结果显示 -->
|
||||||
|
<div id="recommendationsSection" class="section" style="display: none;">
|
||||||
|
<h2>推荐结果</h2>
|
||||||
|
<div id="recommendationsList" class="recommendations-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 操作提示 -->
|
||||||
|
<div id="messageArea" class="message-area"></div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/recommendation.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
386
web_app.py
386
web_app.py
@@ -3,11 +3,20 @@
|
|||||||
网页端应用 - 个性化饮食推荐助手 + 背诵排序功能
|
网页端应用 - 个性化饮食推荐助手 + 背诵排序功能
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify, session, Response
|
||||||
import re
|
import re
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 导入业务模块
|
||||||
|
from core.base import BaseConfig, AppCore, ModuleManager, ModuleType, initialize_app
|
||||||
|
from modules.data_collection import DataCollectionModule
|
||||||
|
from modules.ai_analysis import AIAnalysisModule
|
||||||
|
from modules.recommendation_engine import RecommendationEngine
|
||||||
|
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -27,6 +36,43 @@ app.config['SECRET_KEY'] = 'your-secret-key-here'
|
|||||||
app.jinja_env.auto_reload = True
|
app.jinja_env.auto_reload = True
|
||||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||||
|
|
||||||
|
# 确保所有响应使用UTF-8编码
|
||||||
|
@app.after_request
|
||||||
|
def after_request(response):
|
||||||
|
"""确保所有响应使用UTF-8编码"""
|
||||||
|
response.headers['Content-Type'] = response.headers.get('Content-Type', 'text/html; charset=utf-8')
|
||||||
|
if 'charset=' not in response.headers.get('Content-Type', ''):
|
||||||
|
if response.headers.get('Content-Type', '').startswith('text/html'):
|
||||||
|
response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||||
|
elif response.headers.get('Content-Type', '').startswith('application/json'):
|
||||||
|
response.headers['Content-Type'] = 'application/json; charset=utf-8'
|
||||||
|
return response
|
||||||
|
|
||||||
|
# 初始化应用核心(延迟加载,避免在导入时就初始化)
|
||||||
|
app_core = None
|
||||||
|
config = None
|
||||||
|
|
||||||
|
def get_app_core():
|
||||||
|
"""获取应用核心实例(延迟初始化)"""
|
||||||
|
global app_core, config
|
||||||
|
if app_core is None:
|
||||||
|
config = BaseConfig()
|
||||||
|
if initialize_app(config):
|
||||||
|
app_core = AppCore(config)
|
||||||
|
# 注册所有模块
|
||||||
|
module_manager = ModuleManager(config)
|
||||||
|
module_manager.register_module(DataCollectionModule(config))
|
||||||
|
module_manager.register_module(AIAnalysisModule(config))
|
||||||
|
module_manager.register_module(RecommendationEngine(config))
|
||||||
|
module_manager.register_module(OCRCalorieRecognitionModule(config))
|
||||||
|
module_manager.initialize_all()
|
||||||
|
app_core.module_manager = module_manager
|
||||||
|
app_core.start()
|
||||||
|
logger.info("应用核心初始化完成")
|
||||||
|
else:
|
||||||
|
logger.error("应用核心初始化失败")
|
||||||
|
return app_core
|
||||||
|
|
||||||
|
|
||||||
class RecitationSorter:
|
class RecitationSorter:
|
||||||
"""背诵排序器"""
|
"""背诵排序器"""
|
||||||
@@ -110,13 +156,31 @@ sorter = RecitationSorter()
|
|||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""首页"""
|
"""首页"""
|
||||||
return render_template('index.html')
|
return render_template('index.html', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/recitation')
|
@app.route('/recitation')
|
||||||
def recitation():
|
def recitation():
|
||||||
"""背诵排序页面"""
|
"""背诵排序页面"""
|
||||||
return render_template('recitation.html')
|
return render_template('recitation.html', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/data-collection')
|
||||||
|
def data_collection():
|
||||||
|
"""数据采集页面"""
|
||||||
|
return render_template('data_collection.html', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/recommendation')
|
||||||
|
def recommendation():
|
||||||
|
"""推荐页面"""
|
||||||
|
return render_template('recommendation.html', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/analysis')
|
||||||
|
def analysis():
|
||||||
|
"""分析页面"""
|
||||||
|
return render_template('analysis.html', encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/extract', methods=['POST'])
|
@app.route('/api/extract', methods=['POST'])
|
||||||
@@ -189,6 +253,322 @@ def sort_items():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/export/sorted', methods=['POST'])
|
||||||
|
def export_sorted():
|
||||||
|
"""导出排序结果"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
items = data.get('items', [])
|
||||||
|
export_format = data.get('format', 'txt') # txt, json, csv
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '没有可导出的数据'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if export_format == 'json':
|
||||||
|
content = json.dumps({
|
||||||
|
'items': items,
|
||||||
|
'count': len(items),
|
||||||
|
'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
filename = f'背诵排序结果_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json'
|
||||||
|
mimetype = 'application/json; charset=utf-8'
|
||||||
|
elif export_format == 'csv':
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
output = io.StringIO()
|
||||||
|
writer = csv.writer(output)
|
||||||
|
writer.writerow(['序号', '知识点'])
|
||||||
|
for i, item in enumerate(items, 1):
|
||||||
|
writer.writerow([i, item])
|
||||||
|
content = output.getvalue()
|
||||||
|
filename = f'背诵排序结果_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'
|
||||||
|
mimetype = 'text/csv; charset=utf-8'
|
||||||
|
else: # txt
|
||||||
|
content_lines = ['背诵排序结果', '=' * 50, '']
|
||||||
|
for i, item in enumerate(items, 1):
|
||||||
|
content_lines.append(f'{i}. {item}')
|
||||||
|
content_lines.extend(['', '=' * 50, f'导出时间: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'])
|
||||||
|
content = '\n'.join(content_lines)
|
||||||
|
filename = f'背诵排序结果_{datetime.now().strftime("%Y%m%d_%H%M%S")}.txt'
|
||||||
|
mimetype = 'text/plain; charset=utf-8'
|
||||||
|
|
||||||
|
response = Response(
|
||||||
|
content.encode('utf-8'),
|
||||||
|
mimetype=mimetype,
|
||||||
|
headers={
|
||||||
|
'Content-Disposition': f'attachment; filename="{filename}"'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"导出失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'导出失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 业务功能API ====================
|
||||||
|
|
||||||
|
@app.route('/api/user/login', methods=['POST'])
|
||||||
|
def user_login():
|
||||||
|
"""用户登录"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
user_id = data.get('user_id', '').strip()
|
||||||
|
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请输入用户ID'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 获取用户数据(如果不存在会自动创建)
|
||||||
|
core = get_app_core()
|
||||||
|
user_data = core.get_user_data(user_id)
|
||||||
|
|
||||||
|
if user_data:
|
||||||
|
session['user_id'] = user_id
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'user_id': user_id,
|
||||||
|
'name': user_data.profile.get('name', '未设置')
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户数据获取失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"用户登录失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'登录失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/user/register', methods=['POST'])
|
||||||
|
def user_register():
|
||||||
|
"""用户注册"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
user_id = data.get('user_id', '').strip()
|
||||||
|
name = data.get('name', '').strip()
|
||||||
|
|
||||||
|
if not user_id or not name:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请输入用户ID和姓名'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
user_data = core.get_user_data(user_id)
|
||||||
|
|
||||||
|
# 更新用户基本信息
|
||||||
|
user_data.profile['name'] = name
|
||||||
|
core.data_manager.save_user_data(user_data)
|
||||||
|
|
||||||
|
session['user_id'] = user_id
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'user_id': user_id,
|
||||||
|
'name': name
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"用户注册失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'注册失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/questionnaire/submit', methods=['POST'])
|
||||||
|
def submit_questionnaire():
|
||||||
|
"""提交问卷"""
|
||||||
|
try:
|
||||||
|
user_id = session.get('user_id') or request.json.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请先登录'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
questionnaire_type = data.get('type', 'basic') # basic, taste, physiological
|
||||||
|
answers = data.get('answers', {})
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
input_data = {
|
||||||
|
'type': 'questionnaire',
|
||||||
|
'questionnaire_type': questionnaire_type,
|
||||||
|
'answers': answers
|
||||||
|
}
|
||||||
|
|
||||||
|
result = core.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id)
|
||||||
|
|
||||||
|
if result and result.result.get('success', False):
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '问卷提交成功'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': result.result.get('message', '问卷提交失败') if result else '处理失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"提交问卷失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'提交失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/meal/record', methods=['POST'])
|
||||||
|
def record_meal():
|
||||||
|
"""记录餐食"""
|
||||||
|
try:
|
||||||
|
user_id = session.get('user_id') or request.json.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请先登录'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
meal_data = {
|
||||||
|
'date': data.get('date', datetime.now().strftime('%Y-%m-%d')),
|
||||||
|
'meal_type': data.get('meal_type', 'lunch'), # breakfast, lunch, dinner
|
||||||
|
'foods': data.get('foods', []),
|
||||||
|
'quantities': data.get('quantities', []),
|
||||||
|
'calories': data.get('calories', 0),
|
||||||
|
'satisfaction_score': data.get('satisfaction_score', 3),
|
||||||
|
'notes': data.get('notes', '')
|
||||||
|
}
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
input_data = {
|
||||||
|
'type': 'meal_record',
|
||||||
|
**meal_data
|
||||||
|
}
|
||||||
|
|
||||||
|
result = core.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id)
|
||||||
|
|
||||||
|
if result and result.result.get('success', False):
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': '餐食记录成功'
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': result.result.get('message', '记录失败') if result else '处理失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"记录餐食失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'记录失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/recommendation/get', methods=['POST'])
|
||||||
|
def get_recommendations():
|
||||||
|
"""获取餐食推荐"""
|
||||||
|
try:
|
||||||
|
user_id = session.get('user_id') or request.json.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请先登录'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
meal_type = data.get('meal_type', 'lunch')
|
||||||
|
preferences = data.get('preferences', {})
|
||||||
|
context = data.get('context', {})
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
input_data = {
|
||||||
|
'type': 'meal_recommendation',
|
||||||
|
'meal_type': meal_type,
|
||||||
|
'preferences': preferences,
|
||||||
|
'context': context
|
||||||
|
}
|
||||||
|
|
||||||
|
result = core.process_user_request(ModuleType.RECOMMENDATION, input_data, user_id)
|
||||||
|
|
||||||
|
if result and result.result.get('success', False):
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'recommendations': result.result.get('recommendations', [])
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': result.result.get('message', '推荐失败') if result else '处理失败',
|
||||||
|
'recommendations': []
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取推荐失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'获取失败: {str(e)}',
|
||||||
|
'recommendations': []
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/analysis/nutrition', methods=['POST'])
|
||||||
|
def analyze_nutrition():
|
||||||
|
"""营养分析"""
|
||||||
|
try:
|
||||||
|
user_id = session.get('user_id') or request.json.get('user_id')
|
||||||
|
if not user_id:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请先登录'
|
||||||
|
}), 401
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
meal_data = data.get('meal_data', {})
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
input_data = {
|
||||||
|
'type': 'nutrition_analysis',
|
||||||
|
'meal_data': meal_data
|
||||||
|
}
|
||||||
|
|
||||||
|
result = core.process_user_request(ModuleType.USER_ANALYSIS, input_data, user_id)
|
||||||
|
|
||||||
|
if result and result.result.get('success', False):
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'analysis': result.result.get('analysis', {})
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': result.result.get('message', '分析失败') if result else '处理失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"营养分析失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'分析失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/health')
|
@app.route('/health')
|
||||||
def health():
|
def health():
|
||||||
"""健康检查"""
|
"""健康检查"""
|
||||||
|
|||||||
Reference in New Issue
Block a user