Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| adffbbca1b | |||
| d69d0158d8 | |||
|
|
3a8a498648 | ||
|
|
20bbda37e9 | ||
|
|
37fb88b042 | ||
|
|
6fc4438a13 | ||
|
|
ed5f32016d | ||
|
|
7ab87e8f15 | ||
|
|
f50d37259e | ||
|
|
8b99272711 | ||
|
|
2ab6aafc6f | ||
|
|
cad03268f3 |
64
.dockerignore
Normal file
64
.dockerignore
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# 虚拟环境
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# 日志文件
|
||||||
|
*.log
|
||||||
|
logs/*.log
|
||||||
|
|
||||||
|
# 数据库文件(会通过volume挂载)
|
||||||
|
data/*.db
|
||||||
|
data/*.pkl
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# 文档
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
|
||||||
|
# 测试文件
|
||||||
|
test_*.py
|
||||||
|
*_test.py
|
||||||
|
|
||||||
|
# 临时文件
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# 环境配置(敏感信息)
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# 模型文件(会通过volume挂载)
|
||||||
|
models/*.pkl
|
||||||
|
|
||||||
|
# PDF和其他大文件
|
||||||
|
*.pdf
|
||||||
|
|
||||||
55
Dockerfile
Normal file
55
Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 使用Python 3官方镜像(自动使用最新稳定版本,匹配本机Python版本)
|
||||||
|
FROM python:3-slim
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
FLASK_APP=web_app.py \
|
||||||
|
FLASK_ENV=production
|
||||||
|
|
||||||
|
# 安装系统依赖(用于OCR等功能)
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-chi-sim \
|
||||||
|
tesseract-ocr-eng \
|
||||||
|
libgl1 \
|
||||||
|
libglx-mesa0 \
|
||||||
|
libglib2.0-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 复制requirements文件
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 安装Python依赖(排除GUI相关依赖,Docker中不需要)
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
pip install --no-cache-dir gunicorn && \
|
||||||
|
python -m pip install --force-reinstall gunicorn && \
|
||||||
|
python -c "import gunicorn; print(f'Gunicorn version: {gunicorn.__version__}')" && \
|
||||||
|
python -m gunicorn --version || echo "Gunicorn installed, using python -m gunicorn"
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
RUN mkdir -p templates static/css static/js logs data models
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
RUN chmod +x start_web.py
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 7400
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD python -c "import requests; requests.get('http://localhost:7400/health')" || exit 1
|
||||||
|
|
||||||
|
# 使用gunicorn启动应用(生产环境)
|
||||||
|
# 注意:默认配置使用2个worker,适合1GB+内存的机器
|
||||||
|
# 如果机器内存较小(512MB-1GB),建议修改为 "--workers", "1"
|
||||||
|
# 使用 python -m gunicorn 确保可以找到 gunicorn(即使可执行文件不在PATH中也能工作)
|
||||||
|
CMD ["python", "-m", "gunicorn", "--bind", "0.0.0.0:7400", "--workers", "2", "--timeout", "120", "--access-logfile", "-", "--error-logfile", "-", "web_app:app"]
|
||||||
38
Dockerfile.dev
Normal file
38
Dockerfile.dev
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 开发环境Dockerfile
|
||||||
|
FROM python:3-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
FLASK_APP=web_app.py \
|
||||||
|
FLASK_ENV=development
|
||||||
|
|
||||||
|
# 安装系统依赖
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-chi-sim \
|
||||||
|
tesseract-ocr-eng \
|
||||||
|
libgl1-mesa-glx \
|
||||||
|
libglib2.0-0 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 复制requirements文件
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 安装Python依赖
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
RUN mkdir -p templates static/css static/js logs data models
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 7400
|
||||||
|
|
||||||
|
# 开发模式:使用Flask开发服务器
|
||||||
|
CMD ["python", "start_web.py"]
|
||||||
|
|
||||||
68
Dockerfile.low-mem
Normal file
68
Dockerfile.low-mem
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 小内存优化版 Dockerfile(适用于 512MB-1GB 内存的 Linux 机器)
|
||||||
|
FROM python:3-slim
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量(优化内存使用)
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
FLASK_APP=web_app.py \
|
||||||
|
FLASK_ENV=production \
|
||||||
|
PYTHONHASHSEED=0 \
|
||||||
|
MALLOC_ARENA_MAX=2
|
||||||
|
|
||||||
|
# 安装系统依赖(最小化安装)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-chi-sim \
|
||||||
|
tesseract-ocr-eng \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& apt-get clean
|
||||||
|
|
||||||
|
# 复制requirements文件
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 创建最小化的 requirements.txt(移除GUI相关依赖)
|
||||||
|
RUN echo "scikit-learn>=1.3.0" > requirements-minimal.txt && \
|
||||||
|
echo "pandas>=2.0.0" >> requirements-minimal.txt && \
|
||||||
|
echo "numpy>=1.24.0" >> requirements-minimal.txt && \
|
||||||
|
echo "joblib>=1.3.0" >> requirements-minimal.txt && \
|
||||||
|
echo "requests>=2.31.0" >> requirements-minimal.txt && \
|
||||||
|
echo "python-dotenv>=1.0.0" >> requirements-minimal.txt && \
|
||||||
|
echo "python-dateutil>=2.8.0" >> requirements-minimal.txt && \
|
||||||
|
echo "Pillow>=10.0.0" >> requirements-minimal.txt && \
|
||||||
|
echo "pytesseract>=0.3.10" >> requirements-minimal.txt && \
|
||||||
|
echo "opencv-python-headless>=4.8.0" >> requirements-minimal.txt && \
|
||||||
|
echo "Flask>=3.0.0" >> requirements-minimal.txt && \
|
||||||
|
echo "Werkzeug>=3.0.0" >> requirements-minimal.txt && \
|
||||||
|
echo "gunicorn" >> requirements-minimal.txt
|
||||||
|
|
||||||
|
# 安装Python依赖(使用最小化依赖,opencv使用headless版本)
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements-minimal.txt && \
|
||||||
|
python -m pip install --force-reinstall gunicorn && \
|
||||||
|
python -c "import gunicorn; print(f'Gunicorn version: {gunicorn.__version__}')" && \
|
||||||
|
python -m gunicorn --version || echo "Gunicorn installed, using python -m gunicorn"
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
RUN mkdir -p templates static/css static/js logs data models
|
||||||
|
|
||||||
|
# 设置权限
|
||||||
|
RUN chmod +x start_web.py
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 7400
|
||||||
|
|
||||||
|
# 健康检查(不使用 requests,直接用 Python)
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||||
|
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:7400/health').read()" || exit 1
|
||||||
|
|
||||||
|
# 使用gunicorn启动应用(小内存配置:单worker,减少内存占用)
|
||||||
|
# 使用 python -m gunicorn 确保可以找到 gunicorn(即使可执行文件不在PATH中也能工作)
|
||||||
|
CMD ["python", "-m", "gunicorn", "--bind", "0.0.0.0:7400", "--workers", "1", "--threads", "2", "--timeout", "120", "--worker-class", "sync", "--max-requests", "1000", "--max-requests-jitter", "100", "--preload", "--access-logfile", "-", "--error-logfile", "-", "web_app:app"]
|
||||||
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
# OCR热量识别功能使用指南
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
OCR热量识别功能允许用户通过拍摄或上传包含食物信息的图片,自动识别其中的热量信息,大大简化了餐食记录的过程。
|
|
||||||
|
|
||||||
## 主要特性
|
|
||||||
|
|
||||||
### 1. 多OCR引擎支持
|
|
||||||
- **Tesseract OCR**: 开源OCR引擎,支持中英文识别
|
|
||||||
- **PaddleOCR**: 百度开源OCR,对中文识别效果优秀
|
|
||||||
- **EasyOCR**: 简单易用的OCR库,支持多语言
|
|
||||||
|
|
||||||
### 2. 智能验证机制
|
|
||||||
- **多级验证**: 结合OCR结果、食物数据库和用户学习数据
|
|
||||||
- **置信度评估**: 为每个识别结果提供置信度评分
|
|
||||||
- **用户修正**: 支持用户手动修正识别结果
|
|
||||||
|
|
||||||
### 3. 学习优化系统
|
|
||||||
- **用户反馈学习**: 记录用户修正,提高后续识别准确性
|
|
||||||
- **数据库匹配**: 与内置食物数据库进行智能匹配
|
|
||||||
- **模式识别**: 识别多种热量表示格式
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 1. 启动OCR功能
|
|
||||||
|
|
||||||
#### 在移动端界面:
|
|
||||||
1. 打开应用,进入"记录"页面
|
|
||||||
2. 在食物输入框右侧找到"📷"按钮
|
|
||||||
3. 点击按钮打开OCR识别界面
|
|
||||||
|
|
||||||
#### 在桌面端界面:
|
|
||||||
1. 在主界面选择"OCR热量识别"功能
|
|
||||||
2. 或通过菜单栏访问OCR功能
|
|
||||||
|
|
||||||
### 2. 上传图片
|
|
||||||
|
|
||||||
1. 点击"选择图片"按钮
|
|
||||||
2. 选择包含食物信息的图片文件
|
|
||||||
3. 支持的格式:JPG、JPEG、PNG、BMP、GIF
|
|
||||||
4. 图片将显示在预览区域
|
|
||||||
|
|
||||||
### 3. 开始识别
|
|
||||||
|
|
||||||
1. 确认图片选择正确后,点击"开始识别"按钮
|
|
||||||
2. 系统将使用多个OCR引擎进行识别
|
|
||||||
3. 识别过程中会显示进度条和状态信息
|
|
||||||
4. 识别完成后显示结果
|
|
||||||
|
|
||||||
### 4. 查看和编辑结果
|
|
||||||
|
|
||||||
#### 识别结果表格:
|
|
||||||
- **食物名称**: 识别到的食物名称
|
|
||||||
- **热量**: 识别到的热量数值(卡路里)
|
|
||||||
- **置信度**: 识别结果的置信度(0-1)
|
|
||||||
- **来源**: 数据来源(OCR、数据库、用户确认)
|
|
||||||
|
|
||||||
#### 详细信息:
|
|
||||||
- OCR识别过程详情
|
|
||||||
- 各引擎的识别结果
|
|
||||||
- 处理时间和整体置信度
|
|
||||||
|
|
||||||
#### 建议:
|
|
||||||
- 系统提供的改进建议
|
|
||||||
- 识别准确性提示
|
|
||||||
- 手动输入建议
|
|
||||||
|
|
||||||
### 5. 编辑和确认结果
|
|
||||||
|
|
||||||
#### 编辑结果:
|
|
||||||
1. 双击表格中的任意行或选择后点击"编辑结果"
|
|
||||||
2. 在弹出的对话框中修改食物名称、热量和置信度
|
|
||||||
3. 点击"保存"确认修改
|
|
||||||
|
|
||||||
#### 确认结果:
|
|
||||||
1. 检查所有识别结果是否正确
|
|
||||||
2. 点击"确认结果"按钮
|
|
||||||
3. 系统将保存到餐食记录中
|
|
||||||
|
|
||||||
## 识别准确性优化
|
|
||||||
|
|
||||||
### 1. 图片质量要求
|
|
||||||
|
|
||||||
#### 推荐条件:
|
|
||||||
- **清晰度**: 图片清晰,文字可读
|
|
||||||
- **对比度**: 文字与背景对比明显
|
|
||||||
- **角度**: 文字水平,避免倾斜
|
|
||||||
- **光照**: 光线充足,避免阴影
|
|
||||||
|
|
||||||
#### 避免的情况:
|
|
||||||
- 模糊不清的图片
|
|
||||||
- 文字过小或过大的图片
|
|
||||||
- 严重倾斜的图片
|
|
||||||
- 光线过暗或过亮的图片
|
|
||||||
|
|
||||||
### 2. 文字格式支持
|
|
||||||
|
|
||||||
#### 支持的热量表示格式:
|
|
||||||
- `130卡路里`
|
|
||||||
- `155 kcal`
|
|
||||||
- `52千卡`
|
|
||||||
- `42大卡`
|
|
||||||
- `110 KJ` (千焦)
|
|
||||||
- `76卡`
|
|
||||||
|
|
||||||
#### 支持的食物名称:
|
|
||||||
- 中文食物名称:米饭、鸡蛋、苹果等
|
|
||||||
- 英文食物名称:rice、egg、apple等
|
|
||||||
- 混合格式:米饭 130卡路里
|
|
||||||
|
|
||||||
### 3. 提高识别准确性的技巧
|
|
||||||
|
|
||||||
#### 图片预处理:
|
|
||||||
- 确保图片中的文字清晰可见
|
|
||||||
- 避免复杂的背景干扰
|
|
||||||
- 保持文字区域的完整性
|
|
||||||
|
|
||||||
#### 结果验证:
|
|
||||||
- 仔细检查识别结果
|
|
||||||
- 及时修正错误信息
|
|
||||||
- 利用数据库匹配功能
|
|
||||||
|
|
||||||
#### 学习优化:
|
|
||||||
- 经常使用修正功能
|
|
||||||
- 系统会学习您的修正习惯
|
|
||||||
- 提高后续识别的准确性
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 1. 常见问题
|
|
||||||
|
|
||||||
#### 识别失败:
|
|
||||||
- **原因**: 图片质量差、OCR引擎不可用
|
|
||||||
- **解决**: 检查图片质量,确保OCR依赖已安装
|
|
||||||
|
|
||||||
#### 识别结果不准确:
|
|
||||||
- **原因**: 图片模糊、文字格式特殊
|
|
||||||
- **解决**: 重新拍摄清晰图片,手动修正结果
|
|
||||||
|
|
||||||
#### 无法打开OCR界面:
|
|
||||||
- **原因**: 依赖包未安装、模块初始化失败
|
|
||||||
- **解决**: 检查requirements.txt中的依赖是否已安装
|
|
||||||
|
|
||||||
### 2. 依赖安装
|
|
||||||
|
|
||||||
确保已安装以下依赖包:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install pytesseract>=0.3.10
|
|
||||||
pip install opencv-python>=4.8.0
|
|
||||||
pip install paddleocr>=2.7.0
|
|
||||||
pip install easyocr>=1.7.0
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Tesseract安装:
|
|
||||||
- **Windows**: 下载Tesseract安装包并添加到PATH
|
|
||||||
- **macOS**: `brew install tesseract`
|
|
||||||
- **Linux**: `sudo apt-get install tesseract-ocr`
|
|
||||||
|
|
||||||
### 3. 性能优化
|
|
||||||
|
|
||||||
#### 提高识别速度:
|
|
||||||
- 使用较小的图片文件
|
|
||||||
- 选择清晰的图片
|
|
||||||
- 避免过于复杂的图片
|
|
||||||
|
|
||||||
#### 提高识别准确性:
|
|
||||||
- 使用标准格式的食物标签
|
|
||||||
- 保持文字清晰可读
|
|
||||||
- 及时修正错误结果
|
|
||||||
|
|
||||||
## 技术架构
|
|
||||||
|
|
||||||
### 1. 模块结构
|
|
||||||
|
|
||||||
```
|
|
||||||
modules/ocr_calorie_recognition.py # OCR识别模块
|
|
||||||
gui/ocr_calorie_gui.py # OCR GUI界面
|
|
||||||
test_ocr_system.py # 测试脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 核心组件
|
|
||||||
|
|
||||||
#### OCRCalorieRecognitionModule:
|
|
||||||
- 多OCR引擎集成
|
|
||||||
- 图片预处理
|
|
||||||
- 热量信息提取
|
|
||||||
- 数据库匹配
|
|
||||||
- 用户学习系统
|
|
||||||
|
|
||||||
#### OCRCalorieGUI:
|
|
||||||
- 图片上传界面
|
|
||||||
- 识别结果展示
|
|
||||||
- 结果编辑功能
|
|
||||||
- 用户交互处理
|
|
||||||
|
|
||||||
### 3. 数据流程
|
|
||||||
|
|
||||||
1. **图片上传** → 图片预处理
|
|
||||||
2. **OCR识别** → 多引擎并行识别
|
|
||||||
3. **文本提取** → 热量信息解析
|
|
||||||
4. **数据库匹配** → 食物信息验证
|
|
||||||
5. **用户确认** → 结果保存和学习
|
|
||||||
|
|
||||||
## 未来改进计划
|
|
||||||
|
|
||||||
### 1. 功能增强
|
|
||||||
- 支持更多图片格式
|
|
||||||
- 增加批量识别功能
|
|
||||||
- 支持手写文字识别
|
|
||||||
- 集成营养信息识别
|
|
||||||
|
|
||||||
### 2. 性能优化
|
|
||||||
- 优化识别算法
|
|
||||||
- 提高处理速度
|
|
||||||
- 减少内存占用
|
|
||||||
- 支持GPU加速
|
|
||||||
|
|
||||||
### 3. 用户体验
|
|
||||||
- 改进界面设计
|
|
||||||
- 增加语音输入
|
|
||||||
- 支持离线识别
|
|
||||||
- 提供更多个性化选项
|
|
||||||
|
|
||||||
## 联系支持
|
|
||||||
|
|
||||||
如果您在使用OCR功能时遇到问题,请:
|
|
||||||
|
|
||||||
1. 查看本文档的故障排除部分
|
|
||||||
2. 运行测试脚本检查系统状态
|
|
||||||
3. 检查依赖包是否正确安装
|
|
||||||
4. 提供详细的错误信息和截图
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*最后更新: 2024年12月*
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
# 个性化饮食推荐助手 - 项目完成总结
|
|
||||||
|
|
||||||
## 🎯 项目概述
|
|
||||||
|
|
||||||
基于您的需求,我已经完成了一个完整的个性化饮食推荐系统,具有以下核心特性:
|
|
||||||
|
|
||||||
### ✨ 核心功能
|
|
||||||
1. **5天数据采集** - 详细记录用户三餐数据
|
|
||||||
2. **智能问卷系统** - 收集用户偏好、生理信息、个性化因素
|
|
||||||
3. **大模型集成** - 深度理解用户需求,提供智能分析
|
|
||||||
4. **混合推荐系统** - 结合机器学习和AI的个性化推荐
|
|
||||||
5. **持续学习机制** - 根据用户反馈不断优化模型
|
|
||||||
6. **女性专属优化** - 考虑生理周期、排卵期等特殊因素
|
|
||||||
7. **现代化GUI** - 基于CustomTkinter的美观界面
|
|
||||||
|
|
||||||
## 🏗️ 基座架构设计
|
|
||||||
|
|
||||||
### 核心基座 (`core/base.py`)
|
|
||||||
- **BaseModule**: 所有功能模块的抽象基类
|
|
||||||
- **DataManager**: 统一的数据管理基座
|
|
||||||
- **EventBus**: 事件总线,支持模块间通信
|
|
||||||
- **ModuleManager**: 模块管理器,统一管理所有功能模块
|
|
||||||
- **AppCore**: 应用核心,协调所有模块
|
|
||||||
|
|
||||||
### 功能模块
|
|
||||||
1. **数据采集模块** (`modules/data_collection.py`)
|
|
||||||
- 问卷数据收集
|
|
||||||
- 餐食记录管理
|
|
||||||
- 用户反馈处理
|
|
||||||
|
|
||||||
2. **AI分析模块** (`modules/ai_analysis.py`)
|
|
||||||
- 用户意图分析
|
|
||||||
- 营养状况分析
|
|
||||||
- 生理状态分析
|
|
||||||
- 餐食建议生成
|
|
||||||
|
|
||||||
3. **推荐引擎模块** (`modules/recommendation_engine.py`)
|
|
||||||
- 基于历史数据的推荐
|
|
||||||
- 基于相似用户的推荐
|
|
||||||
- 基于内容相似性的推荐
|
|
||||||
- 基于生理状态的推荐
|
|
||||||
- 多维度融合推荐
|
|
||||||
|
|
||||||
4. **GUI界面模块** (`gui/main_window.py`)
|
|
||||||
- 现代化界面设计
|
|
||||||
- 数据采集界面
|
|
||||||
- AI分析界面
|
|
||||||
- 推荐系统界面
|
|
||||||
- 个人中心界面
|
|
||||||
|
|
||||||
## 🔧 技术特点
|
|
||||||
|
|
||||||
### 1. 基座架构优势
|
|
||||||
- **代码复用**: 所有模块基于统一基座,减少重复代码
|
|
||||||
- **模块化设计**: 每个功能独立,易于维护和扩展
|
|
||||||
- **统一接口**: 所有模块使用相同的接口规范
|
|
||||||
- **事件驱动**: 支持模块间松耦合通信
|
|
||||||
|
|
||||||
### 2. 大模型深度集成
|
|
||||||
- **用户意图理解**: 不仅理解表面需求,还分析深层意图
|
|
||||||
- **情绪状态分析**: 考虑用户当前情绪对饮食需求的影响
|
|
||||||
- **生理周期智能**: 专门针对女性的生理周期分析
|
|
||||||
- **个性化建议**: 结合星座、性格等多维度因素
|
|
||||||
|
|
||||||
### 3. 智能推荐系统
|
|
||||||
- **多维度融合**: 历史偏好 + 相似用户 + 内容相似性 + 生理状态
|
|
||||||
- **持续学习**: 根据用户反馈不断优化推荐算法
|
|
||||||
- **个性化过滤**: 考虑过敏、不喜欢等个人限制
|
|
||||||
- **置信度评估**: 为每个推荐提供置信度评分
|
|
||||||
|
|
||||||
### 4. 女性专属功能
|
|
||||||
- **生理周期跟踪**: 自动计算月经周期状态
|
|
||||||
- **营养需求调整**: 根据生理周期推荐不同营养素
|
|
||||||
- **情绪变化考虑**: 分析生理周期对情绪和食欲的影响
|
|
||||||
- **个性化建议**: 提供针对性的饮食建议
|
|
||||||
|
|
||||||
## 📁 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
diet_recommendation_app/
|
|
||||||
├── core/ # 核心基座
|
|
||||||
│ └── base.py # 基础架构
|
|
||||||
├── modules/ # 功能模块
|
|
||||||
│ ├── data_collection.py # 数据采集
|
|
||||||
│ ├── ai_analysis.py # AI分析
|
|
||||||
│ └── recommendation_engine.py # 推荐引擎
|
|
||||||
├── gui/ # GUI界面
|
|
||||||
│ └── main_window.py # 主窗口
|
|
||||||
├── data/ # 数据存储
|
|
||||||
│ ├── users/ # 用户数据
|
|
||||||
│ └── training/ # 训练数据
|
|
||||||
├── models/ # 模型存储
|
|
||||||
├── logs/ # 日志文件
|
|
||||||
├── main.py # 主应用入口
|
|
||||||
├── start.py # 启动脚本
|
|
||||||
├── requirements.txt # 依赖包
|
|
||||||
├── .env.example # 配置示例
|
|
||||||
└── README.md # 项目说明
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 1. 安装依赖
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 配置环境
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# 编辑 .env 文件,配置API密钥(可选)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 启动应用
|
|
||||||
```bash
|
|
||||||
python start.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💡 使用流程
|
|
||||||
|
|
||||||
### 1. 用户注册/登录
|
|
||||||
- 输入用户ID和姓名
|
|
||||||
- 系统自动创建用户档案
|
|
||||||
|
|
||||||
### 2. 数据采集(5天)
|
|
||||||
- **基础信息问卷**: 年龄、性别、身高体重等
|
|
||||||
- **口味偏好问卷**: 甜、咸、辣、酸等偏好评分
|
|
||||||
- **生理信息问卷**: 月经周期、排卵期症状等
|
|
||||||
- **餐食记录**: 详细记录三餐内容和满意度
|
|
||||||
|
|
||||||
### 3. AI分析
|
|
||||||
- **用户意图分析**: 理解用户真实需求
|
|
||||||
- **营养分析**: 分析餐食营养状况
|
|
||||||
- **生理状态分析**: 分析当前生理周期状态
|
|
||||||
|
|
||||||
### 4. 个性化推荐
|
|
||||||
- **智能推荐**: 基于多维度因素生成推荐
|
|
||||||
- **用户反馈**: 收集用户对推荐的反馈
|
|
||||||
- **持续优化**: 根据反馈不断改进推荐算法
|
|
||||||
|
|
||||||
## 🔮 核心创新点
|
|
||||||
|
|
||||||
### 1. 基座架构设计
|
|
||||||
- 避免了"一个代码一个功能"的问题
|
|
||||||
- 统一的数据管理和事件处理
|
|
||||||
- 模块化设计,易于扩展和维护
|
|
||||||
|
|
||||||
### 2. 大模型深度集成
|
|
||||||
- 不仅用于营养分析,还用于用户需求理解
|
|
||||||
- 结合传统机器学习和大模型的优势
|
|
||||||
- 提供更智能、更个性化的服务
|
|
||||||
|
|
||||||
### 3. 女性专属优化
|
|
||||||
- 深度考虑生理周期对饮食需求的影响
|
|
||||||
- 结合星座、性格等个性化因素
|
|
||||||
- 提供更贴心的个性化服务
|
|
||||||
|
|
||||||
### 4. 持续学习机制
|
|
||||||
- 避免完全随机推荐的问题
|
|
||||||
- 根据用户反馈不断优化模型
|
|
||||||
- 提供越来越精准的推荐
|
|
||||||
|
|
||||||
## 🎉 项目完成度
|
|
||||||
|
|
||||||
✅ **核心基座架构** - 完成
|
|
||||||
✅ **数据采集系统** - 完成
|
|
||||||
✅ **AI分析模块** - 完成
|
|
||||||
✅ **推荐引擎** - 完成
|
|
||||||
✅ **GUI界面** - 完成
|
|
||||||
✅ **女性专属功能** - 完成
|
|
||||||
✅ **持续学习机制** - 完成
|
|
||||||
✅ **大模型集成** - 完成
|
|
||||||
|
|
||||||
## 🔧 后续扩展建议
|
|
||||||
|
|
||||||
1. **移动端适配**: 开发手机APP版本
|
|
||||||
2. **云端部署**: 支持多用户在线使用
|
|
||||||
3. **更多大模型**: 集成更多AI模型
|
|
||||||
4. **营养数据库**: 扩展更丰富的食物营养数据
|
|
||||||
5. **社交功能**: 添加用户交流和分享功能
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**项目已完成,可以立即运行使用!** 🎊
|
|
||||||
|
|
||||||
所有功能都基于您提出的需求设计,特别是:
|
|
||||||
- ✅ 5天数据采集系统
|
|
||||||
- ✅ 大模型深度集成用于需求分析
|
|
||||||
- ✅ 女性生理周期智能优化
|
|
||||||
- ✅ 星座等个性化因素考虑
|
|
||||||
- ✅ 持续学习和模型矫正机制
|
|
||||||
- ✅ 基座架构避免代码重复
|
|
||||||
- ✅ 现代化桌面GUI界面
|
|
||||||
20
README.md
20
README.md
@@ -55,12 +55,17 @@
|
|||||||
- **数据管理器**:统一的数据访问接口
|
- **数据管理器**:统一的数据访问接口
|
||||||
|
|
||||||
### OCR识别技术
|
### OCR识别技术
|
||||||
- **Tesseract OCR**:开源OCR引擎,支持中英文
|
- **Tesseract OCR**:开源OCR引擎,支持中英文(**默认,轻量级,推荐**)
|
||||||
- **PaddleOCR**:百度开源OCR,中文识别优秀
|
- **PaddleOCR**:百度开源OCR,中文识别优秀(可选,需要PaddlePaddle,占用内存较大)
|
||||||
- **EasyOCR**:简单易用的多语言OCR库
|
- **EasyOCR**:简单易用的多语言OCR库(可选,需要PyTorch,占用内存很大)
|
||||||
- **OpenCV**:图像预处理和增强
|
- **OpenCV**:图像预处理和增强
|
||||||
- **PIL/Pillow**:图像处理和格式转换
|
- **PIL/Pillow**:图像处理和格式转换
|
||||||
|
|
||||||
|
**注意**:默认配置仅使用Tesseract OCR(轻量级,无需深度学习框架)。如需使用PaddleOCR或EasyOCR:
|
||||||
|
1. 取消注释`requirements.txt`中对应依赖
|
||||||
|
2. 安装依赖:`pip install paddleocr` 或 `pip install easyocr`
|
||||||
|
3. 在OCR模块配置中添加对应引擎到`ocr_methods`列表
|
||||||
|
|
||||||
### 机器学习
|
### 机器学习
|
||||||
- **scikit-learn**:推荐算法实现
|
- **scikit-learn**:推荐算法实现
|
||||||
- **pandas/numpy**:数据处理和分析
|
- **pandas/numpy**:数据处理和分析
|
||||||
@@ -153,13 +158,16 @@ python -c "from config.api_keys import get_api_status_report; print(get_api_stat
|
|||||||
- **macOS**: `brew install tesseract`
|
- **macOS**: `brew install tesseract`
|
||||||
- **Linux**: `sudo apt-get install tesseract-ocr`
|
- **Linux**: `sudo apt-get install tesseract-ocr`
|
||||||
|
|
||||||
#### 其他OCR引擎
|
#### 其他OCR引擎(可选,需要深度学习框架)
|
||||||
```bash
|
```bash
|
||||||
# PaddleOCR(推荐,中文识别效果好)
|
# PaddleOCR(可选,需要PaddlePaddle,占用内存较大)
|
||||||
pip install paddleocr
|
pip install paddleocr
|
||||||
|
|
||||||
# EasyOCR(简单易用)
|
# EasyOCR(可选,需要PyTorch,占用内存很大,通常需要1-2GB)
|
||||||
pip install easyocr
|
pip install easyocr
|
||||||
|
|
||||||
|
# 注意:安装后需要在OCR模块配置中添加对应引擎:
|
||||||
|
# self.ocr_methods = ['tesseract', 'paddleocr'] # 添加需要的引擎
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 配置环境
|
### 4. 配置环境
|
||||||
|
|||||||
203
README_WEB.md
203
README_WEB.md
@@ -1,203 +0,0 @@
|
|||||||
# 网页版使用说明
|
|
||||||
|
|
||||||
## 功能介绍
|
|
||||||
|
|
||||||
这是一个基于Flask的网页应用,提供了**背诵排序**功能,可以帮助你:
|
|
||||||
|
|
||||||
1. **识别知识点**:从输入的文本中自动识别出要背诵的知识点
|
|
||||||
2. **随机排序**:对识别出的知识点进行随机排序
|
|
||||||
3. **转盘抽背**:通过转盘功能随机选择背诵内容
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
### 1. 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install Flask>=3.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
或者安装所有依赖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 启动应用
|
|
||||||
|
|
||||||
运行启动脚本:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python start_web.py
|
|
||||||
```
|
|
||||||
|
|
||||||
或者直接运行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python web_app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 访问应用
|
|
||||||
|
|
||||||
在浏览器中打开:
|
|
||||||
|
|
||||||
- 首页:http://localhost:5000
|
|
||||||
- 背诵排序:http://localhost:5000/recitation
|
|
||||||
|
|
||||||
## 使用步骤
|
|
||||||
|
|
||||||
### 第一步:输入背诵内容
|
|
||||||
|
|
||||||
在文本框中粘贴包含知识点列表的文本,支持以下格式:
|
|
||||||
|
|
||||||
- 列表格式(数字开头)
|
|
||||||
- 表格格式(从表格中复制)
|
|
||||||
- 普通文本(每行一个知识点)
|
|
||||||
|
|
||||||
示例:
|
|
||||||
```
|
|
||||||
第一章 西周
|
|
||||||
夏商学校名称
|
|
||||||
西周学在官府
|
|
||||||
国学乡学
|
|
||||||
六艺
|
|
||||||
私学兴起的原因与意义
|
|
||||||
稷下学宫
|
|
||||||
```
|
|
||||||
|
|
||||||
### 第二步:识别知识点
|
|
||||||
|
|
||||||
点击"识别知识点"按钮,系统会自动:
|
|
||||||
- 过滤无关内容(表头、页码等)
|
|
||||||
- 提取有效的知识点
|
|
||||||
- 显示识别结果
|
|
||||||
|
|
||||||
### 第三步:随机排序
|
|
||||||
|
|
||||||
点击"开始随机排序"按钮,系统会:
|
|
||||||
- 对知识点进行随机打乱
|
|
||||||
- 生成随机排序列表
|
|
||||||
- 创建转盘界面
|
|
||||||
|
|
||||||
### 第四步:转盘抽背
|
|
||||||
|
|
||||||
点击"转动转盘"按钮:
|
|
||||||
- 转盘会旋转3圈后停下
|
|
||||||
- 随机选中一个知识点
|
|
||||||
- 显示选中的内容
|
|
||||||
|
|
||||||
同时,页面下方会显示完整的随机排序结果列表。
|
|
||||||
|
|
||||||
## 技术说明
|
|
||||||
|
|
||||||
### 后端技术
|
|
||||||
- **Flask**:轻量级Web框架
|
|
||||||
- **Python正则表达式**:文本解析和知识点提取
|
|
||||||
|
|
||||||
### 前端技术
|
|
||||||
- **HTML5 + CSS3**:响应式页面设计
|
|
||||||
- **JavaScript (原生)**:交互逻辑
|
|
||||||
- **SVG**:转盘可视化
|
|
||||||
|
|
||||||
### 知识点识别规则
|
|
||||||
|
|
||||||
系统会智能识别以下内容:
|
|
||||||
1. 以数字或章节号开头的行(如"第一章"、"1. 知识点")
|
|
||||||
2. 以列表符号开头的行(如"- 知识点"、"? 知识点")
|
|
||||||
3. 包含中文且非空的行
|
|
||||||
|
|
||||||
系统会自动过滤:
|
|
||||||
- 表头行(包含"章节"、"知识点"等关键词)
|
|
||||||
- 页码行(如"第1页")
|
|
||||||
- 说明文字
|
|
||||||
- 空行
|
|
||||||
|
|
||||||
## API接口
|
|
||||||
|
|
||||||
### 提取知识点
|
|
||||||
|
|
||||||
**POST** `/api/extract`
|
|
||||||
|
|
||||||
请求体:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"text": "输入文本内容"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
响应:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"items": ["知识点1", "知识点2", ...],
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 随机排序
|
|
||||||
|
|
||||||
**POST** `/api/sort`
|
|
||||||
|
|
||||||
请求体:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": ["知识点1", "知识点2", ...]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
响应:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"items": ["知识点2", "知识点1", ...],
|
|
||||||
"count": 2
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
|
|
||||||
```
|
|
||||||
diet_recommendation_app/
|
|
||||||
├── web_app.py # Flask应用主文件
|
|
||||||
├── start_web.py # 启动脚本
|
|
||||||
├── templates/ # HTML模板
|
|
||||||
│ ├── index.html # 首页
|
|
||||||
│ └── recitation.html # 背诵排序页面
|
|
||||||
├── static/ # 静态资源
|
|
||||||
│ ├── css/
|
|
||||||
│ │ ├── style.css # 通用样式
|
|
||||||
│ │ └── recitation.css # 背诵排序页面样式
|
|
||||||
│ └── js/
|
|
||||||
│ └── recitation.js # 前端交互逻辑
|
|
||||||
└── logs/ # 日志文件
|
|
||||||
└── web_app.log
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 首次运行会自动创建必要的目录(templates、static、logs)
|
|
||||||
2. 建议在本地环境中使用,如需公网访问请配置防火墙和反向代理
|
|
||||||
3. 日志文件保存在 `logs/web_app.log`
|
|
||||||
|
|
||||||
## 故障排除
|
|
||||||
|
|
||||||
### 问题:无法启动应用
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 检查Flask是否已安装:`pip list | grep Flask`
|
|
||||||
- 检查端口5000是否被占用
|
|
||||||
- 查看日志文件 `logs/web_app.log`
|
|
||||||
|
|
||||||
### 问题:无法识别知识点
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 确保输入文本格式正确
|
|
||||||
- 尝试手动整理文本,每行一个知识点
|
|
||||||
- 检查是否包含特殊字符
|
|
||||||
|
|
||||||
### 问题:转盘不显示或旋转异常
|
|
||||||
|
|
||||||
**解决方案**:
|
|
||||||
- 检查浏览器是否支持SVG
|
|
||||||
- 清除浏览器缓存
|
|
||||||
- 使用现代浏览器(Chrome、Firefox、Edge等)
|
|
||||||
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# 界面美化总结
|
|
||||||
|
|
||||||
## 美化成果
|
|
||||||
|
|
||||||
我已经成功为饮食推荐应用进行了全面的界面美化,主要改进包括:
|
|
||||||
|
|
||||||
### 1. 圆角设计系统
|
|
||||||
|
|
||||||
#### 圆角半径配置
|
|
||||||
- **小圆角**: 8px - 用于小按钮和输入框
|
|
||||||
- **中圆角**: 12px - 用于标准组件
|
|
||||||
- **大圆角**: 15px - 用于主要按钮和卡片
|
|
||||||
- **超大圆角**: 20px - 用于页面容器
|
|
||||||
- **极大圆角**: 25px - 用于主框架
|
|
||||||
|
|
||||||
#### 应用范围
|
|
||||||
- ✅ 主容器和页面容器
|
|
||||||
- ✅ 状态栏和导航栏
|
|
||||||
- ✅ 卡片式框架
|
|
||||||
- ✅ 按钮组件
|
|
||||||
- ✅ 输入框组件
|
|
||||||
- ✅ 标签组件
|
|
||||||
|
|
||||||
### 2. 颜色主题系统
|
|
||||||
|
|
||||||
#### 主要颜色
|
|
||||||
- **主色调**: #3498db (蓝色)
|
|
||||||
- **次要色**: #2ecc71 (绿色)
|
|
||||||
- **强调色**: #e74c3c (红色)
|
|
||||||
- **警告色**: #f39c12 (橙色)
|
|
||||||
- **信息色**: #9b59b6 (紫色)
|
|
||||||
|
|
||||||
#### 背景色系
|
|
||||||
- **浅色背景**: #ffffff (白色)
|
|
||||||
- **深色背景**: #2b2b2b (深灰)
|
|
||||||
- **卡片背景**: #f8f9fa (浅灰)
|
|
||||||
- **容器背景**: #f0f0f0 (更浅灰)
|
|
||||||
|
|
||||||
#### 文字颜色
|
|
||||||
- **主要文字**: #2c3e50 (深蓝灰)
|
|
||||||
- **次要文字**: #34495e (中蓝灰)
|
|
||||||
- **辅助文字**: #7f8c8d (浅灰)
|
|
||||||
|
|
||||||
### 3. 组件美化
|
|
||||||
|
|
||||||
#### 按钮美化
|
|
||||||
- **圆角按钮**: 15px圆角半径
|
|
||||||
- **悬停效果**: 颜色渐变
|
|
||||||
- **多色主题**: 支持主色、次色、强调色等
|
|
||||||
- **图标按钮**: 支持emoji图标
|
|
||||||
|
|
||||||
#### 输入框美化
|
|
||||||
- **圆角输入框**: 12px圆角半径
|
|
||||||
- **边框设计**: 1px细边框
|
|
||||||
- **背景色**: 浅色背景
|
|
||||||
- **占位符**: 友好的提示文字
|
|
||||||
|
|
||||||
#### 卡片设计
|
|
||||||
- **卡片式布局**: 20px圆角半径
|
|
||||||
- **阴影效果**: 边框阴影
|
|
||||||
- **内边距**: 统一的内边距设计
|
|
||||||
- **层次感**: 多层级卡片结构
|
|
||||||
|
|
||||||
### 4. 布局优化
|
|
||||||
|
|
||||||
#### 间距系统
|
|
||||||
- **小间距**: 5px
|
|
||||||
- **标准间距**: 10px
|
|
||||||
- **中等间距**: 15px
|
|
||||||
- **大间距**: 20px
|
|
||||||
- **超大间距**: 25px
|
|
||||||
|
|
||||||
#### 字体系统
|
|
||||||
- **标题字体**: Arial 22px 粗体
|
|
||||||
- **副标题**: Arial 18px 粗体
|
|
||||||
- **正文字体**: Arial 14px
|
|
||||||
- **小字体**: Arial 12px
|
|
||||||
- **微小字体**: Arial 10px
|
|
||||||
|
|
||||||
### 5. 页面美化详情
|
|
||||||
|
|
||||||
#### 移动端主界面
|
|
||||||
- **主容器**: 20px圆角,浅色背景
|
|
||||||
- **页面容器**: 25px圆角,白色背景,边框阴影
|
|
||||||
- **状态栏**: 15px圆角,透明背景
|
|
||||||
- **导航栏**: 20px圆角,白色背景,边框设计
|
|
||||||
|
|
||||||
#### 首页美化
|
|
||||||
- **欢迎区域**: 25px圆角,浅色背景
|
|
||||||
- **用户卡片**: 20px圆角,白色背景
|
|
||||||
- **快速操作**: 20px圆角,浅色背景
|
|
||||||
- **按钮设计**: 15px圆角,多色主题
|
|
||||||
|
|
||||||
#### 记录页面美化
|
|
||||||
- **餐次选择**: 15px圆角,白色背景
|
|
||||||
- **食物输入**: 15px圆角,白色背景
|
|
||||||
- **输入框**: 12px圆角,浅色背景
|
|
||||||
- **功能按钮**: 12px圆角,彩色主题
|
|
||||||
|
|
||||||
#### OCR界面美化
|
|
||||||
- **标题**: 18px字体,深色文字
|
|
||||||
- **上传区域**: 实线边框,浅色背景
|
|
||||||
- **控制区域**: 实线边框,浅色背景
|
|
||||||
- **按钮**: 强调色主题
|
|
||||||
|
|
||||||
### 6. 技术实现
|
|
||||||
|
|
||||||
#### 样式配置系统
|
|
||||||
- **StyleConfig类**: 统一管理颜色、圆角、字体、间距
|
|
||||||
- **预设样式**: 预定义的样式组合
|
|
||||||
- **工具函数**: 快速创建美化组件的函数
|
|
||||||
|
|
||||||
#### 组件工厂
|
|
||||||
- **create_rounded_frame()**: 创建圆角框架
|
|
||||||
- **create_accent_button()**: 创建强调色按钮
|
|
||||||
- **create_rounded_entry()**: 创建圆角输入框
|
|
||||||
- **create_card_frame()**: 创建卡片式框架
|
|
||||||
|
|
||||||
#### 主题应用
|
|
||||||
- **apply_rounded_theme()**: 应用圆角主题
|
|
||||||
- **apply_preset_style()**: 应用预设样式
|
|
||||||
- **颜色管理**: 统一的颜色配置系统
|
|
||||||
|
|
||||||
### 7. 用户体验提升
|
|
||||||
|
|
||||||
#### 视觉改进
|
|
||||||
- ✅ 减少方形设计,增加圆角元素
|
|
||||||
- ✅ 统一的颜色主题
|
|
||||||
- ✅ 清晰的层次结构
|
|
||||||
- ✅ 友好的视觉反馈
|
|
||||||
|
|
||||||
#### 交互改进
|
|
||||||
- ✅ 悬停效果
|
|
||||||
- ✅ 按钮状态变化
|
|
||||||
- ✅ 平滑的视觉过渡
|
|
||||||
- ✅ 直观的图标使用
|
|
||||||
|
|
||||||
#### 移动端适配
|
|
||||||
- ✅ 适合手机屏幕的尺寸
|
|
||||||
- ✅ 触摸友好的按钮大小
|
|
||||||
- ✅ 清晰的文字显示
|
|
||||||
- ✅ 合理的间距布局
|
|
||||||
|
|
||||||
### 8. 测试验证
|
|
||||||
|
|
||||||
#### 功能测试
|
|
||||||
- ✅ 样式配置测试通过
|
|
||||||
- ✅ 预设样式测试通过
|
|
||||||
- ✅ 圆角主题测试通过
|
|
||||||
- ✅ 组件创建测试通过
|
|
||||||
|
|
||||||
#### 界面测试
|
|
||||||
- ✅ 移动端界面美化完成
|
|
||||||
- ✅ OCR界面美化完成
|
|
||||||
- ✅ 按钮和输入框美化完成
|
|
||||||
- ✅ 卡片式布局美化完成
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
gui/
|
|
||||||
├── styles.py # 样式配置系统
|
|
||||||
├── mobile_main_window.py # 移动端主界面(已美化)
|
|
||||||
├── ocr_calorie_gui.py # OCR界面(已美化)
|
|
||||||
└── main_window.py # 桌面端界面
|
|
||||||
|
|
||||||
test_ui_beautification.py # 界面美化测试脚本
|
|
||||||
UI_BEAUTIFICATION_SUMMARY.md # 美化总结文档
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用说明
|
|
||||||
|
|
||||||
### 应用美化样式
|
|
||||||
```python
|
|
||||||
from gui.styles import apply_rounded_theme, create_card_frame, create_accent_button
|
|
||||||
|
|
||||||
# 应用圆角主题
|
|
||||||
apply_rounded_theme()
|
|
||||||
|
|
||||||
# 创建美化组件
|
|
||||||
card = create_card_frame(parent)
|
|
||||||
button = create_accent_button(parent, "按钮文字", color_type='primary')
|
|
||||||
```
|
|
||||||
|
|
||||||
### 自定义样式
|
|
||||||
```python
|
|
||||||
from gui.styles import StyleConfig
|
|
||||||
|
|
||||||
# 使用预定义颜色
|
|
||||||
color = StyleConfig.COLORS['primary']
|
|
||||||
|
|
||||||
# 使用预定义圆角
|
|
||||||
radius = StyleConfig.CORNER_RADIUS['large']
|
|
||||||
|
|
||||||
# 使用预定义字体
|
|
||||||
font = StyleConfig.FONTS['title']
|
|
||||||
```
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
|
|
||||||
通过这次界面美化,我们实现了:
|
|
||||||
|
|
||||||
1. **统一的圆角设计系统** - 所有组件都采用圆角设计,减少方形元素
|
|
||||||
2. **完整的颜色主题** - 统一的颜色配置,支持多色主题
|
|
||||||
3. **优化的用户体验** - 更友好的视觉设计和交互体验
|
|
||||||
4. **可维护的代码结构** - 模块化的样式配置系统
|
|
||||||
5. **全面的测试验证** - 确保美化效果正常工作
|
|
||||||
|
|
||||||
界面现在具有现代化的外观,符合移动端应用的设计趋势,为用户提供更好的使用体验。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*美化完成时间: 2024年12月*
|
|
||||||
@@ -105,9 +105,10 @@ class MLConfig:
|
|||||||
class OCRConfig:
|
class OCRConfig:
|
||||||
"""OCR识别配置"""
|
"""OCR识别配置"""
|
||||||
# OCR引擎配置
|
# OCR引擎配置
|
||||||
enable_tesseract: bool = True
|
# 默认只启用轻量级Tesseract,其他引擎需要额外安装深度学习框架
|
||||||
enable_paddleocr: bool = True
|
enable_tesseract: bool = True # 轻量级,推荐使用
|
||||||
enable_easyocr: bool = True
|
enable_paddleocr: bool = False # 可选,需要PaddlePaddle,占用内存较大
|
||||||
|
enable_easyocr: bool = False # 可选,需要PyTorch,占用内存很大(1-2GB)
|
||||||
|
|
||||||
# 识别参数
|
# 识别参数
|
||||||
min_confidence: float = 0.6
|
min_confidence: float = 0.6
|
||||||
|
|||||||
286
core/base.py
286
core/base.py
@@ -135,6 +135,50 @@ class DataManager:
|
|||||||
)
|
)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
# 餐食记录表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS meal_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT,
|
||||||
|
date TEXT,
|
||||||
|
meal_type TEXT,
|
||||||
|
foods TEXT,
|
||||||
|
quantities TEXT,
|
||||||
|
calories REAL,
|
||||||
|
satisfaction_score INTEGER,
|
||||||
|
food_items TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 反馈记录表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS feedback_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT,
|
||||||
|
date TEXT,
|
||||||
|
recommended_foods TEXT,
|
||||||
|
user_choice TEXT,
|
||||||
|
feedback_type TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# 问卷记录表
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS questionnaire_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT,
|
||||||
|
questionnaire_type TEXT,
|
||||||
|
answers TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (user_id),
|
||||||
|
UNIQUE(user_id, questionnaire_type)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
# 分析结果表
|
# 分析结果表
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS analysis_results (
|
CREATE TABLE IF NOT EXISTS analysis_results (
|
||||||
@@ -163,15 +207,24 @@ class DataManager:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def save_user_data(self, user_data: UserData) -> bool:
|
def save_user_data(self, user_data: UserData) -> bool:
|
||||||
"""保存用户数据"""
|
"""保存用户数据(只保存profile,其他数据用独立表)"""
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 只保存profile和preferences到users表
|
||||||
|
user_basic_data = {
|
||||||
|
'user_id': user_data.user_id,
|
||||||
|
'profile': user_data.profile,
|
||||||
|
'preferences': user_data.preferences,
|
||||||
|
'created_at': user_data.created_at,
|
||||||
|
'updated_at': user_data.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
INSERT OR REPLACE INTO users (user_id, data, updated_at)
|
INSERT OR REPLACE INTO users (user_id, data, updated_at)
|
||||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||||
''', (user_data.user_id, json.dumps(user_data.__dict__)))
|
''', (user_data.user_id, json.dumps(user_basic_data)))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -195,66 +248,95 @@ class DataManager:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 解析用户基本信息
|
# 解析用户基本信息
|
||||||
data_dict = json.loads(result[0])
|
try:
|
||||||
|
data_dict = json.loads(result[0])
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"解析用户数据JSON失败: user_id={user_id}, error={e}")
|
||||||
|
conn.close()
|
||||||
|
return None
|
||||||
|
|
||||||
# 获取餐食记录
|
# 获取餐食记录(如果表存在)
|
||||||
cursor.execute('''
|
|
||||||
SELECT date, meal_type, foods, quantities, calories, satisfaction_score, food_items
|
|
||||||
FROM meal_records
|
|
||||||
WHERE user_id = ?
|
|
||||||
ORDER BY date DESC
|
|
||||||
''', (user_id,))
|
|
||||||
|
|
||||||
meal_rows = cursor.fetchall()
|
|
||||||
meals = []
|
meals = []
|
||||||
for row in meal_rows:
|
try:
|
||||||
meal = {
|
cursor.execute('''
|
||||||
'date': row[0],
|
SELECT date, meal_type, foods, quantities, calories, satisfaction_score, food_items
|
||||||
'meal_type': row[1],
|
FROM meal_records
|
||||||
'foods': json.loads(row[2]) if row[2] else [],
|
WHERE user_id = ?
|
||||||
'quantities': json.loads(row[3]) if row[3] else [],
|
ORDER BY date DESC
|
||||||
'calories': row[4],
|
''', (user_id,))
|
||||||
'satisfaction_score': row[5],
|
|
||||||
'food_items': json.loads(row[6]) if row[6] else []
|
|
||||||
}
|
|
||||||
meals.append(meal)
|
|
||||||
|
|
||||||
# 获取反馈记录
|
meal_rows = cursor.fetchall()
|
||||||
cursor.execute('''
|
for row in meal_rows:
|
||||||
SELECT date, recommended_foods, user_choice, feedback_type
|
try:
|
||||||
FROM feedback_records
|
meal = {
|
||||||
WHERE user_id = ?
|
'date': row[0],
|
||||||
ORDER BY date DESC
|
'meal_type': row[1],
|
||||||
''', (user_id,))
|
'foods': json.loads(row[2]) if row[2] else [],
|
||||||
|
'quantities': json.loads(row[3]) if row[3] else [],
|
||||||
|
'calories': row[4],
|
||||||
|
'satisfaction_score': row[5],
|
||||||
|
'food_items': json.loads(row[6]) if row[6] else []
|
||||||
|
}
|
||||||
|
meals.append(meal)
|
||||||
|
except (json.JSONDecodeError, IndexError) as e:
|
||||||
|
logger.warning(f"解析餐食记录失败: {e}")
|
||||||
|
continue
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
# 表不存在,跳过
|
||||||
|
logger.warning(f"meal_records表不存在,跳过: {e}")
|
||||||
|
|
||||||
feedback_rows = cursor.fetchall()
|
# 获取反馈记录(如果表存在)
|
||||||
feedback = []
|
feedback = []
|
||||||
for row in feedback_rows:
|
try:
|
||||||
fb = {
|
cursor.execute('''
|
||||||
'date': row[0],
|
SELECT date, recommended_foods, user_choice, feedback_type
|
||||||
'recommended_foods': json.loads(row[1]) if row[1] else [],
|
FROM feedback_records
|
||||||
'user_choice': row[2],
|
WHERE user_id = ?
|
||||||
'feedback_type': row[3]
|
ORDER BY date DESC
|
||||||
}
|
''', (user_id,))
|
||||||
feedback.append(fb)
|
|
||||||
|
|
||||||
# 获取问卷数据
|
feedback_rows = cursor.fetchall()
|
||||||
cursor.execute('''
|
for row in feedback_rows:
|
||||||
SELECT questionnaire_type, answers
|
try:
|
||||||
FROM questionnaire_records
|
fb = {
|
||||||
WHERE user_id = ?
|
'date': row[0],
|
||||||
''', (user_id,))
|
'recommended_foods': json.loads(row[1]) if row[1] else [],
|
||||||
|
'user_choice': row[2],
|
||||||
|
'feedback_type': row[3]
|
||||||
|
}
|
||||||
|
feedback.append(fb)
|
||||||
|
except (json.JSONDecodeError, IndexError) as e:
|
||||||
|
logger.warning(f"解析反馈记录失败: {e}")
|
||||||
|
continue
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
# 表不存在,跳过
|
||||||
|
logger.warning(f"feedback_records表不存在,跳过: {e}")
|
||||||
|
|
||||||
questionnaire_rows = cursor.fetchall()
|
# 获取问卷数据(如果表存在)
|
||||||
preferences = {}
|
preferences = {}
|
||||||
for row in questionnaire_rows:
|
try:
|
||||||
preferences[row[0]] = json.loads(row[1]) if row[1] else {}
|
cursor.execute('''
|
||||||
|
SELECT questionnaire_type, answers
|
||||||
|
FROM questionnaire_records
|
||||||
|
WHERE user_id = ?
|
||||||
|
''', (user_id,))
|
||||||
|
|
||||||
|
questionnaire_rows = cursor.fetchall()
|
||||||
|
for row in questionnaire_rows:
|
||||||
|
try:
|
||||||
|
preferences[row[0]] = json.loads(row[1]) if row[1] else {}
|
||||||
|
except (json.JSONDecodeError, IndexError) as e:
|
||||||
|
logger.warning(f"解析问卷记录失败: {e}")
|
||||||
|
continue
|
||||||
|
except sqlite3.OperationalError as e:
|
||||||
|
# 表不存在,跳过
|
||||||
|
logger.warning(f"questionnaire_records表不存在,跳过: {e}")
|
||||||
|
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
# 构建完整的用户数据
|
# 构建完整的用户数据
|
||||||
user_data = UserData(
|
user_data = UserData(
|
||||||
user_id=data_dict['user_id'],
|
user_id=data_dict.get('user_id', user_id),
|
||||||
profile=data_dict.get('profile', {}),
|
profile=data_dict.get('profile', {}),
|
||||||
meals=meals,
|
meals=meals,
|
||||||
feedback=feedback,
|
feedback=feedback,
|
||||||
@@ -266,7 +348,7 @@ class DataManager:
|
|||||||
return user_data
|
return user_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取用户数据失败: {e}")
|
logger.error(f"获取用户数据失败: user_id={user_id}, error={e}", exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save_analysis_result(self, result: AnalysisResult) -> bool:
|
def save_analysis_result(self, result: AnalysisResult) -> bool:
|
||||||
@@ -340,6 +422,110 @@ class DataManager:
|
|||||||
logger.error(f"获取分析历史失败: {e}")
|
logger.error(f"获取分析历史失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def save_meal_record(self, user_id: str, meal_data: Dict[str, Any]) -> bool:
|
||||||
|
"""保存餐食记录(包括非推荐食物,用于学习)"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 提取food_items,如果没有则从foods生成
|
||||||
|
food_items = meal_data.get('food_items', [])
|
||||||
|
if not food_items and meal_data.get('foods'):
|
||||||
|
food_items = [{'name': food, 'quantity': qty}
|
||||||
|
for food, qty in zip(meal_data.get('foods', []),
|
||||||
|
meal_data.get('quantities', []))]
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO meal_records
|
||||||
|
(user_id, date, meal_type, foods, quantities, calories, satisfaction_score, food_items)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
user_id,
|
||||||
|
meal_data.get('date'),
|
||||||
|
meal_data.get('meal_type'),
|
||||||
|
json.dumps(meal_data.get('foods', [])),
|
||||||
|
json.dumps(meal_data.get('quantities', [])),
|
||||||
|
meal_data.get('calories'),
|
||||||
|
meal_data.get('satisfaction_score'),
|
||||||
|
json.dumps(food_items)
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存餐食记录失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def save_recommendation_result(self, user_id: str, recommendation_data: Dict[str, Any]) -> int:
|
||||||
|
"""保存推荐结果到recommendations表,返回recommendation_id"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# 添加recommendations表(如果不存在)
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS recommendations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id TEXT,
|
||||||
|
date TEXT,
|
||||||
|
meal_type TEXT,
|
||||||
|
recommended_foods TEXT,
|
||||||
|
reasoning TEXT,
|
||||||
|
confidence_score REAL,
|
||||||
|
special_considerations TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (user_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO recommendations
|
||||||
|
(user_id, date, meal_type, recommended_foods, reasoning, confidence_score, special_considerations)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
user_id,
|
||||||
|
recommendation_data.get('date', datetime.now().strftime('%Y-%m-%d')),
|
||||||
|
recommendation_data.get('meal_type'),
|
||||||
|
json.dumps(recommendation_data.get('recommended_foods', [])),
|
||||||
|
recommendation_data.get('reasoning', ''),
|
||||||
|
recommendation_data.get('confidence', 0.5),
|
||||||
|
json.dumps(recommendation_data.get('special_considerations', []))
|
||||||
|
))
|
||||||
|
|
||||||
|
recommendation_id = cursor.lastrowid
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return recommendation_id
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存推荐结果失败: {e}")
|
||||||
|
return -1
|
||||||
|
|
||||||
|
def save_feedback_record(self, user_id: str, feedback_data: Dict[str, Any]) -> bool:
|
||||||
|
"""保存用户反馈(用户实际吃的食物,可能不是推荐的)"""
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO feedback_records
|
||||||
|
(user_id, date, recommended_foods, user_choice, feedback_type)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
''', (
|
||||||
|
user_id,
|
||||||
|
feedback_data.get('date', datetime.now().strftime('%Y-%m-%d')),
|
||||||
|
json.dumps(feedback_data.get('recommended_foods', [])),
|
||||||
|
feedback_data.get('user_choice', ''),
|
||||||
|
feedback_data.get('feedback_type', 'custom') # custom表示用户自定义选择
|
||||||
|
))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"保存反馈记录失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class EventBus:
|
class EventBus:
|
||||||
"""事件总线基座"""
|
"""事件总线基座"""
|
||||||
|
|||||||
29
docker-compose.dev.yml
Normal file
29
docker-compose.dev.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
container_name: diet_recommendation_app_dev
|
||||||
|
ports:
|
||||||
|
- "7400:7400"
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=development
|
||||||
|
- FLASK_DEBUG=1
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
volumes:
|
||||||
|
# 代码热重载
|
||||||
|
- .:/app
|
||||||
|
# 持久化数据
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./models:/app/models
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
41
docker-compose.low-mem.yml
Normal file
41
docker-compose.low-mem.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.low-mem
|
||||||
|
container_name: diet_recommendation_app_low_mem
|
||||||
|
ports:
|
||||||
|
- "7400:7400"
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- PYTHONHASHSEED=0
|
||||||
|
- MALLOC_ARENA_MAX=2
|
||||||
|
volumes:
|
||||||
|
# 持久化数据目录
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./models:/app/models
|
||||||
|
restart: unless-stopped
|
||||||
|
# 内存限制(小内存机器配置)
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:7400/health').read()"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: diet_recommendation_app
|
||||||
|
ports:
|
||||||
|
- "7400:7400"
|
||||||
|
environment:
|
||||||
|
- FLASK_ENV=production
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
volumes:
|
||||||
|
# 持久化数据目录
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./models:/app/models
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:7400/health')"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
app-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
86
git_push.bat
Normal file
86
git_push.bat
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
@echo off
|
||||||
|
REM -*- coding: utf-8 -*-
|
||||||
|
REM Git 自动上传脚本(Windows)
|
||||||
|
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
echo === Git 自动上传脚本 ===
|
||||||
|
|
||||||
|
REM 检查是否在 git 仓库中
|
||||||
|
if not exist .git (
|
||||||
|
echo 错误: 当前目录不是 git 仓库
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 获取当前分支
|
||||||
|
for /f "tokens=2" %%i in ('git branch --show-current 2^>nul') do set CURRENT_BRANCH=%%i
|
||||||
|
if "!CURRENT_BRANCH!"=="" (
|
||||||
|
for /f "delims=" %%i in ('git rev-parse --abbrev-ref HEAD 2^>nul') do set CURRENT_BRANCH=%%i
|
||||||
|
)
|
||||||
|
echo 当前分支: !CURRENT_BRANCH!
|
||||||
|
|
||||||
|
REM 检查是否有未提交的更改
|
||||||
|
git status --porcelain >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo 没有需要提交的更改
|
||||||
|
set /p CONTINUE="是否继续检查远程更新? (y/n) "
|
||||||
|
if /i "!CONTINUE!"=="y" (
|
||||||
|
echo 拉取远程更新...
|
||||||
|
git pull origin !CURRENT_BRANCH! 2>nul || echo 拉取失败,可能没有远程分支
|
||||||
|
)
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 显示当前状态
|
||||||
|
echo 当前更改状态:
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
REM 询问是否继续
|
||||||
|
set /p CONTINUE="是否继续提交并推送? (y/n) "
|
||||||
|
if /i not "!CONTINUE!"=="y" (
|
||||||
|
echo 操作已取消
|
||||||
|
exit /b 0
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 获取提交信息
|
||||||
|
if "%1"=="" (
|
||||||
|
set /p COMMIT_MSG="请输入提交信息(或直接按回车使用默认信息): "
|
||||||
|
if "!COMMIT_MSG!"=="" (
|
||||||
|
for /f "tokens=1-3 delims=/- " %%a in ('date /t') do set DATE_STR=%%a-%%b-%%c
|
||||||
|
for /f "tokens=1-2 delims=: " %%a in ('time /t') do set TIME_STR=%%a:%%b
|
||||||
|
set COMMIT_MSG=chore: update code !DATE_STR! !TIME_STR!
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
set COMMIT_MSG=%1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 添加所有更改
|
||||||
|
echo 添加文件到暂存区...
|
||||||
|
git add -A
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo 添加文件失败
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 提交更改
|
||||||
|
echo 提交更改...
|
||||||
|
git commit -m "!COMMIT_MSG!"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo 提交失败
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo 提交成功!
|
||||||
|
|
||||||
|
REM 推送到远程
|
||||||
|
echo 推送到远程仓库...
|
||||||
|
git push origin !CURRENT_BRANCH!
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo 推送失败,可能需要设置远程仓库或认证
|
||||||
|
echo 提示: 可以手动执行 'git push origin !CURRENT_BRANCH!'
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo ✓ 推送成功!
|
||||||
|
echo === 完成 ===
|
||||||
|
|
||||||
138
git_push.py
Normal file
138
git_push.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Git 自动上传脚本(跨平台)
|
||||||
|
支持 Windows、Linux、Mac
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import platform
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 颜色输出(Windows 10+ 支持 ANSI)
|
||||||
|
def print_color(text, color="white"):
|
||||||
|
colors = {
|
||||||
|
"red": "\033[0;31m",
|
||||||
|
"green": "\033[0;32m",
|
||||||
|
"yellow": "\033[1;33m",
|
||||||
|
"blue": "\033[0;34m",
|
||||||
|
"reset": "\033[0m"
|
||||||
|
}
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
# Windows 10+ 支持 ANSI,需要启用
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
color_code = colors.get(color.lower(), colors["reset"])
|
||||||
|
reset = colors["reset"]
|
||||||
|
print(f"{color_code}{text}{reset}")
|
||||||
|
|
||||||
|
def run_command(cmd, check=True):
|
||||||
|
"""执行命令并返回结果"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
shell=True,
|
||||||
|
check=check,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
return False, e.stdout.strip() if hasattr(e, 'stdout') else "", e.stderr.strip() if hasattr(e, 'stderr') else ""
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print_color("=== Git 自动上传脚本 ===", "green")
|
||||||
|
|
||||||
|
# 检查是否在 git 仓库中
|
||||||
|
if not os.path.exists(".git"):
|
||||||
|
print_color("错误: 当前目录不是 git 仓库", "red")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 获取当前分支
|
||||||
|
success, branch, _ = run_command("git branch --show-current", check=False)
|
||||||
|
if not success:
|
||||||
|
success, branch, _ = run_command("git rev-parse --abbrev-ref HEAD", check=False)
|
||||||
|
|
||||||
|
if not branch:
|
||||||
|
print_color("错误: 无法获取当前分支", "red")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print_color(f"当前分支: {branch}", "yellow")
|
||||||
|
|
||||||
|
# 检查是否有未提交的更改
|
||||||
|
success, status, _ = run_command("git status --porcelain", check=False)
|
||||||
|
if not status:
|
||||||
|
print_color("没有需要提交的更改", "yellow")
|
||||||
|
response = input("是否继续检查远程更新? (y/n) ").strip().lower()
|
||||||
|
if response == 'y':
|
||||||
|
print_color("拉取远程更新...", "green")
|
||||||
|
run_command(f"git pull origin {branch}", check=False)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 显示当前状态
|
||||||
|
print_color("当前更改状态:", "yellow")
|
||||||
|
success, status_output, _ = run_command("git status --short", check=False)
|
||||||
|
print(status_output)
|
||||||
|
|
||||||
|
# 询问是否继续
|
||||||
|
response = input("是否继续提交并推送? (y/n) ").strip().lower()
|
||||||
|
if response != 'y':
|
||||||
|
print_color("操作已取消", "yellow")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 获取提交信息
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
commit_msg = " ".join(sys.argv[1:])
|
||||||
|
else:
|
||||||
|
default_msg = f"chore: update code {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||||
|
user_msg = input(f"请输入提交信息(或直接按回车使用默认信息): ").strip()
|
||||||
|
commit_msg = user_msg if user_msg else default_msg
|
||||||
|
|
||||||
|
# 添加所有更改
|
||||||
|
print_color("添加文件到暂存区...", "green")
|
||||||
|
success, _, error = run_command("git add -A")
|
||||||
|
if not success:
|
||||||
|
print_color(f"添加文件失败: {error}", "red")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# 提交更改
|
||||||
|
print_color("提交更改...", "green")
|
||||||
|
success, _, error = run_command(f'git commit -m "{commit_msg}"')
|
||||||
|
if not success:
|
||||||
|
if "nothing to commit" in error.lower():
|
||||||
|
print_color("没有需要提交的更改", "yellow")
|
||||||
|
sys.exit(0)
|
||||||
|
print_color(f"提交失败: {error}", "red")
|
||||||
|
sys.exit(1)
|
||||||
|
print_color("提交成功!", "green")
|
||||||
|
|
||||||
|
# 推送到远程
|
||||||
|
print_color("推送到远程仓库...", "green")
|
||||||
|
success, _, error = run_command(f"git push origin {branch}")
|
||||||
|
if not success:
|
||||||
|
print_color(f"推送失败: {error}", "red")
|
||||||
|
print_color(f"提示: 可以手动执行 'git push origin {branch}'", "yellow")
|
||||||
|
print_color("可能需要设置远程仓库地址或进行身份验证", "yellow")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print_color("✓ 推送成功!", "green")
|
||||||
|
print_color("=== 完成 ===", "green")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print_color("\n操作已取消", "yellow")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print_color(f"发生错误: {e}", "red")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
84
git_push.sh
Normal file
84
git_push.sh
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Git 自动上传脚本(Linux/Mac)
|
||||||
|
|
||||||
|
set -e # 遇到错误立即退出
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== Git 自动上传脚本 ===${NC}"
|
||||||
|
|
||||||
|
# 检查是否在 git 仓库中
|
||||||
|
if [ ! -d .git ]; then
|
||||||
|
echo -e "${RED}错误: 当前目录不是 git 仓库${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取当前分支
|
||||||
|
CURRENT_BRANCH=$(git branch --show-current)
|
||||||
|
echo -e "${YELLOW}当前分支: ${CURRENT_BRANCH}${NC}"
|
||||||
|
|
||||||
|
# 检查是否有未提交的更改
|
||||||
|
if [ -z "$(git status --porcelain)" ]; then
|
||||||
|
echo -e "${YELLOW}没有需要提交的更改${NC}"
|
||||||
|
read -p "是否继续检查远程更新? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${GREEN}拉取远程更新...${NC}"
|
||||||
|
git pull origin "$CURRENT_BRANCH" || echo -e "${YELLOW}拉取失败,可能没有远程分支${NC}"
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 显示当前状态
|
||||||
|
echo -e "${YELLOW}当前更改状态:${NC}"
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
# 询问是否继续
|
||||||
|
read -p "是否继续提交并推送? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo -e "${YELLOW}操作已取消${NC}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取提交信息
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo -e "${YELLOW}请输入提交信息(或直接按回车使用默认信息):${NC}"
|
||||||
|
read -r COMMIT_MSG
|
||||||
|
if [ -z "$COMMIT_MSG" ]; then
|
||||||
|
COMMIT_MSG="chore: update code $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
COMMIT_MSG="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 添加所有更改
|
||||||
|
echo -e "${GREEN}添加文件到暂存区...${NC}"
|
||||||
|
git add -A
|
||||||
|
|
||||||
|
# 提交更改
|
||||||
|
echo -e "${GREEN}提交更改...${NC}"
|
||||||
|
if git commit -m "$COMMIT_MSG"; then
|
||||||
|
echo -e "${GREEN}提交成功!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}提交失败${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 推送到远程
|
||||||
|
echo -e "${GREEN}推送到远程仓库...${NC}"
|
||||||
|
if git push origin "$CURRENT_BRANCH"; then
|
||||||
|
echo -e "${GREEN}✓ 推送成功!${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}推送失败,可能需要设置远程仓库或认证${NC}"
|
||||||
|
echo -e "${YELLOW}提示: 可以手动执行 'git push origin $CURRENT_BRANCH'${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}=== 完成 ===${NC}"
|
||||||
|
|
||||||
@@ -60,8 +60,10 @@ class OCRCalorieRecognitionModule(BaseModule):
|
|||||||
def __init__(self, config: BaseConfig):
|
def __init__(self, config: BaseConfig):
|
||||||
super().__init__(config, ModuleType.DATA_COLLECTION)
|
super().__init__(config, ModuleType.DATA_COLLECTION)
|
||||||
|
|
||||||
# OCR配置
|
# OCR配置 - 优先使用轻量级OCR引擎,避免内存占用过大
|
||||||
self.ocr_methods = ['tesseract', 'paddleocr', 'easyocr']
|
# 默认只使用tesseract(轻量级),其他OCR引擎需要手动安装且作为可选依赖
|
||||||
|
self.ocr_methods = ['tesseract'] # 轻量级默认配置
|
||||||
|
# 可选添加其他OCR引擎:'paddleocr', 'easyocr' (需要安装对应依赖)
|
||||||
self.min_confidence = 0.6
|
self.min_confidence = 0.6
|
||||||
self.max_processing_time = 30.0
|
self.max_processing_time = 30.0
|
||||||
|
|
||||||
@@ -278,9 +280,11 @@ class OCRCalorieRecognitionModule(BaseModule):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _paddleocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
|
def _paddleocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
|
||||||
"""使用PaddleOCR进行识别"""
|
"""使用PaddleOCR进行识别(可选依赖,需要PaddlePaddle,占用内存较大)"""
|
||||||
try:
|
try:
|
||||||
# 这里需要安装paddleocr: pip install paddleocr
|
# 注意:PaddleOCR需要安装paddleocr和PaddlePaddle,占用内存较大
|
||||||
|
# 如需使用,请手动安装: pip install paddleocr
|
||||||
|
# 然后需要在OCR方法列表中添加'paddleocr'
|
||||||
from paddleocr import PaddleOCR
|
from paddleocr import PaddleOCR
|
||||||
|
|
||||||
if 'paddleocr' not in self.ocr_engines:
|
if 'paddleocr' not in self.ocr_engines:
|
||||||
@@ -327,9 +331,11 @@ class OCRCalorieRecognitionModule(BaseModule):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def _easyocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
|
def _easyocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
|
||||||
"""使用EasyOCR进行识别"""
|
"""使用EasyOCR进行识别(可选依赖,需要PyTorch,占用内存很大)"""
|
||||||
try:
|
try:
|
||||||
# 这里需要安装easyocr: pip install easyocr
|
# 注意:EasyOCR需要安装easyocr和PyTorch,占用内存很大(通常需要1-2GB)
|
||||||
|
# 如需使用,请手动安装: pip install easyocr
|
||||||
|
# 然后需要在OCR方法列表中添加'easyocr'
|
||||||
import easyocr
|
import easyocr
|
||||||
|
|
||||||
if 'easyocr' not in self.ocr_engines:
|
if 'easyocr' not in self.ocr_engines:
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ class RecommendationEngine(BaseModule):
|
|||||||
context = input_data.get('context', {})
|
context = input_data.get('context', {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 检查数据充足性
|
||||||
|
data_check = self._check_data_sufficiency(user_data)
|
||||||
|
|
||||||
# 生成完整餐食搭配推荐
|
# 生成完整餐食搭配推荐
|
||||||
meal_combinations = self._generate_meal_combinations(user_data, meal_type, preferences, context)
|
meal_combinations = self._generate_meal_combinations(user_data, meal_type, preferences, context)
|
||||||
|
|
||||||
@@ -101,6 +104,7 @@ class RecommendationEngine(BaseModule):
|
|||||||
'recommendations': meal_combinations,
|
'recommendations': meal_combinations,
|
||||||
'reasoning': self._generate_meal_reasoning(meal_combinations, user_data, meal_type),
|
'reasoning': self._generate_meal_reasoning(meal_combinations, user_data, meal_type),
|
||||||
'confidence': self._calculate_recommendation_confidence(user_data),
|
'confidence': self._calculate_recommendation_confidence(user_data),
|
||||||
|
'data_sufficiency': data_check,
|
||||||
'metadata': {
|
'metadata': {
|
||||||
'meal_type': meal_type,
|
'meal_type': meal_type,
|
||||||
'combination_count': len(meal_combinations)
|
'combination_count': len(meal_combinations)
|
||||||
@@ -117,27 +121,34 @@ class RecommendationEngine(BaseModule):
|
|||||||
combinations = []
|
combinations = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 基于用户历史数据生成搭配
|
# 检查数据充足性
|
||||||
historical_combinations = self._generate_historical_combinations(user_data, meal_type)
|
data_check = self._check_data_sufficiency(user_data)
|
||||||
combinations.extend(historical_combinations)
|
|
||||||
|
|
||||||
# 2. 基于用户偏好生成个性化搭配
|
# 数据充足:使用个性化推荐
|
||||||
personalized_combinations = self._generate_personalized_combinations(user_data, meal_type, preferences)
|
if data_check['sufficient']:
|
||||||
combinations.extend(personalized_combinations)
|
# 1. 基于用户历史数据生成搭配
|
||||||
|
historical_combinations = self._generate_historical_combinations(user_data, meal_type)
|
||||||
|
combinations.extend(historical_combinations)
|
||||||
|
|
||||||
# 3. 基于相似用户生成搭配
|
# 2. 基于用户偏好生成个性化搭配
|
||||||
similar_user_combinations = self._generate_similar_user_combinations(user_data, meal_type)
|
personalized_combinations = self._generate_personalized_combinations(user_data, meal_type, preferences)
|
||||||
combinations.extend(similar_user_combinations)
|
combinations.extend(personalized_combinations)
|
||||||
|
|
||||||
# 4. 如果没有足够数据,使用模板生成
|
# 3. 如果个性化推荐不足,补充模板
|
||||||
if len(combinations) < 3:
|
if len(combinations) < 3:
|
||||||
|
template_combinations = self._generate_template_combinations(user_data, meal_type)
|
||||||
|
combinations.extend(template_combinations)
|
||||||
|
|
||||||
|
# 数据不足:使用通用模板推荐
|
||||||
|
else:
|
||||||
|
self.logger.info(f"用户数据不足({data_check['days']}天),使用通用推荐")
|
||||||
template_combinations = self._generate_template_combinations(user_data, meal_type)
|
template_combinations = self._generate_template_combinations(user_data, meal_type)
|
||||||
combinations.extend(template_combinations)
|
combinations.extend(template_combinations)
|
||||||
|
|
||||||
# 5. 去重和排序
|
# 去重和排序
|
||||||
combinations = self._deduplicate_and_rank_combinations(combinations, user_data)
|
combinations = self._deduplicate_and_rank_combinations(combinations, user_data)
|
||||||
|
|
||||||
# 6. 确保至少有一些推荐
|
# 确保至少有推荐
|
||||||
if not combinations:
|
if not combinations:
|
||||||
combinations = self._generate_fallback_combinations(meal_type)
|
combinations = self._generate_fallback_combinations(meal_type)
|
||||||
|
|
||||||
@@ -145,7 +156,6 @@ class RecommendationEngine(BaseModule):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"生成餐食搭配失败: {e}")
|
self.logger.error(f"生成餐食搭配失败: {e}")
|
||||||
# 返回基础推荐
|
|
||||||
return self._generate_fallback_combinations(meal_type)
|
return self._generate_fallback_combinations(meal_type)
|
||||||
|
|
||||||
def _generate_historical_combinations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]:
|
def _generate_historical_combinations(self, user_data: UserData, meal_type: str) -> List[Dict[str, Any]]:
|
||||||
@@ -666,18 +676,44 @@ class RecommendationEngine(BaseModule):
|
|||||||
else:
|
else:
|
||||||
return f"推荐{top_rec['food']},基于您的个人偏好"
|
return f"推荐{top_rec['food']},基于您的个人偏好"
|
||||||
|
|
||||||
|
def _check_data_sufficiency(self, user_data: UserData) -> Dict[str, Any]:
|
||||||
|
"""检查数据充足性(按日期去重计算天数)"""
|
||||||
|
if not user_data.meals:
|
||||||
|
return {
|
||||||
|
'sufficient': False,
|
||||||
|
'days': 0,
|
||||||
|
'meals': 0,
|
||||||
|
'message': '暂无餐食记录,将提供通用推荐'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 按日期去重计算天数
|
||||||
|
unique_dates = set(meal.get('date') for meal in user_data.meals if meal.get('date'))
|
||||||
|
meal_count = len(user_data.meals)
|
||||||
|
day_count = len(unique_dates)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sufficient': day_count >= 3,
|
||||||
|
'days': day_count,
|
||||||
|
'meals': meal_count,
|
||||||
|
'message': f'已收集{day_count}天{meal_count}餐数据' + (',数据充足' if day_count >= 3 else ',建议继续记录')
|
||||||
|
}
|
||||||
|
|
||||||
def _calculate_recommendation_confidence(self, user_data: UserData) -> float:
|
def _calculate_recommendation_confidence(self, user_data: UserData) -> float:
|
||||||
"""计算推荐置信度"""
|
"""计算推荐置信度"""
|
||||||
meal_count = len(user_data.meals)
|
data_check = self._check_data_sufficiency(user_data)
|
||||||
|
meal_count = data_check['meals']
|
||||||
|
day_count = data_check['days']
|
||||||
feedback_count = len(user_data.feedback)
|
feedback_count = len(user_data.feedback)
|
||||||
|
|
||||||
# 基于数据量计算置信度
|
# 基于数据量计算置信度
|
||||||
if meal_count >= 15 and feedback_count >= 5:
|
if day_count >= 5 and feedback_count >= 5:
|
||||||
return 0.9
|
return 0.9
|
||||||
elif meal_count >= 10 and feedback_count >= 3:
|
elif day_count >= 3 and meal_count >= 7:
|
||||||
return 0.7
|
return 0.7
|
||||||
elif meal_count >= 5:
|
elif day_count >= 3:
|
||||||
return 0.5
|
return 0.6
|
||||||
|
elif meal_count >= 3:
|
||||||
|
return 0.4
|
||||||
else:
|
else:
|
||||||
return 0.3
|
return 0.3
|
||||||
|
|
||||||
@@ -991,7 +1027,7 @@ if __name__ == "__main__":
|
|||||||
# 初始化应用
|
# 初始化应用
|
||||||
config = BaseConfig()
|
config = BaseConfig()
|
||||||
if initialize_app(config):
|
if initialize_app(config):
|
||||||
print("✅ 应用初始化成功")
|
print(" 应用初始化成功")
|
||||||
|
|
||||||
# 测试餐食推荐
|
# 测试餐食推荐
|
||||||
test_user_id = "test_user_001"
|
test_user_id = "test_user_001"
|
||||||
@@ -999,7 +1035,7 @@ if __name__ == "__main__":
|
|||||||
result = generate_meal_recommendations(test_user_id, "lunch", {"taste": "sweet"})
|
result = generate_meal_recommendations(test_user_id, "lunch", {"taste": "sweet"})
|
||||||
if result and result.get('success'):
|
if result and result.get('success'):
|
||||||
recommendations = result.get('recommendations', [])
|
recommendations = result.get('recommendations', [])
|
||||||
print(f"✅ 餐食推荐成功,推荐了{len(recommendations)}种食物")
|
print(f" 餐食推荐成功,推荐了{len(recommendations)}种食物")
|
||||||
for rec in recommendations[:3]:
|
for rec in recommendations[:3]:
|
||||||
print(f" - {rec['food']}: {rec.get('score', 0):.2f}")
|
print(f" - {rec['food']}: {rec.get('score', 0):.2f}")
|
||||||
|
|
||||||
@@ -1007,10 +1043,10 @@ if __name__ == "__main__":
|
|||||||
result = find_similar_foods(test_user_id, "米饭")
|
result = find_similar_foods(test_user_id, "米饭")
|
||||||
if result and result.get('success'):
|
if result and result.get('success'):
|
||||||
similar_foods = result.get('similar_foods', [])
|
similar_foods = result.get('similar_foods', [])
|
||||||
print(f"✅ 相似食物查找成功,找到{len(similar_foods)}种相似食物")
|
print(f" 相似食物查找成功,找到{len(similar_foods)}种相似食物")
|
||||||
|
|
||||||
# 清理应用
|
# 清理应用
|
||||||
cleanup_app()
|
cleanup_app()
|
||||||
print("✅ 应用清理完成")
|
print(" 应用清理完成")
|
||||||
else:
|
else:
|
||||||
print("❌ 应用初始化失败")
|
print(" 应用初始化失败")
|
||||||
|
|||||||
@@ -19,14 +19,19 @@ python-dateutil>=2.8.0
|
|||||||
# 图像处理 (GUI需要)
|
# 图像处理 (GUI需要)
|
||||||
Pillow>=10.0.0
|
Pillow>=10.0.0
|
||||||
|
|
||||||
# OCR识别依赖
|
# OCR识别依赖(必需,轻量级)
|
||||||
pytesseract>=0.3.10
|
pytesseract>=0.3.10
|
||||||
opencv-python>=4.8.0
|
opencv-python>=4.8.0
|
||||||
paddleocr>=2.7.0
|
|
||||||
easyocr>=1.7.0
|
# OCR识别依赖(可选,需要额外安装深度学习框架,占用内存较大)
|
||||||
|
# paddleocr>=2.7.0 # 可选,需要PaddlePaddle,占用内存较大
|
||||||
|
# easyocr>=1.7.0 # 可选,需要PyTorch,占用内存很大(1-2GB)
|
||||||
|
|
||||||
# 移动端支持 (可选)
|
# 移动端支持 (可选)
|
||||||
kivy>=2.1.0
|
kivy>=2.1.0
|
||||||
kivymd>=1.1.1
|
kivymd>=1.1.1
|
||||||
# 缃戦〉绔敮鎸乣nFlask>=3.0.0
|
|
||||||
|
# Web端支持 (必需)
|
||||||
|
Flask>=3.0.0
|
||||||
Werkzeug>=3.0.0
|
Werkzeug>=3.0.0
|
||||||
|
gunicorn>=21.2.0 # Web服务器(生产环境必需)
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ def check_flask():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""启动网页应用"""
|
|
||||||
print("🌐 启动网页应用...")
|
|
||||||
print("=" * 50)
|
|
||||||
|
|
||||||
if not check_flask():
|
if not check_flask():
|
||||||
return False
|
return False
|
||||||
@@ -36,13 +33,13 @@ def main():
|
|||||||
|
|
||||||
print("\n🚀 正在启动网页服务器...")
|
print("\n🚀 正在启动网页服务器...")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("📱 访问地址: http://localhost:5000")
|
print("📱 访问地址: http://localhost:7400")
|
||||||
print("📝 背诵排序: http://localhost:5000/recitation")
|
print("📝 背诵排序: http://localhost:7400/recitation")
|
||||||
print("\n按 Ctrl+C 停止服务器\n")
|
print("\n按 Ctrl+C 停止服务器\n")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from web_app import app
|
from web_app import app
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=7400)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n👋 服务器已停止")
|
print("\n👋 服务器已停止")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
.recitation-container {
|
.recitation-container {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate; /* 创建新的堆叠上下文,防止外部覆盖层 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 防止意外覆盖层显示 - 只清除空的或意外的伪元素 */
|
||||||
|
.recitation-container *::before,
|
||||||
|
.recitation-container *::after {
|
||||||
|
/* 保留正常使用的伪元素,只阻止意外内容 */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保容器内所有元素正常显示,无意外覆盖 */
|
||||||
|
.items-list,
|
||||||
|
.sorted-list {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: white;
|
||||||
|
overflow: hidden; /* 防止内容溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 转盘容器保持相对定位,确保指针正常显示 */
|
||||||
|
.wheel-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: visible; /* 转盘指针需要可见 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-section,
|
.input-section,
|
||||||
@@ -70,7 +95,7 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 转盘样式 */
|
/* ת<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ */
|
||||||
.wheel-container {
|
.wheel-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
@@ -147,7 +172,75 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 排序结果列表 */
|
/* 掌握按钮样式 */
|
||||||
|
.mastery-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mastered {
|
||||||
|
padding: 12px 30px;
|
||||||
|
font-size: 1em;
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 4px 15px rgba(67, 233, 123, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mastered:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(67, 233, 123, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-mastered:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-forgot {
|
||||||
|
padding: 12px 30px;
|
||||||
|
font-size: 1em;
|
||||||
|
background: linear-gradient(135deg, #f5576c 0%, #fa709a 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 4px 15px rgba(245, 87, 108, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-forgot:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(245, 87, 108, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-forgot:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 掌握情况信息 */
|
||||||
|
.mastery-info {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastery-info #remainingNum {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD> */
|
||||||
.sorted-list {
|
.sorted-list {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -189,9 +282,30 @@
|
|||||||
.sorted-item-text {
|
.sorted-item-text {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 导出按钮样式 */
|
/* 已掌握的项目样式 */
|
||||||
|
.sorted-item-mastered {
|
||||||
|
background: #d4edda;
|
||||||
|
border-left-color: #28a745;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorted-item-mastered .sorted-item-text {
|
||||||
|
color: #155724;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mastered-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ť<EFBFBD><C5A5>ʽ */
|
||||||
.export-buttons {
|
.export-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -219,7 +333,7 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* <EFBFBD><EFBFBD>Ӧʽ<EFBFBD><EFBFBD><EFBFBD> */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.wheel-container {
|
.wheel-container {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// 分析功能脚本
|
// 营养分析功能脚本
|
||||||
|
|
||||||
let currentUserId = null;
|
let currentUserId = null;
|
||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const loginSection = document.getElementById('loginSection');
|
const loginSection = document.getElementById('loginSection');
|
||||||
const requestSection = document.getElementById('requestSection');
|
const requestSection = document.getElementById('requestSection');
|
||||||
const analysisSection = document.getElementById('analysisSection');
|
const analysisSection = document.getElementById('analysisSection');
|
||||||
@@ -14,27 +14,28 @@ const analyzeBtn = document.getElementById('analyzeBtn');
|
|||||||
const mealDataTextarea = document.getElementById('mealData');
|
const mealDataTextarea = document.getElementById('mealData');
|
||||||
const analysisResult = document.getElementById('analysisResult');
|
const analysisResult = document.getElementById('analysisResult');
|
||||||
|
|
||||||
// 显示消息
|
// 显示消息
|
||||||
function showMessage(message, type = 'info') {
|
function showMessage(message, type = 'info') {
|
||||||
messageArea.textContent = message;
|
messageArea.textContent = message;
|
||||||
messageArea.className = `message-area ${type}`;
|
messageArea.className = `message-area ${type}`;
|
||||||
|
messageArea.style.display = 'block';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
messageArea.className = 'message-area';
|
messageArea.className = 'message-area';
|
||||||
messageArea.style.display = 'none';
|
messageArea.style.display = 'none';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户登录
|
// 用户登录
|
||||||
loginBtn.addEventListener('click', async () => {
|
loginBtn.addEventListener('click', async () => {
|
||||||
const userId = userIdInput.value.trim();
|
const userId = userIdInput.value.trim();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
showMessage('请输入用户ID', 'error');
|
showMessage('请输入用户ID', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loginBtn.disabled = true;
|
loginBtn.disabled = true;
|
||||||
loginBtn.textContent = '登录中...';
|
loginBtn.textContent = '登录中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/user/login', {
|
const response = await fetch('/api/user/login', {
|
||||||
@@ -53,30 +54,30 @@ loginBtn.addEventListener('click', async () => {
|
|||||||
currentUserId = userId;
|
currentUserId = userId;
|
||||||
loginSection.style.display = 'none';
|
loginSection.style.display = 'none';
|
||||||
requestSection.style.display = 'block';
|
requestSection.style.display = 'block';
|
||||||
showMessage(`欢迎,${data.name || userId}!`, 'success');
|
showMessage(`欢迎,${data.name || userId}!`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '登录失败', 'error');
|
showMessage(data.message || '登录失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
showMessage('登录失败,请检查网络连接', 'error');
|
showMessage('登录失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = '登录';
|
loginBtn.textContent = '登录';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 开始分析
|
// 开始分析
|
||||||
analyzeBtn.addEventListener('click', async () => {
|
analyzeBtn.addEventListener('click', async () => {
|
||||||
const mealDataText = mealDataTextarea.value.trim();
|
const mealDataText = mealDataTextarea.value.trim();
|
||||||
|
|
||||||
if (!mealDataText) {
|
if (!mealDataText) {
|
||||||
showMessage('请输入餐食信息', 'error');
|
showMessage('请输入餐食信息', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
analyzeBtn.disabled = true;
|
analyzeBtn.disabled = true;
|
||||||
analyzeBtn.textContent = '分析中...';
|
analyzeBtn.textContent = '分析中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/analysis/nutrition', {
|
const response = await fetch('/api/analysis/nutrition', {
|
||||||
@@ -97,37 +98,47 @@ analyzeBtn.addEventListener('click', async () => {
|
|||||||
if (data.success && data.analysis) {
|
if (data.success && data.analysis) {
|
||||||
displayAnalysis(data.analysis);
|
displayAnalysis(data.analysis);
|
||||||
analysisSection.style.display = 'block';
|
analysisSection.style.display = 'block';
|
||||||
showMessage('分析完成!', 'success');
|
showMessage('分析完成!', 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '分析失败', 'error');
|
showMessage(data.message || '分析失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('分析失败:', error);
|
console.error('分析失败:', error);
|
||||||
showMessage('分析失败,请检查网络连接', 'error');
|
showMessage('分析失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
analyzeBtn.disabled = false;
|
analyzeBtn.disabled = false;
|
||||||
analyzeBtn.textContent = '开始分析';
|
analyzeBtn.textContent = '开始分析';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示分析结果
|
// 显示分析结果
|
||||||
function displayAnalysis(analysis) {
|
function displayAnalysis(analysis) {
|
||||||
analysisResult.innerHTML = '';
|
analysisResult.innerHTML = '';
|
||||||
|
|
||||||
if (typeof analysis === 'string') {
|
if (typeof analysis === 'string') {
|
||||||
// 如果是字符串,直接显示
|
// 如果是字符串,直接显示
|
||||||
analysisResult.innerHTML = `<div class="analysis-section"><p>${analysis.replace(/\n/g, '<br>')}</p></div>`;
|
analysisResult.innerHTML = `<div class="analysis-section"><p>${escapeHtml(analysis).replace(/\n/g, '<br>')}</p></div>`;
|
||||||
} else if (analysis.analysis) {
|
} else if (analysis.analysis) {
|
||||||
// 如果包含analysis字段
|
// 如果有analysis字段
|
||||||
analysisResult.innerHTML = `<div class="analysis-section"><h4>分析结果</h4><p>${analysis.analysis.replace(/\n/g, '<br>')}</p></div>`;
|
analysisResult.innerHTML = `<div class="analysis-section"><h4>营养分析</h4><p>${escapeHtml(analysis.analysis).replace(/\n/g, '<br>')}</p></div>`;
|
||||||
} else {
|
} else {
|
||||||
// 如果是对象,格式化显示
|
// 如果是对象,格式化显示
|
||||||
for (const [key, value] of Object.entries(analysis)) {
|
for (const [key, value] of Object.entries(analysis)) {
|
||||||
const section = document.createElement('div');
|
const section = document.createElement('div');
|
||||||
section.className = 'analysis-section';
|
section.className = 'analysis-section';
|
||||||
section.innerHTML = `<h4>${key}</h4><p>${typeof value === 'string' ? value.replace(/\n/g, '<br>') : JSON.stringify(value, null, 2)}</p>`;
|
const displayKey = escapeHtml(key);
|
||||||
|
const displayValue = typeof value === 'string'
|
||||||
|
? escapeHtml(value).replace(/\n/g, '<br>')
|
||||||
|
: escapeHtml(JSON.stringify(value, null, 2));
|
||||||
|
section.innerHTML = `<h4>${displayKey}</h4><p>${displayValue}</p>`;
|
||||||
analysisResult.appendChild(section);
|
analysisResult.appendChild(section);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTML转义函数,防止XSS攻击
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// 数据采集功能脚本
|
// 数据采集功能脚本
|
||||||
|
|
||||||
let currentUserId = null;
|
let currentUserId = null;
|
||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const loginSection = document.getElementById('loginSection');
|
const loginSection = document.getElementById('loginSection');
|
||||||
const basicQuestionnaire = document.getElementById('basicQuestionnaire');
|
const basicQuestionnaire = document.getElementById('basicQuestionnaire');
|
||||||
const mealRecord = document.getElementById('mealRecord');
|
const mealRecord = document.getElementById('mealRecord');
|
||||||
@@ -15,28 +15,29 @@ const userNameInput = document.getElementById('userName');
|
|||||||
const submitBasicBtn = document.getElementById('submitBasicBtn');
|
const submitBasicBtn = document.getElementById('submitBasicBtn');
|
||||||
const submitMealBtn = document.getElementById('submitMealBtn');
|
const submitMealBtn = document.getElementById('submitMealBtn');
|
||||||
|
|
||||||
// 显示消息
|
// 显示消息
|
||||||
function showMessage(message, type = 'info') {
|
function showMessage(message, type = 'info') {
|
||||||
messageArea.textContent = message;
|
messageArea.textContent = message;
|
||||||
messageArea.className = `message-area ${type}`;
|
messageArea.className = `message-area ${type}`;
|
||||||
|
messageArea.style.display = 'block';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
messageArea.className = 'message-area';
|
messageArea.className = 'message-area';
|
||||||
messageArea.style.display = 'none';
|
messageArea.style.display = 'none';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户登录
|
// 用户登录/注册
|
||||||
loginBtn.addEventListener('click', async () => {
|
loginBtn.addEventListener('click', async () => {
|
||||||
const userId = userIdInput.value.trim();
|
const userId = userIdInput.value.trim();
|
||||||
const userName = userNameInput.value.trim();
|
const userName = userNameInput.value.trim();
|
||||||
|
|
||||||
if (!userId || !userName) {
|
if (!userId || !userName) {
|
||||||
showMessage('请输入用户ID和姓名', 'error');
|
showMessage('请输入用户ID和姓名', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loginBtn.disabled = true;
|
loginBtn.disabled = true;
|
||||||
loginBtn.textContent = '登录中...';
|
loginBtn.textContent = '登录中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/user/register', {
|
const response = await fetch('/api/user/register', {
|
||||||
@@ -57,20 +58,20 @@ loginBtn.addEventListener('click', async () => {
|
|||||||
loginSection.style.display = 'none';
|
loginSection.style.display = 'none';
|
||||||
basicQuestionnaire.style.display = 'block';
|
basicQuestionnaire.style.display = 'block';
|
||||||
mealRecord.style.display = 'block';
|
mealRecord.style.display = 'block';
|
||||||
showMessage('登录成功!', 'success');
|
showMessage('注册成功!', 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '登录失败', 'error');
|
showMessage(data.message || '注册失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('注册失败:', error);
|
||||||
showMessage('登录失败,请检查网络连接', 'error');
|
showMessage('注册失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = '登录/注册';
|
loginBtn.textContent = '登录/注册';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 提交基础信息
|
// 提交基础信息
|
||||||
submitBasicBtn.addEventListener('click', async () => {
|
submitBasicBtn.addEventListener('click', async () => {
|
||||||
const age = document.getElementById('age').value;
|
const age = document.getElementById('age').value;
|
||||||
const gender = document.getElementById('gender').value;
|
const gender = document.getElementById('gender').value;
|
||||||
@@ -79,12 +80,12 @@ submitBasicBtn.addEventListener('click', async () => {
|
|||||||
const activityLevel = document.getElementById('activityLevel').value;
|
const activityLevel = document.getElementById('activityLevel').value;
|
||||||
|
|
||||||
if (!age || !gender || !height || !weight || !activityLevel) {
|
if (!age || !gender || !height || !weight || !activityLevel) {
|
||||||
showMessage('请填写完整的基础信息', 'error');
|
showMessage('请填写完整的基础信息', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
submitBasicBtn.disabled = true;
|
submitBasicBtn.disabled = true;
|
||||||
submitBasicBtn.textContent = '提交中...';
|
submitBasicBtn.textContent = '提交中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/questionnaire/submit', {
|
const response = await fetch('/api/questionnaire/submit', {
|
||||||
@@ -108,20 +109,20 @@ submitBasicBtn.addEventListener('click', async () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showMessage('基础信息提交成功!', 'success');
|
showMessage('基础信息提交成功!', 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '提交失败', 'error');
|
showMessage(data.message || '提交失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提交失败:', error);
|
console.error('提交失败:', error);
|
||||||
showMessage('提交失败,请检查网络连接', 'error');
|
showMessage('提交失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
submitBasicBtn.disabled = false;
|
submitBasicBtn.disabled = false;
|
||||||
submitBasicBtn.textContent = '提交基础信息';
|
submitBasicBtn.textContent = '提交基础信息';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 记录餐食
|
// 记录餐食
|
||||||
submitMealBtn.addEventListener('click', async () => {
|
submitMealBtn.addEventListener('click', async () => {
|
||||||
const mealDate = document.getElementById('mealDate').value || new Date().toISOString().split('T')[0];
|
const mealDate = document.getElementById('mealDate').value || new Date().toISOString().split('T')[0];
|
||||||
const mealType = document.getElementById('mealType').value;
|
const mealType = document.getElementById('mealType').value;
|
||||||
@@ -130,20 +131,20 @@ submitMealBtn.addEventListener('click', async () => {
|
|||||||
const satisfaction = document.getElementById('satisfaction').value;
|
const satisfaction = document.getElementById('satisfaction').value;
|
||||||
|
|
||||||
if (!foodsText || !calories) {
|
if (!foodsText || !calories) {
|
||||||
showMessage('请填写完整的餐食信息', 'error');
|
showMessage('请填写完整的餐食信息', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析食物列表
|
// 解析食物列表
|
||||||
const foods = foodsText.split('\n').filter(line => line.trim());
|
const foods = foodsText.split('\n').filter(line => line.trim());
|
||||||
const quantities = foods.map(f => {
|
const quantities = foods.map(f => {
|
||||||
const parts = f.trim().split(/\s+/);
|
const parts = f.trim().split(/\s+/);
|
||||||
return parts.length > 1 ? parts.slice(1).join(' ') : '适量';
|
return parts.length > 1 ? parts.slice(1).join(' ') : '适量';
|
||||||
});
|
});
|
||||||
const foodNames = foods.map(f => f.trim().split(/\s+/)[0]);
|
const foodNames = foods.map(f => f.trim().split(/\s+/)[0]);
|
||||||
|
|
||||||
submitMealBtn.disabled = true;
|
submitMealBtn.disabled = true;
|
||||||
submitMealBtn.textContent = '记录中...';
|
submitMealBtn.textContent = '记录中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/meal/record', {
|
const response = await fetch('/api/meal/record', {
|
||||||
@@ -166,28 +167,27 @@ submitMealBtn.addEventListener('click', async () => {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showMessage('餐食记录成功!', 'success');
|
showMessage('餐食记录成功!', 'success');
|
||||||
// 清空表单
|
// 清空表单
|
||||||
document.getElementById('foods').value = '';
|
document.getElementById('foods').value = '';
|
||||||
document.getElementById('calories').value = '';
|
document.getElementById('calories').value = '';
|
||||||
document.getElementById('satisfaction').value = '3';
|
document.getElementById('satisfaction').value = '3';
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '记录失败', 'error');
|
showMessage(data.message || '记录失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('记录失败:', error);
|
console.error('记录失败:', error);
|
||||||
showMessage('记录失败,请检查网络连接', 'error');
|
showMessage('记录失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
submitMealBtn.disabled = false;
|
submitMealBtn.disabled = false;
|
||||||
submitMealBtn.textContent = '记录餐食';
|
submitMealBtn.textContent = '记录餐食';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置默认日期为今天
|
// 设置默认日期为今天
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mealDateInput = document.getElementById('mealDate');
|
const mealDateInput = document.getElementById('mealDate');
|
||||||
if (mealDateInput) {
|
if (mealDateInput) {
|
||||||
mealDateInput.value = new Date().toISOString().split('T')[0];
|
mealDateInput.value = new Date().toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
// 背诵排序功能脚本
|
// 背诵排序功能脚本
|
||||||
|
|
||||||
let extractedItems = [];
|
let extractedItems = [];
|
||||||
let sortedItems = [];
|
let sortedItems = []; // 原始排序后的所有项目
|
||||||
|
let availableItems = []; // 当前可用于转盘的项目(排除已掌握的)
|
||||||
|
let masteredItems = []; // 已掌握的项目列表
|
||||||
let currentSpinIndex = 0;
|
let currentSpinIndex = 0;
|
||||||
|
let currentSelectedItem = null; // 当前转盘选中的项目
|
||||||
let isSpinning = false;
|
let isSpinning = false;
|
||||||
|
|
||||||
// 颜色配置 - 转盘使用不同颜色
|
// 颜色配置 - 转盘使用不同颜色
|
||||||
const colors = [
|
const colors = [
|
||||||
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
||||||
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
||||||
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
||||||
];
|
];
|
||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const textInput = document.getElementById('textInput');
|
const textInput = document.getElementById('textInput');
|
||||||
const extractBtn = document.getElementById('extractBtn');
|
const extractBtn = document.getElementById('extractBtn');
|
||||||
const extractedSection = document.getElementById('extractedSection');
|
const extractedSection = document.getElementById('extractedSection');
|
||||||
@@ -29,18 +32,78 @@ const resetBtn = document.getElementById('resetBtn');
|
|||||||
const exportTxtBtn = document.getElementById('exportTxtBtn');
|
const exportTxtBtn = document.getElementById('exportTxtBtn');
|
||||||
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
||||||
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
||||||
|
const masteredBtn = document.getElementById('masteredBtn');
|
||||||
|
const forgotBtn = document.getElementById('forgotBtn');
|
||||||
|
const masteryButtons = document.getElementById('masteryButtons');
|
||||||
|
const remainingNum = document.getElementById('remainingNum');
|
||||||
|
|
||||||
// 本地存储键名
|
// 本地存储键
|
||||||
const STORAGE_KEY_EXTRACTED = 'recitation_extracted_items';
|
const STORAGE_KEY_EXTRACTED = 'recitation_extracted_items';
|
||||||
const STORAGE_KEY_SORTED = 'recitation_sorted_items';
|
const STORAGE_KEY_SORTED = 'recitation_sorted_items';
|
||||||
const STORAGE_KEY_ORIGINAL_TEXT = 'recitation_original_text';
|
const STORAGE_KEY_ORIGINAL_TEXT = 'recitation_original_text';
|
||||||
|
const STORAGE_KEY_MASTERED = 'recitation_mastered_items';
|
||||||
|
|
||||||
// 页面加载时恢复数据
|
// 页面加载时恢复数据并清理异常元素
|
||||||
window.addEventListener('DOMContentLoaded', () => {
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 首先清理任何可能的异常覆盖层或调试元素
|
||||||
|
cleanupAbnormalElements();
|
||||||
|
|
||||||
|
// 然后恢复保存的数据
|
||||||
restoreFromStorage();
|
restoreFromStorage();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 保存到本地存储
|
// 清理异常元素 - 移除可能的覆盖层、时间戳等调试元素
|
||||||
|
function cleanupAbnormalElements() {
|
||||||
|
try {
|
||||||
|
// 查找所有可能包含时间戳格式的文本节点(如 25-11-03-16, 03-16:41 等)
|
||||||
|
const walker = document.createTreeWalker(
|
||||||
|
document.body,
|
||||||
|
NodeFilter.SHOW_TEXT,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
let node;
|
||||||
|
const timestampPattern = /\d{2}-\d{2}-\d{2}-\d{2}|\d{2}-\d{2}:\d{2}/;
|
||||||
|
const nodesToRemove = [];
|
||||||
|
|
||||||
|
while (node = walker.nextNode()) {
|
||||||
|
if (timestampPattern.test(node.textContent.trim())) {
|
||||||
|
// 找到包含时间戳的文本节点,移除其父元素(如果它是独立显示的元素)
|
||||||
|
const parent = node.parentElement;
|
||||||
|
if (parent && (
|
||||||
|
parent.style.opacity && parseFloat(parent.style.opacity) < 1 ||
|
||||||
|
parent.style.position === 'absolute' ||
|
||||||
|
parent.style.position === 'fixed'
|
||||||
|
)) {
|
||||||
|
nodesToRemove.push(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除异常元素
|
||||||
|
nodesToRemove.forEach(el => {
|
||||||
|
if (el && el.parentNode) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移除任何具有明显时间戳样式的元素(半透明、绝对定位等)
|
||||||
|
document.querySelectorAll('*').forEach(el => {
|
||||||
|
const style = window.getComputedStyle(el);
|
||||||
|
if (style.opacity && parseFloat(style.opacity) < 0.8 &&
|
||||||
|
(style.position === 'absolute' || style.position === 'fixed') &&
|
||||||
|
el.textContent.match(timestampPattern)) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('清理异常元素时出错:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
function saveToStorage() {
|
function saveToStorage() {
|
||||||
try {
|
try {
|
||||||
if (extractedItems.length > 0) {
|
if (extractedItems.length > 0) {
|
||||||
@@ -52,17 +115,21 @@ function saveToStorage() {
|
|||||||
if (textInput.value.trim()) {
|
if (textInput.value.trim()) {
|
||||||
localStorage.setItem(STORAGE_KEY_ORIGINAL_TEXT, textInput.value);
|
localStorage.setItem(STORAGE_KEY_ORIGINAL_TEXT, textInput.value);
|
||||||
}
|
}
|
||||||
|
if (masteredItems.length > 0) {
|
||||||
|
localStorage.setItem(STORAGE_KEY_MASTERED, JSON.stringify(masteredItems));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存到本地存储失败:', e);
|
console.error('保存到本地存储失败:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从本地存储恢复
|
// 从本地存储恢复
|
||||||
function restoreFromStorage() {
|
function restoreFromStorage() {
|
||||||
try {
|
try {
|
||||||
const savedExtracted = localStorage.getItem(STORAGE_KEY_EXTRACTED);
|
const savedExtracted = localStorage.getItem(STORAGE_KEY_EXTRACTED);
|
||||||
const savedSorted = localStorage.getItem(STORAGE_KEY_SORTED);
|
const savedSorted = localStorage.getItem(STORAGE_KEY_SORTED);
|
||||||
const savedText = localStorage.getItem(STORAGE_KEY_ORIGINAL_TEXT);
|
const savedText = localStorage.getItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||||
|
const savedMastered = localStorage.getItem(STORAGE_KEY_MASTERED);
|
||||||
|
|
||||||
if (savedText) {
|
if (savedText) {
|
||||||
textInput.value = savedText;
|
textInput.value = savedText;
|
||||||
@@ -75,36 +142,68 @@ function restoreFromStorage() {
|
|||||||
textInput.disabled = true;
|
textInput.disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (savedMastered) {
|
||||||
|
masteredItems = JSON.parse(savedMastered);
|
||||||
|
}
|
||||||
|
|
||||||
if (savedSorted) {
|
if (savedSorted) {
|
||||||
sortedItems = JSON.parse(savedSorted);
|
sortedItems = JSON.parse(savedSorted);
|
||||||
|
updateAvailableItems(); // 更新可用项目列表
|
||||||
displaySortedItems(sortedItems);
|
displaySortedItems(sortedItems);
|
||||||
createWheel(sortedItems);
|
createWheel(availableItems);
|
||||||
wheelSection.style.display = 'block';
|
wheelSection.style.display = 'block';
|
||||||
resultSection.style.display = 'block';
|
resultSection.style.display = 'block';
|
||||||
|
updateMasteryInfo();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('从本地存储恢复失败:', e);
|
console.error('从本地存储恢复失败:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除本地存储
|
// 清除本地存储 - 完全清理所有相关数据
|
||||||
function clearStorage() {
|
function clearStorage() {
|
||||||
localStorage.removeItem(STORAGE_KEY_EXTRACTED);
|
try {
|
||||||
localStorage.removeItem(STORAGE_KEY_SORTED);
|
localStorage.removeItem(STORAGE_KEY_EXTRACTED);
|
||||||
localStorage.removeItem(STORAGE_KEY_ORIGINAL_TEXT);
|
localStorage.removeItem(STORAGE_KEY_SORTED);
|
||||||
|
localStorage.removeItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||||
|
localStorage.removeItem(STORAGE_KEY_MASTERED);
|
||||||
|
|
||||||
|
// 清除任何可能存在的其他相关键
|
||||||
|
const keysToRemove = [];
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key && key.startsWith('recitation_')) {
|
||||||
|
keysToRemove.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除本地存储失败:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取知识点
|
// 更新可用项目列表(排除已掌握的)
|
||||||
|
function updateAvailableItems() {
|
||||||
|
availableItems = sortedItems.filter(item => !masteredItems.includes(item));
|
||||||
|
updateMasteryInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新掌握情况信息
|
||||||
|
function updateMasteryInfo() {
|
||||||
|
remainingNum.textContent = availableItems.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取知识点
|
||||||
extractBtn.addEventListener('click', async () => {
|
extractBtn.addEventListener('click', async () => {
|
||||||
const text = textInput.value.trim();
|
const text = textInput.value.trim();
|
||||||
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
alert('请输入要处理的文本');
|
alert('请输入需要识别的文本');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
extractBtn.disabled = true;
|
extractBtn.disabled = true;
|
||||||
extractBtn.textContent = '识别中...';
|
extractBtn.textContent = '识别中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/extract', {
|
const response = await fetch('/api/extract', {
|
||||||
@@ -122,20 +221,20 @@ extractBtn.addEventListener('click', async () => {
|
|||||||
displayExtractedItems(extractedItems);
|
displayExtractedItems(extractedItems);
|
||||||
extractedSection.style.display = 'block';
|
extractedSection.style.display = 'block';
|
||||||
textInput.disabled = true;
|
textInput.disabled = true;
|
||||||
saveToStorage(); // 保存到本地存储
|
saveToStorage(); // 保存到本地存储
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '提取失败');
|
alert(data.message || '提取失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('提取失败:', error);
|
console.error('提取失败:', error);
|
||||||
alert('提取失败,请检查网络连接');
|
alert('提取失败,请检查网络连接');
|
||||||
} finally {
|
} finally {
|
||||||
extractBtn.disabled = false;
|
extractBtn.disabled = false;
|
||||||
extractBtn.textContent = '识别知识点';
|
extractBtn.textContent = '识别知识点';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示提取的项目
|
// 显示提取到的项目
|
||||||
function displayExtractedItems(items) {
|
function displayExtractedItems(items) {
|
||||||
itemCount.textContent = items.length;
|
itemCount.textContent = items.length;
|
||||||
itemsList.innerHTML = '';
|
itemsList.innerHTML = '';
|
||||||
@@ -148,15 +247,15 @@ function displayExtractedItems(items) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 随机排序
|
// 开始排序
|
||||||
sortBtn.addEventListener('click', async () => {
|
sortBtn.addEventListener('click', async () => {
|
||||||
if (extractedItems.length === 0) {
|
if (extractedItems.length === 0) {
|
||||||
alert('请先提取知识点');
|
alert('请先提取知识点');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sortBtn.disabled = true;
|
sortBtn.disabled = true;
|
||||||
sortBtn.textContent = '排序中...';
|
sortBtn.textContent = '排序中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/sort', {
|
const response = await fetch('/api/sort', {
|
||||||
@@ -171,36 +270,45 @@ sortBtn.addEventListener('click', async () => {
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
sortedItems = data.items;
|
sortedItems = data.items;
|
||||||
|
masteredItems = []; // 重新排序时重置已掌握列表
|
||||||
|
updateAvailableItems(); // 更新可用项目
|
||||||
displaySortedItems(sortedItems);
|
displaySortedItems(sortedItems);
|
||||||
createWheel(sortedItems);
|
createWheel(availableItems);
|
||||||
wheelSection.style.display = 'block';
|
wheelSection.style.display = 'block';
|
||||||
resultSection.style.display = 'block';
|
resultSection.style.display = 'block';
|
||||||
currentSpinIndex = 0;
|
currentSpinIndex = 0;
|
||||||
saveToStorage(); // 保存到本地存储
|
currentSelectedItem = null;
|
||||||
|
masteryButtons.style.display = 'none';
|
||||||
|
saveToStorage(); // 保存到本地存储
|
||||||
} else {
|
} else {
|
||||||
alert(data.message || '排序失败');
|
alert(data.message || '排序失败');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('排序失败:', error);
|
console.error('排序失败:', error);
|
||||||
alert('排序失败,请检查网络连接');
|
alert('排序失败,请检查网络连接');
|
||||||
} finally {
|
} finally {
|
||||||
sortBtn.disabled = false;
|
sortBtn.disabled = false;
|
||||||
sortBtn.textContent = '开始随机排序';
|
sortBtn.textContent = '开始随机排序';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 创建转盘 - 使用SVG实现更真实的转盘效果
|
// 创建转盘 - 使用SVG实现真实转盘效果
|
||||||
function createWheel(items) {
|
function createWheel(items) {
|
||||||
wheel.innerHTML = '';
|
wheel.innerHTML = '';
|
||||||
|
|
||||||
if (items.length === 0) return;
|
if (items.length === 0) {
|
||||||
|
currentItem.textContent = '所有知识点已掌握!';
|
||||||
|
spinBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spinBtn.disabled = false;
|
||||||
const anglePerItem = 360 / items.length;
|
const anglePerItem = 360 / items.length;
|
||||||
const radius = 190; // 转盘半径(考虑边框)
|
const radius = 190; // 转盘半径,考虑边框
|
||||||
const centerX = 200;
|
const centerX = 200;
|
||||||
const centerY = 200;
|
const centerY = 200;
|
||||||
|
|
||||||
// 创建SVG转盘
|
// 创建SVG转盘
|
||||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
svg.setAttribute('width', '400');
|
svg.setAttribute('width', '400');
|
||||||
svg.setAttribute('height', '400');
|
svg.setAttribute('height', '400');
|
||||||
@@ -210,6 +318,7 @@ function createWheel(items) {
|
|||||||
svg.style.left = '0';
|
svg.style.left = '0';
|
||||||
svg.style.width = '100%';
|
svg.style.width = '100%';
|
||||||
svg.style.height = '100%';
|
svg.style.height = '100%';
|
||||||
|
svg.style.transition = 'transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99)';
|
||||||
|
|
||||||
items.forEach((item, index) => {
|
items.forEach((item, index) => {
|
||||||
const startAngle = (index * anglePerItem - 90) * Math.PI / 180;
|
const startAngle = (index * anglePerItem - 90) * Math.PI / 180;
|
||||||
@@ -238,7 +347,7 @@ function createWheel(items) {
|
|||||||
|
|
||||||
svg.appendChild(path);
|
svg.appendChild(path);
|
||||||
|
|
||||||
// 添加文本
|
// 添加文本
|
||||||
const midAngle = (startAngle + endAngle) / 2;
|
const midAngle = (startAngle + endAngle) / 2;
|
||||||
const textRadius = radius * 0.7;
|
const textRadius = radius * 0.7;
|
||||||
const textX = centerX + textRadius * Math.cos(midAngle);
|
const textX = centerX + textRadius * Math.cos(midAngle);
|
||||||
@@ -262,41 +371,47 @@ function createWheel(items) {
|
|||||||
wheel.appendChild(svg);
|
wheel.appendChild(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转动转盘
|
// 转动转盘
|
||||||
spinBtn.addEventListener('click', () => {
|
spinBtn.addEventListener('click', () => {
|
||||||
if (isSpinning || sortedItems.length === 0) return;
|
if (isSpinning || availableItems.length === 0) return;
|
||||||
|
|
||||||
isSpinning = true;
|
isSpinning = true;
|
||||||
spinBtn.disabled = true;
|
spinBtn.disabled = true;
|
||||||
currentItem.textContent = '转盘中...';
|
currentItem.textContent = '转盘中...';
|
||||||
|
masteryButtons.style.display = 'none'; // 隐藏按钮,等待转盘停止
|
||||||
|
currentSelectedItem = null;
|
||||||
|
|
||||||
// 随机选择一个索引(添加多圈旋转效果)
|
// 随机选择一个项目,并增加多圈旋转效果
|
||||||
const randomIndex = Math.floor(Math.random() * sortedItems.length);
|
const randomIndex = Math.floor(Math.random() * availableItems.length);
|
||||||
const spins = 3; // 转3圈
|
const spins = 3; // 转3圈
|
||||||
const anglePerItem = 360 / sortedItems.length;
|
const anglePerItem = 360 / availableItems.length;
|
||||||
// 计算目标角度:多转几圈 + 指向选中项
|
// 计算目标角度:转多圈 + 指向选中项
|
||||||
const targetAngle = spins * 360 + (360 - (randomIndex * anglePerItem) - anglePerItem / 2);
|
const targetAngle = spins * 360 + (360 - (randomIndex * anglePerItem) - anglePerItem / 2);
|
||||||
|
|
||||||
// 获取当前角度
|
// 获取当前角度
|
||||||
const svg = wheel.querySelector('svg');
|
const svg = wheel.querySelector('svg');
|
||||||
|
if (!svg) return;
|
||||||
const currentAngle = getCurrentRotation(svg);
|
const currentAngle = getCurrentRotation(svg);
|
||||||
|
|
||||||
// 计算总旋转角度(考虑当前角度)
|
// 计算总旋转角度(考虑当前角度)
|
||||||
const totalRotation = currentAngle + targetAngle;
|
const totalRotation = currentAngle + targetAngle;
|
||||||
|
|
||||||
svg.style.transform = `rotate(${totalRotation}deg)`;
|
svg.style.transform = `rotate(${totalRotation}deg)`;
|
||||||
|
|
||||||
// 转盘停止后显示结果
|
// 转盘停止后显示结果
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
currentItem.textContent = `${randomIndex + 1}. ${sortedItems[randomIndex]}`;
|
currentSelectedItem = availableItems[randomIndex];
|
||||||
|
currentItem.textContent = `${randomIndex + 1}. ${currentSelectedItem}`;
|
||||||
currentSpinIndex = randomIndex;
|
currentSpinIndex = randomIndex;
|
||||||
isSpinning = false;
|
isSpinning = false;
|
||||||
spinBtn.disabled = false;
|
spinBtn.disabled = false;
|
||||||
|
masteryButtons.style.display = 'flex'; // 显示掌握按钮
|
||||||
}, 3000);
|
}, 3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取当前旋转角度
|
// 获取当前旋转角度
|
||||||
function getCurrentRotation(element) {
|
function getCurrentRotation(element) {
|
||||||
|
if (!element) return 0;
|
||||||
const style = window.getComputedStyle(element);
|
const style = window.getComputedStyle(element);
|
||||||
const transform = style.transform;
|
const transform = style.transform;
|
||||||
if (transform === 'none') return 0;
|
if (transform === 'none') return 0;
|
||||||
@@ -306,7 +421,51 @@ function getCurrentRotation(element) {
|
|||||||
return angle;
|
return angle;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示排序结果
|
// 背会了按钮
|
||||||
|
masteredBtn.addEventListener('click', () => {
|
||||||
|
if (!currentSelectedItem) return;
|
||||||
|
|
||||||
|
// 添加到已掌握列表
|
||||||
|
if (!masteredItems.includes(currentSelectedItem)) {
|
||||||
|
masteredItems.push(currentSelectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新可用项目列表
|
||||||
|
updateAvailableItems();
|
||||||
|
|
||||||
|
// 重新创建转盘(因为项目数量可能改变)
|
||||||
|
createWheel(availableItems);
|
||||||
|
|
||||||
|
// 隐藏按钮和当前项目显示
|
||||||
|
masteryButtons.style.display = 'none';
|
||||||
|
currentItem.textContent = '';
|
||||||
|
currentSelectedItem = null;
|
||||||
|
|
||||||
|
// 重置转盘角度
|
||||||
|
const svg = wheel.querySelector('svg');
|
||||||
|
if (svg) {
|
||||||
|
svg.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存状态
|
||||||
|
saveToStorage();
|
||||||
|
|
||||||
|
// 更新排序结果列表显示(标记已掌握的)
|
||||||
|
displaySortedItems(sortedItems);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 忘记了按钮
|
||||||
|
forgotBtn.addEventListener('click', () => {
|
||||||
|
if (!currentSelectedItem) return;
|
||||||
|
|
||||||
|
// 忘记了的项目保留在列表中,不做任何操作
|
||||||
|
// 隐藏按钮,可以继续转动转盘
|
||||||
|
masteryButtons.style.display = 'none';
|
||||||
|
currentItem.textContent = '';
|
||||||
|
currentSelectedItem = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示排序结果
|
||||||
function displaySortedItems(items) {
|
function displaySortedItems(items) {
|
||||||
sortedList.innerHTML = '';
|
sortedList.innerHTML = '';
|
||||||
|
|
||||||
@@ -314,6 +473,11 @@ function displaySortedItems(items) {
|
|||||||
const itemDiv = document.createElement('div');
|
const itemDiv = document.createElement('div');
|
||||||
itemDiv.className = 'sorted-item';
|
itemDiv.className = 'sorted-item';
|
||||||
|
|
||||||
|
// 如果已掌握,添加特殊样式
|
||||||
|
if (masteredItems.includes(item)) {
|
||||||
|
itemDiv.classList.add('sorted-item-mastered');
|
||||||
|
}
|
||||||
|
|
||||||
const numberSpan = document.createElement('span');
|
const numberSpan = document.createElement('span');
|
||||||
numberSpan.className = 'sorted-item-number';
|
numberSpan.className = 'sorted-item-number';
|
||||||
numberSpan.textContent = index + 1;
|
numberSpan.textContent = index + 1;
|
||||||
@@ -322,20 +486,30 @@ function displaySortedItems(items) {
|
|||||||
textSpan.className = 'sorted-item-text';
|
textSpan.className = 'sorted-item-text';
|
||||||
textSpan.textContent = item;
|
textSpan.textContent = item;
|
||||||
|
|
||||||
|
if (masteredItems.includes(item)) {
|
||||||
|
const masteredIcon = document.createElement('span');
|
||||||
|
masteredIcon.className = 'mastered-icon';
|
||||||
|
masteredIcon.textContent = '✓';
|
||||||
|
textSpan.appendChild(masteredIcon);
|
||||||
|
}
|
||||||
|
|
||||||
itemDiv.appendChild(numberSpan);
|
itemDiv.appendChild(numberSpan);
|
||||||
itemDiv.appendChild(textSpan);
|
itemDiv.appendChild(textSpan);
|
||||||
sortedList.appendChild(itemDiv);
|
sortedList.appendChild(itemDiv);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出功能
|
// 导出功能
|
||||||
exportTxtBtn.addEventListener('click', () => exportData('txt'));
|
exportTxtBtn.addEventListener('click', () => exportData('txt'));
|
||||||
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
||||||
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
||||||
|
|
||||||
function exportData(format) {
|
function exportData(format) {
|
||||||
if (sortedItems.length === 0) {
|
// 导出时只导出当前可用的项目(未掌握的)
|
||||||
alert('没有可导出的数据');
|
const itemsToExport = availableItems.length > 0 ? availableItems : sortedItems;
|
||||||
|
|
||||||
|
if (itemsToExport.length === 0) {
|
||||||
|
alert('没有可导出的数据');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,13 +519,13 @@ function exportData(format) {
|
|||||||
'Content-Type': 'application/json; charset=utf-8'
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
items: sortedItems,
|
items: itemsToExport,
|
||||||
format: format
|
format: format
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('导出失败');
|
throw new Error('导出失败');
|
||||||
}
|
}
|
||||||
return response.blob();
|
return response.blob();
|
||||||
})
|
})
|
||||||
@@ -360,41 +534,74 @@ function exportData(format) {
|
|||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
|
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
|
||||||
a.download = `背诵排序结果_${timestamp}.${format}`;
|
a.download = `背诵排序结果_${timestamp}.${format}`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('导出失败:', error);
|
console.error('导出失败:', error);
|
||||||
alert('导出失败,请重试');
|
alert('导出失败,请稍后重试');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置
|
// 重置 - 完全清理所有数据和显示
|
||||||
resetBtn.addEventListener('click', () => {
|
resetBtn.addEventListener('click', () => {
|
||||||
|
// 重置所有变量
|
||||||
extractedItems = [];
|
extractedItems = [];
|
||||||
sortedItems = [];
|
sortedItems = [];
|
||||||
|
availableItems = [];
|
||||||
|
masteredItems = [];
|
||||||
currentSpinIndex = 0;
|
currentSpinIndex = 0;
|
||||||
|
currentSelectedItem = null;
|
||||||
|
isSpinning = false;
|
||||||
|
|
||||||
|
// 清理输入框
|
||||||
textInput.value = '';
|
textInput.value = '';
|
||||||
textInput.disabled = false;
|
textInput.disabled = false;
|
||||||
|
|
||||||
|
// 隐藏所有区域
|
||||||
extractedSection.style.display = 'none';
|
extractedSection.style.display = 'none';
|
||||||
wheelSection.style.display = 'none';
|
wheelSection.style.display = 'none';
|
||||||
resultSection.style.display = 'none';
|
resultSection.style.display = 'none';
|
||||||
|
|
||||||
|
// 清理提取的项目列表显示
|
||||||
|
itemsList.innerHTML = '';
|
||||||
|
itemCount.textContent = '0';
|
||||||
|
|
||||||
|
// 清理转盘
|
||||||
wheel.innerHTML = '';
|
wheel.innerHTML = '';
|
||||||
const svg = wheel.querySelector('svg');
|
const svg = wheel.querySelector('svg');
|
||||||
if (svg) {
|
if (svg) {
|
||||||
svg.style.transform = 'rotate(0deg)';
|
svg.style.transform = 'rotate(0deg)';
|
||||||
|
svg.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理当前项目显示
|
||||||
currentItem.textContent = '';
|
currentItem.textContent = '';
|
||||||
|
masteryButtons.style.display = 'none';
|
||||||
|
|
||||||
isSpinning = false;
|
// 清理排序结果列表
|
||||||
|
sortedList.innerHTML = '';
|
||||||
|
|
||||||
|
// 重置按钮状态
|
||||||
spinBtn.disabled = false;
|
spinBtn.disabled = false;
|
||||||
|
extractBtn.disabled = false;
|
||||||
|
extractBtn.textContent = '识别知识点';
|
||||||
|
sortBtn.disabled = false;
|
||||||
|
sortBtn.textContent = '开始随机排序';
|
||||||
|
|
||||||
clearStorage(); // 清除本地存储
|
// 更新掌握情况信息
|
||||||
|
remainingNum.textContent = '0';
|
||||||
|
|
||||||
|
// 清除本地存储
|
||||||
|
clearStorage();
|
||||||
|
|
||||||
|
// 强制清理DOM中可能残留的元素
|
||||||
|
document.querySelectorAll('.item-tag').forEach(el => el.remove());
|
||||||
|
document.querySelectorAll('.sorted-item').forEach(el => el.remove());
|
||||||
|
|
||||||
|
// 确认清理完成
|
||||||
|
console.log('已完全重置页面,可以开始新的背诵任务');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// 推荐功能脚本
|
// 推荐功能脚本
|
||||||
|
|
||||||
let currentUserId = null;
|
let currentUserId = null;
|
||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const loginSection = document.getElementById('loginSection');
|
const loginSection = document.getElementById('loginSection');
|
||||||
const requestSection = document.getElementById('requestSection');
|
const requestSection = document.getElementById('requestSection');
|
||||||
const recommendationsSection = document.getElementById('recommendationsSection');
|
const recommendationsSection = document.getElementById('recommendationsSection');
|
||||||
@@ -14,27 +14,28 @@ const getRecommendationBtn = document.getElementById('getRecommendationBtn');
|
|||||||
const mealTypeSelect = document.getElementById('mealType');
|
const mealTypeSelect = document.getElementById('mealType');
|
||||||
const recommendationsList = document.getElementById('recommendationsList');
|
const recommendationsList = document.getElementById('recommendationsList');
|
||||||
|
|
||||||
// 显示消息
|
// 显示消息
|
||||||
function showMessage(message, type = 'info') {
|
function showMessage(message, type = 'info') {
|
||||||
messageArea.textContent = message;
|
messageArea.textContent = message;
|
||||||
messageArea.className = `message-area ${type}`;
|
messageArea.className = `message-area ${type}`;
|
||||||
|
messageArea.style.display = 'block';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
messageArea.className = 'message-area';
|
messageArea.className = 'message-area';
|
||||||
messageArea.style.display = 'none';
|
messageArea.style.display = 'none';
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户登录
|
// 用户登录
|
||||||
loginBtn.addEventListener('click', async () => {
|
loginBtn.addEventListener('click', async () => {
|
||||||
const userId = userIdInput.value.trim();
|
const userId = userIdInput.value.trim();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
showMessage('请输入用户ID', 'error');
|
showMessage('请输入用户ID', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loginBtn.disabled = true;
|
loginBtn.disabled = true;
|
||||||
loginBtn.textContent = '登录中...';
|
loginBtn.textContent = '登录中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/user/login', {
|
const response = await fetch('/api/user/login', {
|
||||||
@@ -53,25 +54,25 @@ loginBtn.addEventListener('click', async () => {
|
|||||||
currentUserId = userId;
|
currentUserId = userId;
|
||||||
loginSection.style.display = 'none';
|
loginSection.style.display = 'none';
|
||||||
requestSection.style.display = 'block';
|
requestSection.style.display = 'block';
|
||||||
showMessage(`欢迎,${data.name || userId}!`, 'success');
|
showMessage(`欢迎,${data.name || userId}!`, 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '登录失败', 'error');
|
showMessage(data.message || '登录失败', 'error');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录失败:', error);
|
console.error('登录失败:', error);
|
||||||
showMessage('登录失败,请检查网络连接', 'error');
|
showMessage('登录失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = '登录';
|
loginBtn.textContent = '登录';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取推荐
|
// 获取推荐
|
||||||
getRecommendationBtn.addEventListener('click', async () => {
|
getRecommendationBtn.addEventListener('click', async () => {
|
||||||
const mealType = mealTypeSelect.value;
|
const mealType = mealTypeSelect.value;
|
||||||
|
|
||||||
getRecommendationBtn.disabled = true;
|
getRecommendationBtn.disabled = true;
|
||||||
getRecommendationBtn.textContent = '获取中...';
|
getRecommendationBtn.textContent = '获取中...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/recommendation/get', {
|
const response = await fetch('/api/recommendation/get', {
|
||||||
@@ -92,20 +93,20 @@ getRecommendationBtn.addEventListener('click', async () => {
|
|||||||
if (data.success && data.recommendations && data.recommendations.length > 0) {
|
if (data.success && data.recommendations && data.recommendations.length > 0) {
|
||||||
displayRecommendations(data.recommendations);
|
displayRecommendations(data.recommendations);
|
||||||
recommendationsSection.style.display = 'block';
|
recommendationsSection.style.display = 'block';
|
||||||
showMessage('推荐获取成功!', 'success');
|
showMessage('推荐获取成功!', 'success');
|
||||||
} else {
|
} else {
|
||||||
showMessage(data.message || '暂无推荐,请先完善个人数据', 'info');
|
showMessage(data.message || '暂无推荐内容,请先完善个人数据', 'info');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取推荐失败:', error);
|
console.error('获取推荐失败:', error);
|
||||||
showMessage('获取推荐失败,请检查网络连接', 'error');
|
showMessage('获取推荐失败,请检查网络连接', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
getRecommendationBtn.disabled = false;
|
getRecommendationBtn.disabled = false;
|
||||||
getRecommendationBtn.textContent = '获取推荐';
|
getRecommendationBtn.textContent = '获取推荐';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 显示推荐结果
|
// 显示推荐结果
|
||||||
function displayRecommendations(recommendations) {
|
function displayRecommendations(recommendations) {
|
||||||
recommendationsList.innerHTML = '';
|
recommendationsList.innerHTML = '';
|
||||||
|
|
||||||
@@ -120,16 +121,34 @@ function displayRecommendations(recommendations) {
|
|||||||
foods = [rec.food];
|
foods = [rec.food];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const foodsHtml = foods.map(food =>
|
||||||
|
`<div class="food-item">${escapeHtml(food)}</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const confidenceHtml = rec.confidence
|
||||||
|
? `<div class="confidence">置信度: ${(rec.confidence * 100).toFixed(1)}%</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const reasonHtml = rec.reason
|
||||||
|
? `<div style="margin-top: 10px; color: #666;">推荐理由: ${escapeHtml(rec.reason)}</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<h3>推荐方案 ${index + 1}</h3>
|
<h3>推荐方案 ${index + 1}</h3>
|
||||||
<div class="food-list">
|
<div class="food-list">
|
||||||
${foods.map(food => `<div class="food-item">${food}</div>`).join('')}
|
${foodsHtml}
|
||||||
</div>
|
</div>
|
||||||
${rec.confidence ? `<div class="confidence">置信度: ${(rec.confidence * 100).toFixed(1)}%</div>` : ''}
|
${confidenceHtml}
|
||||||
${rec.reason ? `<div style="margin-top: 10px; color: #666;">推荐理由: ${rec.reason}</div>` : ''}
|
${reasonHtml}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
recommendationsList.appendChild(card);
|
recommendationsList.appendChild(card);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTML转义函数,防止XSS攻击
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,64 +3,59 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>营养分析 - 个性化饮食推荐助手</title>
|
<title>营养分析 - 个性化饮食推荐助手</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/analysis.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/analysis.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>? 营养分析</h1>
|
<h1>🍎 营养分析</h1>
|
||||||
<p class="subtitle">AI智能营养分析与建议</p>
|
<p class="subtitle">AI智能营养分析</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/" class="nav-item">首页</a>
|
<a href="/" class="nav-item">首页</a>
|
||||||
<a href="/data-collection" class="nav-item">数据采集</a>
|
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||||
<a href="/recommendation" class="nav-item">智能推荐</a>
|
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||||
<a href="/analysis" class="nav-item active">营养分析</a>
|
<a href="/analysis" class="nav-item active">营养分析</a>
|
||||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="analysis-container">
|
<div class="analysis-container">
|
||||||
<!-- 用户登录区域 -->
|
|
||||||
<div id="loginSection" class="section">
|
<div id="loginSection" class="section">
|
||||||
<h2>用户登录</h2>
|
<h2>用户登录</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="userId">用户ID:</label>
|
<label for="userId">用户ID:</label>
|
||||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||||
</div>
|
</div>
|
||||||
<button id="loginBtn" class="btn btn-primary">登录</button>
|
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分析请求区域 -->
|
|
||||||
<div id="requestSection" class="section" style="display: none;">
|
<div id="requestSection" class="section" style="display: none;">
|
||||||
<h2>营养分析</h2>
|
<h2>营养分析</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>餐食信息:</label>
|
<label>餐食信息:</label>
|
||||||
<textarea id="mealData" class="form-textarea" rows="5" placeholder="请输入餐食信息,例如: 早餐:燕麦粥、香蕉、牛奶 热量:350大卡"></textarea>
|
<textarea id="mealData" class="form-textarea" rows="5" placeholder="请输入餐食信息: 例如:燕麦粥、香蕉、牛奶 热量:350大卡"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<button id="analyzeBtn" class="btn btn-primary">开始分析</button>
|
<button id="analyzeBtn" class="btn btn-primary">开始分析</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 分析结果显示 -->
|
|
||||||
<div id="analysisSection" class="section" style="display: none;">
|
<div id="analysisSection" class="section" style="display: none;">
|
||||||
<h2>分析结果</h2>
|
<h2>分析结果</h2>
|
||||||
<div id="analysisResult" class="analysis-result"></div>
|
<div id="analysisResult" class="analysis-result"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作提示 -->
|
|
||||||
<div id="messageArea" class="message-area"></div>
|
<div id="messageArea" class="message-area"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>© 2024 个性化饮食推荐助手</p>
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/analysis.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/analysis.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -3,118 +3,106 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>数据采集 - 个性化饮食推荐助手</title>
|
<title>数据采集 - 个性化饮食推荐助手</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/data_collection.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/data_collection.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>? 数据采集</h1>
|
<h1>📝 数据采集</h1>
|
||||||
<p class="subtitle">建立你的个人饮食档案</p>
|
<p class="subtitle">建立你的个人饮食档案</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/" class="nav-item">首页</a>
|
<a href="/" class="nav-item">首页</a>
|
||||||
<a href="/data-collection" class="nav-item active">数据采集</a>
|
<a href="/data-collection" class="nav-item active">数据采集</a>
|
||||||
<a href="/recommendation" class="nav-item">智能推荐</a>
|
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||||
<a href="/analysis" 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>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="data-collection-container">
|
<div class="data-collection-container">
|
||||||
<!-- 用户登录区域 -->
|
|
||||||
<div id="loginSection" class="section">
|
<div id="loginSection" class="section">
|
||||||
<h2>用户登录</h2>
|
<h2>用户登录</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="userId">用户ID:</label>
|
<label for="userId">用户ID:</label>
|
||||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="userName">姓名:</label>
|
<label for="userName">姓名:</label>
|
||||||
<input type="text" id="userName" class="form-input" placeholder="请输入姓名">
|
<input type="text" id="userName" class="form-input" placeholder="请输入姓名">
|
||||||
</div>
|
</div>
|
||||||
<button id="loginBtn" class="btn btn-primary">登录/注册</button>
|
<button id="loginBtn" class="btn btn-primary">登录/注册</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 基础信息问卷 -->
|
|
||||||
<div id="basicQuestionnaire" class="section" style="display: none;">
|
<div id="basicQuestionnaire" class="section" style="display: none;">
|
||||||
<h2>基础信息问卷</h2>
|
<h2>基础信息问卷</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>年龄:</label>
|
<label>年龄:</label>
|
||||||
<input type="number" id="age" class="form-input" placeholder="请输入年龄">
|
<input type="number" id="age" class="form-input" placeholder="请输入年龄">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>性别:</label>
|
<label>性别:</label>
|
||||||
<select id="gender" class="form-input">
|
<select id="gender" class="form-input">
|
||||||
<option value="">请选择</option>
|
<option value="">请选择</option>
|
||||||
<option value="男">男</option>
|
<option value="男">男</option>
|
||||||
<option value="女">女</option>
|
<option value="女">女</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>身高(cm):</label>
|
<label>身高(cm):</label>
|
||||||
<input type="number" id="height" class="form-input" placeholder="请输入身高">
|
<input type="number" id="height" class="form-input" placeholder="请输入身高">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>体重(kg):</label>
|
<label>体重(kg):</label>
|
||||||
<input type="number" id="weight" class="form-input" placeholder="请输入体重">
|
<input type="number" id="weight" class="form-input" placeholder="请输入体重">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>活动水平:</label>
|
<label>活动水平:</label>
|
||||||
<select id="activityLevel" class="form-input">
|
<select id="activityLevel" class="form-input">
|
||||||
<option value="">请选择</option>
|
<option value="">请选择</option>
|
||||||
<option value="久坐">久坐</option>
|
<option value="久坐">久坐</option>
|
||||||
<option value="轻度活动">轻度活动</option>
|
<option value="轻度活动">轻度活动</option>
|
||||||
<option value="中度活动">中度活动</option>
|
<option value="中度活动">中度活动</option>
|
||||||
<option value="高度活动">高度活动</option>
|
<option value="高度活动">高度活动</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="submitBasicBtn" class="btn btn-primary">提交基础信息</button>
|
<button id="submitBasicBtn" class="btn btn-primary">提交基础信息</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 餐食记录 -->
|
|
||||||
<div id="mealRecord" class="section" style="display: none;">
|
<div id="mealRecord" class="section" style="display: none;">
|
||||||
<h2>记录餐食</h2>
|
<h2>记录餐食</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>日期:</label>
|
<label>日期:</label>
|
||||||
<input type="date" id="mealDate" class="form-input">
|
<input type="date" id="mealDate" class="form-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>餐次:</label>
|
<label>餐次:</label>
|
||||||
<select id="mealType" class="form-input">
|
<select id="mealType" class="form-input">
|
||||||
<option value="breakfast">早餐</option>
|
<option value="breakfast">早餐</option>
|
||||||
<option value="lunch">午餐</option>
|
<option value="lunch">午餐</option>
|
||||||
<option value="dinner">晚餐</option>
|
<option value="dinner">晚餐</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>食物列表(每行一个):</label>
|
<label>食物列表(每行一个):</label>
|
||||||
<textarea id="foods" class="form-textarea" rows="5" placeholder="例如: 燕麦粥 1碗 香蕉 1根 牛奶 200ml"></textarea>
|
<textarea id="foods" class="form-textarea" rows="5" placeholder="例如: 燕麦粥 1碗 香蕉 1根 牛奶 200ml"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>热量(大卡):</label>
|
<label>热量(大卡):</label>
|
||||||
<input type="number" id="calories" class="form-input" placeholder="请输入热量">
|
<input type="number" id="calories" class="form-input" placeholder="请输入热量">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>满意度(1-5分):</label>
|
<label>满意度(1-5分):</label>
|
||||||
<input type="number" id="satisfaction" class="form-input" min="1" max="5" value="3">
|
<input type="number" id="satisfaction" class="form-input" min="1" max="5" value="3">
|
||||||
</div>
|
</div>
|
||||||
<button id="submitMealBtn" class="btn btn-primary">记录餐食</button>
|
<button id="submitMealBtn" class="btn btn-primary">记录餐食</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作提示 -->
|
|
||||||
<div id="messageArea" class="message-area"></div>
|
<div id="messageArea" class="message-area"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>© 2024 个性化饮食推荐助手</p>
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/data_collection.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/data_collection.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
id="textInput"
|
id="textInput"
|
||||||
class="text-input"
|
class="text-input"
|
||||||
placeholder="例如: 第一章 西周 夏商学校名称 西周学在官府 国学乡学 六艺 私学兴起的原因与意义 稷下学宫..."
|
placeholder="请粘贴或输入需要背诵的知识点列表 例如: 第一章 西周 夏商学校名称 西周学在官府 国学乡学 六艺..."
|
||||||
rows="10"
|
rows="10"
|
||||||
></textarea>
|
></textarea>
|
||||||
<button id="extractBtn" class="btn btn-primary">识别知识点</button>
|
<button id="extractBtn" class="btn btn-primary">识别知识点</button>
|
||||||
@@ -51,6 +51,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="spinBtn" class="btn btn-spin">转动转盘</button>
|
<button id="spinBtn" class="btn btn-spin">转动转盘</button>
|
||||||
<div id="currentItem" class="current-item"></div>
|
<div id="currentItem" class="current-item"></div>
|
||||||
|
<div id="masteryButtons" class="mastery-buttons" style="display: none;">
|
||||||
|
<button id="masteredBtn" class="btn btn-mastered">✅ 背会了</button>
|
||||||
|
<button id="forgotBtn" class="btn btn-forgot">❌ 忘记了</button>
|
||||||
|
</div>
|
||||||
|
<div id="masteryInfo" class="mastery-info">
|
||||||
|
<span id="remainingCount">剩余 <span id="remainingNum">0</span> 个知识点</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 排序结果显示 -->
|
<!-- 排序结果显示 -->
|
||||||
@@ -62,7 +69,7 @@
|
|||||||
<button id="exportCsvBtn" class="btn btn-export">导出为CSV</button>
|
<button id="exportCsvBtn" class="btn btn-export">导出为CSV</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -3,68 +3,63 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>智能推荐 - 个性化饮食推荐助手</title>
|
<title>智能推荐 - 个性化饮食推荐助手</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/recommendation.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/recommendation.css') }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>? 智能推荐</h1>
|
<h1>🤖 智能推荐</h1>
|
||||||
<p class="subtitle">基于AI的个性化餐食推荐</p>
|
<p class="subtitle">基于AI的个性化餐食推荐</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/" class="nav-item">首页</a>
|
<a href="/" class="nav-item">首页</a>
|
||||||
<a href="/data-collection" class="nav-item">数据采集</a>
|
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||||
<a href="/recommendation" class="nav-item active">智能推荐</a>
|
<a href="/recommendation" class="nav-item active">智能推荐</a>
|
||||||
<a href="/analysis" 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>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
<div class="recommendation-container">
|
<div class="recommendation-container">
|
||||||
<!-- 用户登录区域 -->
|
|
||||||
<div id="loginSection" class="section">
|
<div id="loginSection" class="section">
|
||||||
<h2>用户登录</h2>
|
<h2>用户登录</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="userId">用户ID:</label>
|
<label for="userId">用户ID:</label>
|
||||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||||
</div>
|
</div>
|
||||||
<button id="loginBtn" class="btn btn-primary">登录</button>
|
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 推荐请求区域 -->
|
|
||||||
<div id="requestSection" class="section" style="display: none;">
|
<div id="requestSection" class="section" style="display: none;">
|
||||||
<h2>获取推荐</h2>
|
<h2>获取推荐</h2>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>餐次:</label>
|
<label>餐次:</label>
|
||||||
<select id="mealType" class="form-input">
|
<select id="mealType" class="form-input">
|
||||||
<option value="breakfast">早餐</option>
|
<option value="breakfast">早餐</option>
|
||||||
<option value="lunch">午餐</option>
|
<option value="lunch">午餐</option>
|
||||||
<option value="dinner">晚餐</option>
|
<option value="dinner">晚餐</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button id="getRecommendationBtn" class="btn btn-primary">获取推荐</button>
|
<button id="getRecommendationBtn" class="btn btn-primary">获取推荐</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 推荐结果显示 -->
|
|
||||||
<div id="recommendationsSection" class="section" style="display: none;">
|
<div id="recommendationsSection" class="section" style="display: none;">
|
||||||
<h2>推荐结果</h2>
|
<h2>推荐结果</h2>
|
||||||
<div id="recommendationsList" class="recommendations-list"></div>
|
<div id="recommendationsList" class="recommendations-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作提示 -->
|
|
||||||
<div id="messageArea" class="message-area"></div>
|
<div id="messageArea" class="message-area"></div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<p>© 2024 个性化饮食推荐助手</p>
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/recommendation.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/recommendation.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
161
web_app.py
161
web_app.py
@@ -18,14 +18,25 @@ from modules.ai_analysis import AIAnalysisModule
|
|||||||
from modules.recommendation_engine import RecommendationEngine
|
from modules.recommendation_engine import RecommendationEngine
|
||||||
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
|
from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志 - 确保UTF-8编码
|
||||||
|
import sys
|
||||||
|
# 设置标准输出编码为UTF-8
|
||||||
|
if sys.stdout.encoding != 'utf-8':
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
if sys.stderr.encoding != 'utf-8':
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
# 创建logs目录
|
||||||
|
Path('logs').mkdir(exist_ok=True)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
handlers=[
|
handlers=[
|
||||||
logging.FileHandler('logs/web_app.log', encoding='utf-8'),
|
logging.FileHandler('logs/web_app.log', encoding='utf-8'),
|
||||||
logging.StreamHandler()
|
logging.StreamHandler(sys.stdout)
|
||||||
]
|
],
|
||||||
|
force=True # 强制重新配置
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -35,6 +46,9 @@ app.config['SECRET_KEY'] = 'your-secret-key-here'
|
|||||||
# 确保模板文件使用UTF-8编码读取
|
# 确保模板文件使用UTF-8编码读取
|
||||||
app.jinja_env.auto_reload = True
|
app.jinja_env.auto_reload = True
|
||||||
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||||
|
# 设置Jinja2模板加载器使用UTF-8编码
|
||||||
|
from jinja2 import FileSystemLoader
|
||||||
|
app.jinja_loader = FileSystemLoader('templates', encoding='utf-8')
|
||||||
|
|
||||||
# 确保所有响应使用UTF-8编码
|
# 确保所有响应使用UTF-8编码
|
||||||
@app.after_request
|
@app.after_request
|
||||||
@@ -156,31 +170,31 @@ sorter = RecitationSorter()
|
|||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
"""首页"""
|
"""首页"""
|
||||||
return render_template('index.html', encoding='utf-8')
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/recitation')
|
@app.route('/recitation')
|
||||||
def recitation():
|
def recitation():
|
||||||
"""背诵排序页面"""
|
"""背诵排序页面"""
|
||||||
return render_template('recitation.html', encoding='utf-8')
|
return render_template('recitation.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/data-collection')
|
@app.route('/data-collection')
|
||||||
def data_collection():
|
def data_collection():
|
||||||
"""数据采集页面"""
|
"""数据采集页面"""
|
||||||
return render_template('data_collection.html', encoding='utf-8')
|
return render_template('data_collection.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/recommendation')
|
@app.route('/recommendation')
|
||||||
def recommendation():
|
def recommendation():
|
||||||
"""推荐页面"""
|
"""推荐页面"""
|
||||||
return render_template('recommendation.html', encoding='utf-8')
|
return render_template('recommendation.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/analysis')
|
@app.route('/analysis')
|
||||||
def analysis():
|
def analysis():
|
||||||
"""分析页面"""
|
"""分析页面"""
|
||||||
return render_template('analysis.html', encoding='utf-8')
|
return render_template('analysis.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/extract', methods=['POST'])
|
@app.route('/api/extract', methods=['POST'])
|
||||||
@@ -295,11 +309,36 @@ def export_sorted():
|
|||||||
filename = f'背诵排序结果_{datetime.now().strftime("%Y%m%d_%H%M%S")}.txt'
|
filename = f'背诵排序结果_{datetime.now().strftime("%Y%m%d_%H%M%S")}.txt'
|
||||||
mimetype = 'text/plain; charset=utf-8'
|
mimetype = 'text/plain; charset=utf-8'
|
||||||
|
|
||||||
|
# 修复文件名编码问题:HTTP头必须使用latin-1编码
|
||||||
|
# 使用RFC 5987标准编码中文文件名
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
# ASCII fallback文件名(确保兼容性)
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
fallback_filename = f'recitation_sorted_{timestamp}.{export_format}'
|
||||||
|
|
||||||
|
# RFC 5987编码:对UTF-8字节序列进行百分号编码
|
||||||
|
# 格式: filename="fallback"; filename*=UTF-8''encoded
|
||||||
|
# 将所有字节都进行百分号编码,确保HTTP头的latin-1兼容性
|
||||||
|
try:
|
||||||
|
utf8_bytes = filename.encode('utf-8')
|
||||||
|
# 对所有字节进行百分号编码(大写十六进制)
|
||||||
|
encoded_filename = ''.join([f'%{b:02X}' for b in utf8_bytes])
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"文件名编码失败,使用fallback: {e}")
|
||||||
|
encoded_filename = fallback_filename
|
||||||
|
|
||||||
|
# 构建Content-Disposition头:同时提供fallback和UTF-8编码版本
|
||||||
|
content_disposition = (
|
||||||
|
f'attachment; filename="{fallback_filename}"; '
|
||||||
|
f"filename*=UTF-8''{encoded_filename}"
|
||||||
|
)
|
||||||
|
|
||||||
response = Response(
|
response = Response(
|
||||||
content.encode('utf-8'),
|
content.encode('utf-8'),
|
||||||
mimetype=mimetype,
|
mimetype=mimetype,
|
||||||
headers={
|
headers={
|
||||||
'Content-Disposition': f'attachment; filename="{filename}"'
|
'Content-Disposition': content_disposition
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -370,19 +409,62 @@ def user_register():
|
|||||||
core = get_app_core()
|
core = get_app_core()
|
||||||
user_data = core.get_user_data(user_id)
|
user_data = core.get_user_data(user_id)
|
||||||
|
|
||||||
# 更新用户基本信息
|
# 如果用户不存在,创建新用户
|
||||||
user_data.profile['name'] = name
|
if not user_data:
|
||||||
core.data_manager.save_user_data(user_data)
|
from core.base import UserData
|
||||||
|
# 直接创建UserData对象并保存
|
||||||
|
initial_profile = {
|
||||||
|
'name': name,
|
||||||
|
'age': 25,
|
||||||
|
'gender': '未知',
|
||||||
|
'height': 170,
|
||||||
|
'weight': 60,
|
||||||
|
'activity_level': 'moderate'
|
||||||
|
}
|
||||||
|
user_data = UserData(
|
||||||
|
user_id=user_id,
|
||||||
|
profile=initial_profile,
|
||||||
|
preferences={}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 保存用户数据
|
||||||
|
if not core.data_manager.save_user_data(user_data):
|
||||||
|
logger.error(f"保存用户数据失败: user_id={user_id}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '创建用户失败:数据保存失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 验证保存是否成功(重新获取一次)
|
||||||
|
saved_user_data = core.get_user_data(user_id)
|
||||||
|
if not saved_user_data:
|
||||||
|
logger.error(f"用户数据保存后无法获取: user_id={user_id}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '创建用户失败:数据验证失败'
|
||||||
|
}), 500
|
||||||
|
user_data = saved_user_data
|
||||||
|
else:
|
||||||
|
# 用户已存在,更新姓名
|
||||||
|
user_data.profile['name'] = name
|
||||||
|
if not core.data_manager.save_user_data(user_data):
|
||||||
|
logger.error(f"更新用户数据失败: user_id={user_id}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '更新用户信息失败'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
# 设置会话
|
||||||
session['user_id'] = user_id
|
session['user_id'] = user_id
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'user_id': user_id,
|
'user_id': user_id,
|
||||||
'name': name
|
'name': user_data.profile.get('name', name)
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"用户注册失败: {e}")
|
logger.error(f"用户注册失败: {e}", exc_info=True)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'注册失败: {str(e)}'
|
'message': f'注册失败: {str(e)}'
|
||||||
@@ -400,11 +482,19 @@ def submit_questionnaire():
|
|||||||
'message': '请先登录'
|
'message': '请先登录'
|
||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
# 确保用户存在
|
||||||
|
user_data = core.get_user_data(user_id)
|
||||||
|
if not user_data:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户数据不存在,请先注册'
|
||||||
|
}), 404
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
questionnaire_type = data.get('type', 'basic') # basic, taste, physiological
|
questionnaire_type = data.get('type', 'basic') # basic, taste, physiological
|
||||||
answers = data.get('answers', {})
|
answers = data.get('answers', {})
|
||||||
|
|
||||||
core = get_app_core()
|
|
||||||
input_data = {
|
input_data = {
|
||||||
'type': 'questionnaire',
|
'type': 'questionnaire',
|
||||||
'questionnaire_type': questionnaire_type,
|
'questionnaire_type': questionnaire_type,
|
||||||
@@ -425,7 +515,7 @@ def submit_questionnaire():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"提交问卷失败: {e}")
|
logger.error(f"提交问卷失败: {e}", exc_info=True)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'提交失败: {str(e)}'
|
'message': f'提交失败: {str(e)}'
|
||||||
@@ -443,6 +533,15 @@ def record_meal():
|
|||||||
'message': '请先登录'
|
'message': '请先登录'
|
||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
# 确保用户存在
|
||||||
|
user_data = core.get_user_data(user_id)
|
||||||
|
if not user_data:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户数据不存在,请先注册'
|
||||||
|
}), 404
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
meal_data = {
|
meal_data = {
|
||||||
'date': data.get('date', datetime.now().strftime('%Y-%m-%d')),
|
'date': data.get('date', datetime.now().strftime('%Y-%m-%d')),
|
||||||
@@ -454,7 +553,6 @@ def record_meal():
|
|||||||
'notes': data.get('notes', '')
|
'notes': data.get('notes', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
core = get_app_core()
|
|
||||||
input_data = {
|
input_data = {
|
||||||
'type': 'meal_record',
|
'type': 'meal_record',
|
||||||
**meal_data
|
**meal_data
|
||||||
@@ -474,7 +572,7 @@ def record_meal():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"记录餐食失败: {e}")
|
logger.error(f"记录餐食失败: {e}", exc_info=True)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'记录失败: {str(e)}'
|
'message': f'记录失败: {str(e)}'
|
||||||
@@ -492,12 +590,21 @@ def get_recommendations():
|
|||||||
'message': '请先登录'
|
'message': '请先登录'
|
||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
# 确保用户存在
|
||||||
|
user_data = core.get_user_data(user_id)
|
||||||
|
if not user_data:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户数据不存在,请先注册',
|
||||||
|
'recommendations': []
|
||||||
|
}), 404
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
meal_type = data.get('meal_type', 'lunch')
|
meal_type = data.get('meal_type', 'lunch')
|
||||||
preferences = data.get('preferences', {})
|
preferences = data.get('preferences', {})
|
||||||
context = data.get('context', {})
|
context = data.get('context', {})
|
||||||
|
|
||||||
core = get_app_core()
|
|
||||||
input_data = {
|
input_data = {
|
||||||
'type': 'meal_recommendation',
|
'type': 'meal_recommendation',
|
||||||
'meal_type': meal_type,
|
'meal_type': meal_type,
|
||||||
@@ -520,7 +627,7 @@ def get_recommendations():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取推荐失败: {e}")
|
logger.error(f"获取推荐失败: {e}", exc_info=True)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'获取失败: {str(e)}',
|
'message': f'获取失败: {str(e)}',
|
||||||
@@ -539,10 +646,18 @@ def analyze_nutrition():
|
|||||||
'message': '请先登录'
|
'message': '请先登录'
|
||||||
}), 401
|
}), 401
|
||||||
|
|
||||||
|
core = get_app_core()
|
||||||
|
# 确保用户存在
|
||||||
|
user_data = core.get_user_data(user_id)
|
||||||
|
if not user_data:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '用户数据不存在,请先注册'
|
||||||
|
}), 404
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
meal_data = data.get('meal_data', {})
|
meal_data = data.get('meal_data', {})
|
||||||
|
|
||||||
core = get_app_core()
|
|
||||||
input_data = {
|
input_data = {
|
||||||
'type': 'nutrition_analysis',
|
'type': 'nutrition_analysis',
|
||||||
'meal_data': meal_data
|
'meal_data': meal_data
|
||||||
@@ -562,7 +677,7 @@ def analyze_nutrition():
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"营养分析失败: {e}")
|
logger.error(f"营养分析失败: {e}", exc_info=True)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'分析失败: {str(e)}'
|
'message': f'分析失败: {str(e)}'
|
||||||
@@ -582,4 +697,4 @@ if __name__ == '__main__':
|
|||||||
Path('logs').mkdir(exist_ok=True)
|
Path('logs').mkdir(exist_ok=True)
|
||||||
|
|
||||||
# 启动应用
|
# 启动应用
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=7400)
|
||||||
|
|||||||
Reference in New Issue
Block a user