feat: complete web app features and fix encoding
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;
|
||||
}
|
||||
|
||||
/* 导出按钮样式 */
|
||||
.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 {
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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 sortedList = document.getElementById('sortedList');
|
||||
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 () => {
|
||||
@@ -55,6 +122,7 @@ extractBtn.addEventListener('click', async () => {
|
||||
displayExtractedItems(extractedItems);
|
||||
extractedSection.style.display = 'block';
|
||||
textInput.disabled = true;
|
||||
saveToStorage(); // 保存到本地存储
|
||||
} else {
|
||||
alert(data.message || '提取失败');
|
||||
}
|
||||
@@ -108,6 +176,7 @@ sortBtn.addEventListener('click', async () => {
|
||||
wheelSection.style.display = 'block';
|
||||
resultSection.style.display = 'block';
|
||||
currentSpinIndex = 0;
|
||||
saveToStorage(); // 保存到本地存储
|
||||
} else {
|
||||
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', () => {
|
||||
extractedItems = [];
|
||||
@@ -281,5 +394,7 @@ resetBtn.addEventListener('click', () => {
|
||||
|
||||
isSpinning = 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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user