feat: add mastery feature to recitation wheel
This commit is contained in:
20
README.md
20
README.md
@@ -55,12 +55,17 @@
|
||||
- **数据管理器**:统一的数据访问接口
|
||||
|
||||
### OCR识别技术
|
||||
- **Tesseract OCR**:开源OCR引擎,支持中英文
|
||||
- **PaddleOCR**:百度开源OCR,中文识别优秀
|
||||
- **EasyOCR**:简单易用的多语言OCR库
|
||||
- **Tesseract OCR**:开源OCR引擎,支持中英文(**默认,轻量级,推荐**)
|
||||
- **PaddleOCR**:百度开源OCR,中文识别优秀(可选,需要PaddlePaddle,占用内存较大)
|
||||
- **EasyOCR**:简单易用的多语言OCR库(可选,需要PyTorch,占用内存很大)
|
||||
- **OpenCV**:图像预处理和增强
|
||||
- **PIL/Pillow**:图像处理和格式转换
|
||||
|
||||
**注意**:默认配置仅使用Tesseract OCR(轻量级,无需深度学习框架)。如需使用PaddleOCR或EasyOCR:
|
||||
1. 取消注释`requirements.txt`中对应依赖
|
||||
2. 安装依赖:`pip install paddleocr` 或 `pip install easyocr`
|
||||
3. 在OCR模块配置中添加对应引擎到`ocr_methods`列表
|
||||
|
||||
### 机器学习
|
||||
- **scikit-learn**:推荐算法实现
|
||||
- **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`
|
||||
- **Linux**: `sudo apt-get install tesseract-ocr`
|
||||
|
||||
#### 其他OCR引擎
|
||||
#### 其他OCR引擎(可选,需要深度学习框架)
|
||||
```bash
|
||||
# PaddleOCR(推荐,中文识别效果好)
|
||||
# PaddleOCR(可选,需要PaddlePaddle,占用内存较大)
|
||||
pip install paddleocr
|
||||
|
||||
# EasyOCR(简单易用)
|
||||
# EasyOCR(可选,需要PyTorch,占用内存很大,通常需要1-2GB)
|
||||
pip install easyocr
|
||||
|
||||
# 注意:安装后需要在OCR模块配置中添加对应引擎:
|
||||
# self.ocr_methods = ['tesseract', 'paddleocr'] # 添加需要的引擎
|
||||
```
|
||||
|
||||
### 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:
|
||||
"""OCR识别配置"""
|
||||
# OCR引擎配置
|
||||
enable_tesseract: bool = True
|
||||
enable_paddleocr: bool = True
|
||||
enable_easyocr: bool = True
|
||||
# 默认只启用轻量级Tesseract,其他引擎需要额外安装深度学习框架
|
||||
enable_tesseract: bool = True # 轻量级,推荐使用
|
||||
enable_paddleocr: bool = False # 可选,需要PaddlePaddle,占用内存较大
|
||||
enable_easyocr: bool = False # 可选,需要PyTorch,占用内存很大(1-2GB)
|
||||
|
||||
# 识别参数
|
||||
min_confidence: float = 0.6
|
||||
|
||||
@@ -60,8 +60,10 @@ class OCRCalorieRecognitionModule(BaseModule):
|
||||
def __init__(self, config: BaseConfig):
|
||||
super().__init__(config, ModuleType.DATA_COLLECTION)
|
||||
|
||||
# OCR配置
|
||||
self.ocr_methods = ['tesseract', 'paddleocr', 'easyocr']
|
||||
# OCR配置 - 优先使用轻量级OCR引擎,避免内存占用过大
|
||||
# 默认只使用tesseract(轻量级),其他OCR引擎需要手动安装且作为可选依赖
|
||||
self.ocr_methods = ['tesseract'] # 轻量级默认配置
|
||||
# 可选添加其他OCR引擎:'paddleocr', 'easyocr' (需要安装对应依赖)
|
||||
self.min_confidence = 0.6
|
||||
self.max_processing_time = 30.0
|
||||
|
||||
@@ -278,9 +280,11 @@ class OCRCalorieRecognitionModule(BaseModule):
|
||||
return None
|
||||
|
||||
def _paddleocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
|
||||
"""使用PaddleOCR进行识别"""
|
||||
"""使用PaddleOCR进行识别(可选依赖,需要PaddlePaddle,占用内存较大)"""
|
||||
try:
|
||||
# 这里需要安装paddleocr: pip install paddleocr
|
||||
# 注意:PaddleOCR需要安装paddleocr和PaddlePaddle,占用内存较大
|
||||
# 如需使用,请手动安装: pip install paddleocr
|
||||
# 然后需要在OCR方法列表中添加'paddleocr'
|
||||
from paddleocr import PaddleOCR
|
||||
|
||||
if 'paddleocr' not in self.ocr_engines:
|
||||
@@ -327,9 +331,11 @@ class OCRCalorieRecognitionModule(BaseModule):
|
||||
return None
|
||||
|
||||
def _easyocr_recognize(self, image: np.ndarray) -> Optional[OCRResult]:
|
||||
"""使用EasyOCR进行识别"""
|
||||
"""使用EasyOCR进行识别(可选依赖,需要PyTorch,占用内存很大)"""
|
||||
try:
|
||||
# 这里需要安装easyocr: pip install easyocr
|
||||
# 注意:EasyOCR需要安装easyocr和PyTorch,占用内存很大(通常需要1-2GB)
|
||||
# 如需使用,请手动安装: pip install easyocr
|
||||
# 然后需要在OCR方法列表中添加'easyocr'
|
||||
import easyocr
|
||||
|
||||
if 'easyocr' not in self.ocr_engines:
|
||||
|
||||
@@ -19,14 +19,18 @@ python-dateutil>=2.8.0
|
||||
# 图像处理 (GUI需要)
|
||||
Pillow>=10.0.0
|
||||
|
||||
# OCR识别依赖
|
||||
# OCR识别依赖(必需,轻量级)
|
||||
pytesseract>=0.3.10
|
||||
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
|
||||
kivymd>=1.1.1
|
||||
# 缃戦〉绔敮鎸乣nFlask>=3.0.0
|
||||
|
||||
# Web端支持 (必需)
|
||||
Flask>=3.0.0
|
||||
Werkzeug>=3.0.0
|
||||
|
||||
@@ -21,9 +21,6 @@ def check_flask():
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""启动网页应用"""
|
||||
print("🌐 启动网页应用...")
|
||||
print("=" * 50)
|
||||
|
||||
if not check_flask():
|
||||
return False
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* 转盘样式 */
|
||||
/* ת<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ */
|
||||
.wheel-container {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
@@ -147,7 +147,75 @@
|
||||
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 {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
@@ -189,9 +257,30 @@
|
||||
.sorted-item-text {
|
||||
flex: 1;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -219,7 +308,7 @@
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
/* <EFBFBD><EFBFBD>Ӧʽ<EFBFBD><EFBFBD><EFBFBD> */
|
||||
@media (max-width: 768px) {
|
||||
.wheel-container {
|
||||
width: 300px;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// 背诵排序功能脚本
|
||||
// 背诵排序功能脚本
|
||||
|
||||
let extractedItems = [];
|
||||
let sortedItems = [];
|
||||
let sortedItems = []; // 原始排序后的所有项目
|
||||
let availableItems = []; // 当前可用于转盘的项目(排除已掌握的)
|
||||
let masteredItems = []; // 已掌握的项目列表
|
||||
let currentSpinIndex = 0;
|
||||
let currentSelectedItem = null; // 当前转盘选中的项目
|
||||
let isSpinning = false;
|
||||
|
||||
// 颜色配置 - 转盘使用不同颜色
|
||||
// 颜色配置 - 转盘使用不同颜色
|
||||
const colors = [
|
||||
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
||||
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
||||
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
||||
];
|
||||
|
||||
// DOM元素
|
||||
// DOM元素
|
||||
const textInput = document.getElementById('textInput');
|
||||
const extractBtn = document.getElementById('extractBtn');
|
||||
const extractedSection = document.getElementById('extractedSection');
|
||||
@@ -29,18 +32,23 @@ const resetBtn = document.getElementById('resetBtn');
|
||||
const exportTxtBtn = document.getElementById('exportTxtBtn');
|
||||
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
||||
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_SORTED = 'recitation_sorted_items';
|
||||
const STORAGE_KEY_ORIGINAL_TEXT = 'recitation_original_text';
|
||||
const STORAGE_KEY_MASTERED = 'recitation_mastered_items';
|
||||
|
||||
// 页面加载时恢复数据
|
||||
// 页面加载时恢复数据
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
restoreFromStorage();
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
// 保存到本地存储
|
||||
function saveToStorage() {
|
||||
try {
|
||||
if (extractedItems.length > 0) {
|
||||
@@ -52,17 +60,21 @@ function saveToStorage() {
|
||||
if (textInput.value.trim()) {
|
||||
localStorage.setItem(STORAGE_KEY_ORIGINAL_TEXT, textInput.value);
|
||||
}
|
||||
if (masteredItems.length > 0) {
|
||||
localStorage.setItem(STORAGE_KEY_MASTERED, JSON.stringify(masteredItems));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存到本地存储失败:', e);
|
||||
console.error('保存到本地存储失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地存储恢复
|
||||
// 从本地存储恢复
|
||||
function restoreFromStorage() {
|
||||
try {
|
||||
const savedExtracted = localStorage.getItem(STORAGE_KEY_EXTRACTED);
|
||||
const savedSorted = localStorage.getItem(STORAGE_KEY_SORTED);
|
||||
const savedText = localStorage.getItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||
const savedMastered = localStorage.getItem(STORAGE_KEY_MASTERED);
|
||||
|
||||
if (savedText) {
|
||||
textInput.value = savedText;
|
||||
@@ -75,36 +87,54 @@ function restoreFromStorage() {
|
||||
textInput.disabled = true;
|
||||
}
|
||||
|
||||
if (savedMastered) {
|
||||
masteredItems = JSON.parse(savedMastered);
|
||||
}
|
||||
|
||||
if (savedSorted) {
|
||||
sortedItems = JSON.parse(savedSorted);
|
||||
updateAvailableItems(); // 更新可用项目列表
|
||||
displaySortedItems(sortedItems);
|
||||
createWheel(sortedItems);
|
||||
createWheel(availableItems);
|
||||
wheelSection.style.display = 'block';
|
||||
resultSection.style.display = 'block';
|
||||
updateMasteryInfo();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('从本地存储恢复失败:', e);
|
||||
console.error('从本地存储恢复失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
// 清除本地存储
|
||||
function clearStorage() {
|
||||
localStorage.removeItem(STORAGE_KEY_EXTRACTED);
|
||||
localStorage.removeItem(STORAGE_KEY_SORTED);
|
||||
localStorage.removeItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||
localStorage.removeItem(STORAGE_KEY_MASTERED);
|
||||
}
|
||||
|
||||
// 提取知识点
|
||||
// 更新可用项目列表(排除已掌握的)
|
||||
function updateAvailableItems() {
|
||||
availableItems = sortedItems.filter(item => !masteredItems.includes(item));
|
||||
updateMasteryInfo();
|
||||
}
|
||||
|
||||
// 更新掌握情况信息
|
||||
function updateMasteryInfo() {
|
||||
remainingNum.textContent = availableItems.length;
|
||||
}
|
||||
|
||||
// 提取知识点
|
||||
extractBtn.addEventListener('click', async () => {
|
||||
const text = textInput.value.trim();
|
||||
|
||||
if (!text) {
|
||||
alert('请输入要处理的文本');
|
||||
alert('请输入需要识别的文本');
|
||||
return;
|
||||
}
|
||||
|
||||
extractBtn.disabled = true;
|
||||
extractBtn.textContent = '识别中...';
|
||||
extractBtn.textContent = '识别中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/extract', {
|
||||
@@ -122,20 +152,20 @@ extractBtn.addEventListener('click', async () => {
|
||||
displayExtractedItems(extractedItems);
|
||||
extractedSection.style.display = 'block';
|
||||
textInput.disabled = true;
|
||||
saveToStorage(); // 保存到本地存储
|
||||
saveToStorage(); // 保存到本地存储
|
||||
} else {
|
||||
alert(data.message || '提取失败');
|
||||
alert(data.message || '提取失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提取失败:', error);
|
||||
alert('提取失败,请检查网络连接');
|
||||
console.error('提取失败:', error);
|
||||
alert('提取失败,请检查网络连接');
|
||||
} finally {
|
||||
extractBtn.disabled = false;
|
||||
extractBtn.textContent = '识别知识点';
|
||||
extractBtn.textContent = '识别知识点';
|
||||
}
|
||||
});
|
||||
|
||||
// 显示提取的项目
|
||||
// 显示提取到的项目
|
||||
function displayExtractedItems(items) {
|
||||
itemCount.textContent = items.length;
|
||||
itemsList.innerHTML = '';
|
||||
@@ -148,15 +178,15 @@ function displayExtractedItems(items) {
|
||||
});
|
||||
}
|
||||
|
||||
// 随机排序
|
||||
// 开始排序
|
||||
sortBtn.addEventListener('click', async () => {
|
||||
if (extractedItems.length === 0) {
|
||||
alert('请先提取知识点');
|
||||
alert('请先提取知识点');
|
||||
return;
|
||||
}
|
||||
|
||||
sortBtn.disabled = true;
|
||||
sortBtn.textContent = '排序中...';
|
||||
sortBtn.textContent = '排序中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sort', {
|
||||
@@ -171,36 +201,45 @@ sortBtn.addEventListener('click', async () => {
|
||||
|
||||
if (data.success) {
|
||||
sortedItems = data.items;
|
||||
masteredItems = []; // 重新排序时重置已掌握列表
|
||||
updateAvailableItems(); // 更新可用项目
|
||||
displaySortedItems(sortedItems);
|
||||
createWheel(sortedItems);
|
||||
createWheel(availableItems);
|
||||
wheelSection.style.display = 'block';
|
||||
resultSection.style.display = 'block';
|
||||
currentSpinIndex = 0;
|
||||
saveToStorage(); // 保存到本地存储
|
||||
currentSelectedItem = null;
|
||||
masteryButtons.style.display = 'none';
|
||||
saveToStorage(); // 保存到本地存储
|
||||
} else {
|
||||
alert(data.message || '排序失败');
|
||||
alert(data.message || '排序失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('排序失败:', error);
|
||||
alert('排序失败,请检查网络连接');
|
||||
console.error('排序失败:', error);
|
||||
alert('排序失败,请检查网络连接');
|
||||
} finally {
|
||||
sortBtn.disabled = false;
|
||||
sortBtn.textContent = '开始随机排序';
|
||||
sortBtn.textContent = '开始随机排序';
|
||||
}
|
||||
});
|
||||
|
||||
// 创建转盘 - 使用SVG实现更真实的转盘效果
|
||||
// 创建转盘 - 使用SVG实现真实转盘效果
|
||||
function createWheel(items) {
|
||||
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 radius = 190; // 转盘半径(考虑边框)
|
||||
const radius = 190; // 转盘半径,考虑边框
|
||||
const centerX = 200;
|
||||
const centerY = 200;
|
||||
|
||||
// 创建SVG转盘
|
||||
// 创建SVG转盘
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', '400');
|
||||
svg.setAttribute('height', '400');
|
||||
@@ -210,6 +249,7 @@ function createWheel(items) {
|
||||
svg.style.left = '0';
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = '100%';
|
||||
svg.style.transition = 'transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99)';
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const startAngle = (index * anglePerItem - 90) * Math.PI / 180;
|
||||
@@ -238,7 +278,7 @@ function createWheel(items) {
|
||||
|
||||
svg.appendChild(path);
|
||||
|
||||
// 添加文本
|
||||
// 添加文本
|
||||
const midAngle = (startAngle + endAngle) / 2;
|
||||
const textRadius = radius * 0.7;
|
||||
const textX = centerX + textRadius * Math.cos(midAngle);
|
||||
@@ -262,41 +302,47 @@ function createWheel(items) {
|
||||
wheel.appendChild(svg);
|
||||
}
|
||||
|
||||
// 转动转盘
|
||||
// 转动转盘
|
||||
spinBtn.addEventListener('click', () => {
|
||||
if (isSpinning || sortedItems.length === 0) return;
|
||||
if (isSpinning || availableItems.length === 0) return;
|
||||
|
||||
isSpinning = true;
|
||||
spinBtn.disabled = true;
|
||||
currentItem.textContent = '转盘中...';
|
||||
currentItem.textContent = '转盘中...';
|
||||
masteryButtons.style.display = 'none'; // 隐藏按钮,等待转盘停止
|
||||
currentSelectedItem = null;
|
||||
|
||||
// 随机选择一个索引(添加多圈旋转效果)
|
||||
const randomIndex = Math.floor(Math.random() * sortedItems.length);
|
||||
const spins = 3; // 转3圈
|
||||
const anglePerItem = 360 / sortedItems.length;
|
||||
// 计算目标角度:多转几圈 + 指向选中项
|
||||
// 随机选择一个项目,并增加多圈旋转效果
|
||||
const randomIndex = Math.floor(Math.random() * availableItems.length);
|
||||
const spins = 3; // 转3圈
|
||||
const anglePerItem = 360 / availableItems.length;
|
||||
// 计算目标角度:转多圈 + 指向选中项
|
||||
const targetAngle = spins * 360 + (360 - (randomIndex * anglePerItem) - anglePerItem / 2);
|
||||
|
||||
// 获取当前角度
|
||||
// 获取当前角度
|
||||
const svg = wheel.querySelector('svg');
|
||||
if (!svg) return;
|
||||
const currentAngle = getCurrentRotation(svg);
|
||||
|
||||
// 计算总旋转角度(考虑当前角度)
|
||||
// 计算总旋转角度(考虑当前角度)
|
||||
const totalRotation = currentAngle + targetAngle;
|
||||
|
||||
svg.style.transform = `rotate(${totalRotation}deg)`;
|
||||
|
||||
// 转盘停止后显示结果
|
||||
// 转盘停止后显示结果
|
||||
setTimeout(() => {
|
||||
currentItem.textContent = `${randomIndex + 1}. ${sortedItems[randomIndex]}`;
|
||||
currentSelectedItem = availableItems[randomIndex];
|
||||
currentItem.textContent = `${randomIndex + 1}. ${currentSelectedItem}`;
|
||||
currentSpinIndex = randomIndex;
|
||||
isSpinning = false;
|
||||
spinBtn.disabled = false;
|
||||
masteryButtons.style.display = 'flex'; // 显示掌握按钮
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// 获取当前旋转角度
|
||||
// 获取当前旋转角度
|
||||
function getCurrentRotation(element) {
|
||||
if (!element) return 0;
|
||||
const style = window.getComputedStyle(element);
|
||||
const transform = style.transform;
|
||||
if (transform === 'none') return 0;
|
||||
@@ -306,7 +352,51 @@ function getCurrentRotation(element) {
|
||||
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) {
|
||||
sortedList.innerHTML = '';
|
||||
|
||||
@@ -314,6 +404,11 @@ function displaySortedItems(items) {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.className = 'sorted-item';
|
||||
|
||||
// 如果已掌握,添加特殊样式
|
||||
if (masteredItems.includes(item)) {
|
||||
itemDiv.classList.add('sorted-item-mastered');
|
||||
}
|
||||
|
||||
const numberSpan = document.createElement('span');
|
||||
numberSpan.className = 'sorted-item-number';
|
||||
numberSpan.textContent = index + 1;
|
||||
@@ -322,20 +417,30 @@ function displaySortedItems(items) {
|
||||
textSpan.className = 'sorted-item-text';
|
||||
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(textSpan);
|
||||
sortedList.appendChild(itemDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// 导出功能
|
||||
// 导出功能
|
||||
exportTxtBtn.addEventListener('click', () => exportData('txt'));
|
||||
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
||||
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
||||
|
||||
function exportData(format) {
|
||||
if (sortedItems.length === 0) {
|
||||
alert('没有可导出的数据');
|
||||
// 导出时只导出当前可用的项目(未掌握的)
|
||||
const itemsToExport = availableItems.length > 0 ? availableItems : sortedItems;
|
||||
|
||||
if (itemsToExport.length === 0) {
|
||||
alert('没有可导出的数据');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,13 +450,13 @@ function exportData(format) {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: sortedItems,
|
||||
items: itemsToExport,
|
||||
format: format
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('导出失败');
|
||||
throw new Error('导出失败');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
@@ -360,23 +465,26 @@ function exportData(format) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
|
||||
a.download = `背诵排序结果_${timestamp}.${format}`;
|
||||
a.download = `背诵排序结果_${timestamp}.${format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('导出失败:', error);
|
||||
alert('导出失败,请重试');
|
||||
console.error('导出失败:', error);
|
||||
alert('导出失败,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 重置
|
||||
// 重置
|
||||
resetBtn.addEventListener('click', () => {
|
||||
extractedItems = [];
|
||||
sortedItems = [];
|
||||
availableItems = [];
|
||||
masteredItems = [];
|
||||
currentSpinIndex = 0;
|
||||
currentSelectedItem = null;
|
||||
|
||||
textInput.value = '';
|
||||
textInput.disabled = false;
|
||||
@@ -391,10 +499,11 @@ resetBtn.addEventListener('click', () => {
|
||||
svg.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
currentItem.textContent = '';
|
||||
masteryButtons.style.display = 'none';
|
||||
|
||||
isSpinning = false;
|
||||
spinBtn.disabled = false;
|
||||
updateMasteryInfo();
|
||||
|
||||
clearStorage(); // 清除本地存储
|
||||
clearStorage(); // 清除本地存储
|
||||
});
|
||||
|
||||
|
||||
@@ -3,64 +3,59 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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/analysis.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>? 营养分析</h1>
|
||||
<p class="subtitle">AI智能营养分析与建议</p>
|
||||
<h1>? Ӫ</h1>
|
||||
<p class="subtitle">AIӪ뽨</p>
|
||||
</header>
|
||||
|
||||
<nav class="nav">
|
||||
<a href="/" class="nav-item">首页</a>
|
||||
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||
<a href="/analysis" class="nav-item active">营养分析</a>
|
||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||
<a href="/" class="nav-item">ҳ</a>
|
||||
<a href="/data-collection" class="nav-item">ݲɼ</a>
|
||||
<a href="/recommendation" class="nav-item">Ƽ</a>
|
||||
<a href="/analysis" class="nav-item active">Ӫ</a>
|
||||
<a href="/recitation" class="nav-item"></a>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="analysis-container">
|
||||
<!-- 用户登录区域 -->
|
||||
<div id="loginSection" class="section">
|
||||
<h2>用户登录</h2>
|
||||
<h2>û¼</h2>
|
||||
<div class="form-group">
|
||||
<label for="userId">用户ID:</label>
|
||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||
<label for="userId">ûID</label>
|
||||
<input type="text" id="userId" class="form-input" placeholder="ûID">
|
||||
</div>
|
||||
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||
<button id="loginBtn" class="btn btn-primary">¼</button>
|
||||
</div>
|
||||
|
||||
<!-- 分析请求区域 -->
|
||||
<div id="requestSection" class="section" style="display: none;">
|
||||
<h2>营养分析</h2>
|
||||
<h2>Ӫ</h2>
|
||||
<div class="form-group">
|
||||
<label>餐食信息:</label>
|
||||
<textarea id="mealData" class="form-textarea" rows="5" placeholder="请输入餐食信息,例如: 早餐:燕麦粥、香蕉、牛奶 热量:350大卡"></textarea>
|
||||
<label>ʳϢ</label>
|
||||
<textarea id="mealData" class="form-textarea" rows="5" placeholder="ʳϢ磺 ͣࡢ㽶ţ 350"></textarea>
|
||||
</div>
|
||||
<button id="analyzeBtn" class="btn btn-primary">开始分析</button>
|
||||
<button id="analyzeBtn" class="btn btn-primary">ʼ</button>
|
||||
</div>
|
||||
|
||||
<!-- 分析结果显示 -->
|
||||
<div id="analysisSection" class="section" style="display: none;">
|
||||
<h2>分析结果</h2>
|
||||
<h2></h2>
|
||||
<div id="analysisResult" class="analysis-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- 操作提示 -->
|
||||
<div id="messageArea" class="message-area"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2024 个性化饮食推荐助手</p>
|
||||
<p>© 2024 ԻʳƼ</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/analysis.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -3,118 +3,106 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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/data_collection.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>? 数据采集</h1>
|
||||
<p class="subtitle">建立你的个人饮食档案</p>
|
||||
<h1>📝 数据采集</h1>
|
||||
<p class="subtitle">建立你的个人饮食档案</p>
|
||||
</header>
|
||||
|
||||
<nav class="nav">
|
||||
<a href="/" class="nav-item">首页</a>
|
||||
<a href="/data-collection" class="nav-item active">数据采集</a>
|
||||
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||
<a href="/analysis" class="nav-item">营养分析</a>
|
||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||
<a href="/" class="nav-item">首页</a>
|
||||
<a href="/data-collection" class="nav-item active">数据采集</a>
|
||||
<a href="/recommendation" class="nav-item">智能推荐</a>
|
||||
<a href="/analysis" class="nav-item">营养分析</a>
|
||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="data-collection-container">
|
||||
<!-- 用户登录区域 -->
|
||||
<div id="loginSection" class="section">
|
||||
<h2>用户登录</h2>
|
||||
<h2>用户登录</h2>
|
||||
<div class="form-group">
|
||||
<label for="userId">用户ID:</label>
|
||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||
<label for="userId">用户ID:</label>
|
||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="userName">姓名:</label>
|
||||
<input type="text" id="userName" class="form-input" placeholder="请输入姓名">
|
||||
<label for="userName">姓名:</label>
|
||||
<input type="text" id="userName" class="form-input" placeholder="请输入姓名">
|
||||
</div>
|
||||
<button id="loginBtn" class="btn btn-primary">登录/注册</button>
|
||||
<button id="loginBtn" class="btn btn-primary">登录/注册</button>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息问卷 -->
|
||||
<div id="basicQuestionnaire" class="section" style="display: none;">
|
||||
<h2>基础信息问卷</h2>
|
||||
<h2>基础信息问卷</h2>
|
||||
<div class="form-group">
|
||||
<label>年龄:</label>
|
||||
<input type="number" id="age" class="form-input" placeholder="请输入年龄">
|
||||
<label>年龄:</label>
|
||||
<input type="number" id="age" class="form-input" placeholder="请输入年龄">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>性别:</label>
|
||||
<label>性别:</label>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>身高(cm):</label>
|
||||
<input type="number" id="height" class="form-input" placeholder="请输入身高">
|
||||
<label>身高(cm):</label>
|
||||
<input type="number" id="height" class="form-input" placeholder="请输入身高">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>体重(kg):</label>
|
||||
<input type="number" id="weight" class="form-input" placeholder="请输入体重">
|
||||
<label>体重(kg):</label>
|
||||
<input type="number" id="weight" class="form-input" placeholder="请输入体重">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>活动水平:</label>
|
||||
<label>活动水平:</label>
|
||||
<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>
|
||||
</div>
|
||||
<button id="submitBasicBtn" class="btn btn-primary">提交基础信息</button>
|
||||
<button id="submitBasicBtn" class="btn btn-primary">提交基础信息</button>
|
||||
</div>
|
||||
|
||||
<!-- 餐食记录 -->
|
||||
<div id="mealRecord" class="section" style="display: none;">
|
||||
<h2>记录餐食</h2>
|
||||
<h2>记录餐食</h2>
|
||||
<div class="form-group">
|
||||
<label>日期:</label>
|
||||
<label>日期:</label>
|
||||
<input type="date" id="mealDate" class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>餐次:</label>
|
||||
<label>餐次:</label>
|
||||
<select id="mealType" class="form-input">
|
||||
<option value="breakfast">早餐</option>
|
||||
<option value="lunch">午餐</option>
|
||||
<option value="dinner">晚餐</option>
|
||||
<option value="breakfast">早餐</option>
|
||||
<option value="lunch">午餐</option>
|
||||
<option value="dinner">晚餐</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>食物列表(每行一个):</label>
|
||||
<textarea id="foods" class="form-textarea" rows="5" placeholder="例如: 燕麦粥 1碗 香蕉 1根 牛奶 200ml"></textarea>
|
||||
<label>食物列表(每行一个):</label>
|
||||
<textarea id="foods" class="form-textarea" rows="5" placeholder="例如: 燕麦粥 1碗 香蕉 1根 牛奶 200ml"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>热量(大卡):</label>
|
||||
<input type="number" id="calories" class="form-input" placeholder="请输入热量">
|
||||
<label>热量(大卡):</label>
|
||||
<input type="number" id="calories" class="form-input" placeholder="请输入热量">
|
||||
</div>
|
||||
<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">
|
||||
</div>
|
||||
<button id="submitMealBtn" class="btn btn-primary">记录餐食</button>
|
||||
<button id="submitMealBtn" class="btn btn-primary">记录餐食</button>
|
||||
</div>
|
||||
|
||||
<!-- 操作提示 -->
|
||||
<div id="messageArea" class="message-area"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2024 个性化饮食推荐助手</p>
|
||||
<p>© 2024 个性化饮食推荐助手</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/data_collection.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -51,6 +51,13 @@
|
||||
</div>
|
||||
<button id="spinBtn" class="btn btn-spin">转动转盘</button>
|
||||
<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>
|
||||
|
||||
<!-- 排序结果显示 -->
|
||||
|
||||
@@ -3,68 +3,63 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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/recommendation.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>? 智能推荐</h1>
|
||||
<p class="subtitle">基于AI的个性化餐食推荐</p>
|
||||
<h1>🤖 智能推荐</h1>
|
||||
<p class="subtitle">基于AI的个性化餐食推荐</p>
|
||||
</header>
|
||||
|
||||
<nav class="nav">
|
||||
<a href="/" class="nav-item">首页</a>
|
||||
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||
<a href="/recommendation" class="nav-item active">智能推荐</a>
|
||||
<a href="/analysis" class="nav-item">营养分析</a>
|
||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||
<a href="/" class="nav-item">首页</a>
|
||||
<a href="/data-collection" class="nav-item">数据采集</a>
|
||||
<a href="/recommendation" class="nav-item active">智能推荐</a>
|
||||
<a href="/analysis" class="nav-item">营养分析</a>
|
||||
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
<div class="recommendation-container">
|
||||
<!-- 用户登录区域 -->
|
||||
<div id="loginSection" class="section">
|
||||
<h2>用户登录</h2>
|
||||
<h2>用户登录</h2>
|
||||
<div class="form-group">
|
||||
<label for="userId">用户ID:</label>
|
||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||
<label for="userId">用户ID:</label>
|
||||
<input type="text" id="userId" class="form-input" placeholder="请输入用户ID">
|
||||
</div>
|
||||
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||
<button id="loginBtn" class="btn btn-primary">登录</button>
|
||||
</div>
|
||||
|
||||
<!-- 推荐请求区域 -->
|
||||
<div id="requestSection" class="section" style="display: none;">
|
||||
<h2>获取推荐</h2>
|
||||
<h2>获取推荐</h2>
|
||||
<div class="form-group">
|
||||
<label>餐次:</label>
|
||||
<label>餐次:</label>
|
||||
<select id="mealType" class="form-input">
|
||||
<option value="breakfast">早餐</option>
|
||||
<option value="lunch">午餐</option>
|
||||
<option value="dinner">晚餐</option>
|
||||
<option value="breakfast">早餐</option>
|
||||
<option value="lunch">午餐</option>
|
||||
<option value="dinner">晚餐</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="getRecommendationBtn" class="btn btn-primary">获取推荐</button>
|
||||
<button id="getRecommendationBtn" class="btn btn-primary">获取推荐</button>
|
||||
</div>
|
||||
|
||||
<!-- 推荐结果显示 -->
|
||||
<div id="recommendationsSection" class="section" style="display: none;">
|
||||
<h2>推荐结果</h2>
|
||||
<h2>推荐结果</h2>
|
||||
<div id="recommendationsList" class="recommendations-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- 操作提示 -->
|
||||
<div id="messageArea" class="message-area"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<p>© 2024 个性化饮食推荐助手</p>
|
||||
<p>© 2024 个性化饮食推荐助手</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/recommendation.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
57
web_app.py
57
web_app.py
@@ -18,14 +18,25 @@ from modules.ai_analysis import AIAnalysisModule
|
||||
from modules.recommendation_engine import RecommendationEngine
|
||||
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(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('logs/web_app.log', encoding='utf-8'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
logging.StreamHandler(sys.stdout)
|
||||
],
|
||||
force=True # 强制重新配置
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -35,6 +46,9 @@ app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||
# 确保模板文件使用UTF-8编码读取
|
||||
app.jinja_env.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编码
|
||||
@app.after_request
|
||||
@@ -156,31 +170,31 @@ sorter = RecitationSorter()
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""首页"""
|
||||
return render_template('index.html', encoding='utf-8')
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@app.route('/recitation')
|
||||
def recitation():
|
||||
"""背诵排序页面"""
|
||||
return render_template('recitation.html', encoding='utf-8')
|
||||
return render_template('recitation.html')
|
||||
|
||||
|
||||
@app.route('/data-collection')
|
||||
def data_collection():
|
||||
"""数据采集页面"""
|
||||
return render_template('data_collection.html', encoding='utf-8')
|
||||
return render_template('data_collection.html')
|
||||
|
||||
|
||||
@app.route('/recommendation')
|
||||
def recommendation():
|
||||
"""推荐页面"""
|
||||
return render_template('recommendation.html', encoding='utf-8')
|
||||
return render_template('recommendation.html')
|
||||
|
||||
|
||||
@app.route('/analysis')
|
||||
def analysis():
|
||||
"""分析页面"""
|
||||
return render_template('analysis.html', encoding='utf-8')
|
||||
return render_template('analysis.html')
|
||||
|
||||
|
||||
@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'
|
||||
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(
|
||||
content.encode('utf-8'),
|
||||
mimetype=mimetype,
|
||||
headers={
|
||||
'Content-Disposition': f'attachment; filename="{filename}"'
|
||||
'Content-Disposition': content_disposition
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user