feat: 添加饮食文字AI识别功能与代码文档

- 饮食页面新增"文字AI识别"按钮,支持输入文字描述后自动识别卡路里
- 修复 database.py 中 time 模块导入冲突问题
- 新增 CODEMAPS.md 代码结构文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 15:37:43 +08:00
parent ac8c95d1a9
commit 3b1f7fb416
3 changed files with 335 additions and 3 deletions

View File

@@ -5037,7 +5037,8 @@ def get_meal_page_html() -> str:
</select>
<label for="meal-description">食物描述</label>
<textarea id="meal-description" name="description" rows="3"></textarea>
<textarea id="meal-description" name="description" rows="3" placeholder="例如:一碗米饭、两个鸡蛋、一份青菜"></textarea>
<button type="button" id="textRecognizeBtn" onclick="recognizeFoodFromText()" style="margin-top:8px; background:#10b981;">文字AI识别</button>
<label for="meal-calories">卡路里(可选,留空自动估算)</label>
<input type="number" id="meal-calories" name="calories" min="0" inputmode="numeric">
@@ -5075,6 +5076,7 @@ def get_meal_page_html() -> str:
// 重置识别状态
document.getElementById('recognizeBtn').style.display = 'none';
document.getElementById('recognizeStatus').style.display = 'none';
document.getElementById('textRecognizeBtn').disabled = false;
}
// ESC 键关闭 Modal
document.addEventListener('keydown', (e) => {
@@ -5161,6 +5163,73 @@ def get_meal_page_html() -> str:
}
}
async function recognizeFoodFromText() {
const descriptionInput = document.getElementById('meal-description');
const statusDiv = document.getElementById('recognizeStatus');
const textRecognizeBtn = document.getElementById('textRecognizeBtn');
const text = descriptionInput.value.trim();
if (!text) {
alert('请先输入食物描述');
return;
}
// 显示识别中状态
statusDiv.style.display = 'block';
statusDiv.style.background = '#e3f2fd';
statusDiv.style.color = '#1976d2';
statusDiv.innerHTML = '🤖 正在识别食物,请稍候...';
textRecognizeBtn.disabled = true;
try {
const formData = new FormData();
formData.append('text', text);
formData.append('provider', 'qwen');
const res = await fetch('/api/meal/recognize', {
method: 'POST',
body: formData,
});
const result = await res.json();
if (res.ok && result.success) {
// 显示成功状态
statusDiv.style.background = '#e8f5e9';
statusDiv.style.color = '#2e7d32';
statusDiv.innerHTML = `✅ 识别成功!<br>食物:${result.description}<br>卡路里:${result.total_calories} 卡`;
// 自动填充表单(更新描述为标准化的描述)
const form = document.getElementById('addMealForm');
if (result.description) {
form.querySelector('[name="description"]').value = result.description;
}
form.querySelector('[name="calories"]').value = result.total_calories || '';
if (result.total_protein) {
form.querySelector('[name="protein"]').value = result.total_protein.toFixed(1);
}
if (result.total_carbs) {
form.querySelector('[name="carbs"]').value = result.total_carbs.toFixed(1);
}
if (result.total_fat) {
form.querySelector('[name="fat"]').value = result.total_fat.toFixed(1);
}
} else {
// 显示错误状态
statusDiv.style.background = '#ffebee';
statusDiv.style.color = '#c62828';
statusDiv.innerHTML = `❌ 识别失败:${result.detail || '未知错误'}`;
}
} catch (error) {
// 显示错误状态
statusDiv.style.background = '#ffebee';
statusDiv.style.color = '#c62828';
statusDiv.innerHTML = `❌ 识别失败:${error.message}`;
} finally {
textRecognizeBtn.disabled = false;
}
}
function formatPhotoUrl(path) {
if (!path) return null;
const marker = '/.vitals/photos/';