diff --git a/README.md b/README.md index e6b0db2..0102719 100644 --- a/README.md +++ b/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. 配置环境 diff --git a/README_WEB.md b/README_WEB.md deleted file mode 100644 index bd81bc0..0000000 --- a/README_WEB.md +++ /dev/null @@ -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 - -## ʹò - -### һ뱳 - -ıճ֪ʶбı֧¸ʽ - -- бʽֿͷ -- ʽӱиƣ -- ͨıÿһ֪ʶ㣩 - -ʾ -``` -һ -ѧУ -ѧڹٸ -ѧѧ - -˽ѧԭ -ѧ -``` - -### ڶʶ֪ʶ - -"ʶ֪ʶ"ťϵͳԶ -- ޹ݣͷҳȣ -- ȡЧ֪ʶ -- ʾʶ - -### - -"ʼ"ťϵͳ᣺ -- ֪ʶ -- б -- ת̽ - -### IJת̳鱳 - -"תת"ť -- ת̻ת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. ״лԶҪĿ¼templatesstaticlogs -2. ڱػʹã蹫÷ǽͷ -3. ־ļ `logs/web_app.log` - -## ų - -### ⣺޷Ӧ - -**** -- FlaskǷѰװ`pip list | grep Flask` -- ˿5000Ƿռ -- 鿴־ļ `logs/web_app.log` - -### ⣺޷ʶ֪ʶ - -**** -- ȷıʽȷ -- ֶıÿһ֪ʶ -- Ƿַ - -### ⣺ת̲ʾת쳣 - -**** -- Ƿ֧SVG -- -- ʹִChromeFirefoxEdgeȣ - diff --git a/UI_BEAUTIFICATION_SUMMARY.md b/UI_BEAUTIFICATION_SUMMARY.md deleted file mode 100644 index a736a2c..0000000 --- a/UI_BEAUTIFICATION_SUMMARY.md +++ /dev/null @@ -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月* diff --git a/config/settings.py b/config/settings.py index 227c193..7e5bfd6 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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 diff --git a/modules/ocr_calorie_recognition.py b/modules/ocr_calorie_recognition.py index 55aeae0..6718f3b 100644 --- a/modules/ocr_calorie_recognition.py +++ b/modules/ocr_calorie_recognition.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 40f83da..001c6f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/start_web.py b/start_web.py index 55ecf98..e0b9051 100644 --- a/start_web.py +++ b/start_web.py @@ -21,9 +21,6 @@ def check_flask(): return False def main(): - """启动网页应用""" - print("🌐 启动网页应用...") - print("=" * 50) if not check_flask(): return False diff --git a/static/css/recitation.css b/static/css/recitation.css index e4ca83d..2cbcc88 100644 --- a/static/css/recitation.css +++ b/static/css/recitation.css @@ -70,7 +70,7 @@ font-size: 0.9em; } -/* תʽ */ +/* ת����ʽ */ .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; +} + +/* �������б� */ .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; +} + +/* ������ť��ʽ */ .export-buttons { display: flex; gap: 10px; @@ -219,7 +308,7 @@ transform: translateY(0); } -/* Ӧʽ */ +/* ��Ӧʽ��� */ @media (max-width: 768px) { .wheel-container { width: 300px; diff --git a/static/js/recitation.js b/static/js/recitation.js index 7cfaa49..f69b6e4 100644 --- a/static/js/recitation.js +++ b/static/js/recitation.js @@ -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; // ת̰뾶DZ߿ + 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(); // 清除本地存储 }); - diff --git a/templates/analysis.html b/templates/analysis.html index 726ef62..3abc953 100644 --- a/templates/analysis.html +++ b/templates/analysis.html @@ -3,64 +3,59 @@ - Ӫ - ԻʳƼ + Ӫ - ԻʳƼ
-

? Ӫ

-

AIӪ뽨

+

? Ӫ

+

AIӪ뽨

-
-

û¼

+

û¼

- - + +
- +
- - -
- diff --git a/templates/data_collection.html b/templates/data_collection.html index 9e405ab..24c418f 100644 --- a/templates/data_collection.html +++ b/templates/data_collection.html @@ -3,118 +3,106 @@ - ݲɼ - ԻʳƼ + 数据采集 - 个性化饮食推荐助手
-

? ݲɼ

-

ĸʳ

+

📝 数据采集

+

建立你的个人饮食档案

- -
-
-

û¼

+

用户登录

- - + +
- - + +
- +
- - - - - -
-
- - - + \ No newline at end of file diff --git a/templates/recitation.html b/templates/recitation.html index a3ccfd2..6676ce3 100644 --- a/templates/recitation.html +++ b/templates/recitation.html @@ -51,6 +51,13 @@
+ +
+ 剩余 0 个知识点 +
diff --git a/templates/recommendation.html b/templates/recommendation.html index 128b5de..2d686e8 100644 --- a/templates/recommendation.html +++ b/templates/recommendation.html @@ -3,68 +3,63 @@ - Ƽ - ԻʳƼ + 智能推荐 - 个性化饮食推荐助手
-

? Ƽ

-

AIĸԻʳƼ

+

🤖 智能推荐

+

基于AI的个性化餐食推荐

-
-

û¼

+

用户登录

- - + +
- +
- - -
- diff --git a/web_app.py b/web_app.py index 657d377..8872e25 100644 --- a/web_app.py +++ b/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 } )