feat: 娣诲姞缃戦〉鐗堝簲鐢ㄥ拰鑳岃鎺掑簭鍔熻兘
- 鍒涘缓Flask缃戦〉搴旂敤妗嗘灦(web_app.py) - 娣诲姞鑳岃鎺掑簭鍔熻兘锛氱煡璇嗙偣璇嗗埆鍜岄殢鏈烘帓搴?- 瀹炵幇杞洏鎶借儗鍔熻兘(鍩轰簬SVG) - 鍒涘缓鍓嶇椤甸潰锛氶椤靛拰鑳岃鎺掑簭椤甸潰 - 娣诲姞鍝嶅簲寮廋SS鏍峰紡 - 鍒涘缓鍚姩鑴氭湰(start_web.py) - 鏇存柊requirements.txt娣诲姞Flask渚濊禆 - 娣诲姞缃戦〉鐗堜娇鐢ㄨ鏄?README_WEB.md)
This commit is contained in:
203
README_WEB.md
Normal file
203
README_WEB.md
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# 网页版使用说明
|
||||||
|
|
||||||
|
## 功能介绍
|
||||||
|
|
||||||
|
这是一个基于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等)
|
||||||
|
|
||||||
@@ -28,3 +28,5 @@ easyocr>=1.7.0
|
|||||||
# 移动端支持 (可选)
|
# 移动端支持 (可选)
|
||||||
kivy>=2.1.0
|
kivy>=2.1.0
|
||||||
kivymd>=1.1.1
|
kivymd>=1.1.1
|
||||||
|
# 缃戦〉绔敮鎸乣nFlask>=3.0.0
|
||||||
|
Werkzeug>=3.0.0
|
||||||
|
|||||||
57
start_web.py
Normal file
57
start_web.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
启动网页应用的脚本
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加项目根目录到Python路径
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
sys.path.insert(0, str(project_root))
|
||||||
|
|
||||||
|
def check_flask():
|
||||||
|
"""检查Flask是否已安装"""
|
||||||
|
try:
|
||||||
|
import flask
|
||||||
|
print(f"✓ Flask已安装 (版本: {flask.__version__})")
|
||||||
|
return True
|
||||||
|
except ImportError:
|
||||||
|
print("✗ Flask未安装,请运行: pip install Flask")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""启动网页应用"""
|
||||||
|
print("🌐 启动网页应用...")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
if not check_flask():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 创建必要的目录
|
||||||
|
Path('templates').mkdir(exist_ok=True)
|
||||||
|
Path('static/css').mkdir(parents=True, exist_ok=True)
|
||||||
|
Path('static/js').mkdir(parents=True, exist_ok=True)
|
||||||
|
Path('logs').mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
print("\n🚀 正在启动网页服务器...")
|
||||||
|
print("=" * 50)
|
||||||
|
print("📱 访问地址: http://localhost:5000")
|
||||||
|
print("📝 背诵排序: http://localhost:5000/recitation")
|
||||||
|
print("\n按 Ctrl+C 停止服务器\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from web_app import app
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n👋 服务器已停止")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 启动失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = main()
|
||||||
|
if not success:
|
||||||
|
sys.exit(1)
|
||||||
212
static/css/recitation.css
Normal file
212
static/css/recitation.css
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
.recitation-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section,
|
||||||
|
.extracted-section,
|
||||||
|
.wheel-section,
|
||||||
|
.result-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
padding: 30px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section h2,
|
||||||
|
.extracted-section h2,
|
||||||
|
.wheel-section h2,
|
||||||
|
.result-section h2 {
|
||||||
|
font-size: 1.8em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint,
|
||||||
|
.info {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info span {
|
||||||
|
color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
font-family: inherit;
|
||||||
|
resize: vertical;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 15px;
|
||||||
|
margin: 5px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 转盘样式 */
|
||||||
|
.wheel-container {
|
||||||
|
position: relative;
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
margin: 40px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
border: 8px solid #667eea;
|
||||||
|
background: white;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99);
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel svg {
|
||||||
|
transition: transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel-pointer {
|
||||||
|
position: absolute;
|
||||||
|
top: -15px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 15px solid transparent;
|
||||||
|
border-right: 15px solid transparent;
|
||||||
|
border-top: 30px solid #667eea;
|
||||||
|
z-index: 10;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spin {
|
||||||
|
display: block;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 15px 50px;
|
||||||
|
font-size: 1.2em;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spin:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 5px 20px rgba(245, 87, 108, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spin:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
min-height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 排序结果列表 */
|
||||||
|
.sorted-list {
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorted-item {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #667eea;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorted-item:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorted-item-number {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 15px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sorted-item-text {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.wheel-container {
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wheel-item {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-section,
|
||||||
|
.extracted-section,
|
||||||
|
.wheel-section,
|
||||||
|
.result-section {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
179
static/css/style.css
Normal file
179
static/css/style.css
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 40px 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.1em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 0;
|
||||||
|
border-bottom: 2px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
flex: 1;
|
||||||
|
padding: 15px 20px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: #667eea;
|
||||||
|
border-bottom-color: #667eea;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 40px 30px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section p {
|
||||||
|
font-size: 1.1em;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ÏìӦʽÉè¼Æ */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 20px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
285
static/js/recitation.js
Normal file
285
static/js/recitation.js
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
// 背诵排序功能脚本
|
||||||
|
|
||||||
|
let extractedItems = [];
|
||||||
|
let sortedItems = [];
|
||||||
|
let currentSpinIndex = 0;
|
||||||
|
let isSpinning = false;
|
||||||
|
|
||||||
|
// 颜色配置 - 转盘使用不同颜色
|
||||||
|
const colors = [
|
||||||
|
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
||||||
|
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
||||||
|
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
||||||
|
];
|
||||||
|
|
||||||
|
// DOM元素
|
||||||
|
const textInput = document.getElementById('textInput');
|
||||||
|
const extractBtn = document.getElementById('extractBtn');
|
||||||
|
const extractedSection = document.getElementById('extractedSection');
|
||||||
|
const itemsList = document.getElementById('itemsList');
|
||||||
|
const itemCount = document.getElementById('itemCount');
|
||||||
|
const sortBtn = document.getElementById('sortBtn');
|
||||||
|
const wheelSection = document.getElementById('wheelSection');
|
||||||
|
const wheel = document.getElementById('wheel');
|
||||||
|
const spinBtn = document.getElementById('spinBtn');
|
||||||
|
const currentItem = document.getElementById('currentItem');
|
||||||
|
const resultSection = document.getElementById('resultSection');
|
||||||
|
const sortedList = document.getElementById('sortedList');
|
||||||
|
const resetBtn = document.getElementById('resetBtn');
|
||||||
|
|
||||||
|
// 提取知识点
|
||||||
|
extractBtn.addEventListener('click', async () => {
|
||||||
|
const text = textInput.value.trim();
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
alert('请输入要处理的文本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
extractBtn.disabled = true;
|
||||||
|
extractBtn.textContent = '识别中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/extract', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
extractedItems = data.items;
|
||||||
|
displayExtractedItems(extractedItems);
|
||||||
|
extractedSection.style.display = 'block';
|
||||||
|
textInput.disabled = true;
|
||||||
|
} else {
|
||||||
|
alert(data.message || '提取失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提取失败:', error);
|
||||||
|
alert('提取失败,请检查网络连接');
|
||||||
|
} finally {
|
||||||
|
extractBtn.disabled = false;
|
||||||
|
extractBtn.textContent = '识别知识点';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示提取的项目
|
||||||
|
function displayExtractedItems(items) {
|
||||||
|
itemCount.textContent = items.length;
|
||||||
|
itemsList.innerHTML = '';
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = 'item-tag';
|
||||||
|
tag.textContent = `${index + 1}. ${item}`;
|
||||||
|
itemsList.appendChild(tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机排序
|
||||||
|
sortBtn.addEventListener('click', async () => {
|
||||||
|
if (extractedItems.length === 0) {
|
||||||
|
alert('请先提取知识点');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBtn.disabled = true;
|
||||||
|
sortBtn.textContent = '排序中...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/sort', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ items: extractedItems })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
sortedItems = data.items;
|
||||||
|
displaySortedItems(sortedItems);
|
||||||
|
createWheel(sortedItems);
|
||||||
|
wheelSection.style.display = 'block';
|
||||||
|
resultSection.style.display = 'block';
|
||||||
|
currentSpinIndex = 0;
|
||||||
|
} else {
|
||||||
|
alert(data.message || '排序失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('排序失败:', error);
|
||||||
|
alert('排序失败,请检查网络连接');
|
||||||
|
} finally {
|
||||||
|
sortBtn.disabled = false;
|
||||||
|
sortBtn.textContent = '开始随机排序';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建转盘 - 使用SVG实现更真实的转盘效果
|
||||||
|
function createWheel(items) {
|
||||||
|
wheel.innerHTML = '';
|
||||||
|
|
||||||
|
if (items.length === 0) return;
|
||||||
|
|
||||||
|
const anglePerItem = 360 / items.length;
|
||||||
|
const radius = 190; // 转盘半径(考虑边框)
|
||||||
|
const centerX = 200;
|
||||||
|
const centerY = 200;
|
||||||
|
|
||||||
|
// 创建SVG转盘
|
||||||
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
svg.setAttribute('width', '400');
|
||||||
|
svg.setAttribute('height', '400');
|
||||||
|
svg.setAttribute('viewBox', '0 0 400 400');
|
||||||
|
svg.style.position = 'absolute';
|
||||||
|
svg.style.top = '0';
|
||||||
|
svg.style.left = '0';
|
||||||
|
svg.style.width = '100%';
|
||||||
|
svg.style.height = '100%';
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const startAngle = (index * anglePerItem - 90) * Math.PI / 180;
|
||||||
|
const endAngle = ((index + 1) * anglePerItem - 90) * Math.PI / 180;
|
||||||
|
|
||||||
|
const x1 = centerX + radius * Math.cos(startAngle);
|
||||||
|
const y1 = centerY + radius * Math.sin(startAngle);
|
||||||
|
const x2 = centerX + radius * Math.cos(endAngle);
|
||||||
|
const y2 = centerY + radius * Math.sin(endAngle);
|
||||||
|
|
||||||
|
const largeArcFlag = anglePerItem > 180 ? 1 : 0;
|
||||||
|
|
||||||
|
const pathData = [
|
||||||
|
`M ${centerX} ${centerY}`,
|
||||||
|
`L ${x1} ${y1}`,
|
||||||
|
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
|
||||||
|
'Z'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||||
|
path.setAttribute('d', pathData);
|
||||||
|
const colorIndex = index % colors.length;
|
||||||
|
path.setAttribute('fill', colors[colorIndex]);
|
||||||
|
path.setAttribute('stroke', '#fff');
|
||||||
|
path.setAttribute('stroke-width', '2');
|
||||||
|
|
||||||
|
svg.appendChild(path);
|
||||||
|
|
||||||
|
// 添加文本
|
||||||
|
const midAngle = (startAngle + endAngle) / 2;
|
||||||
|
const textRadius = radius * 0.7;
|
||||||
|
const textX = centerX + textRadius * Math.cos(midAngle);
|
||||||
|
const textY = centerY + textRadius * Math.sin(midAngle);
|
||||||
|
|
||||||
|
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
|
text.setAttribute('x', textX);
|
||||||
|
text.setAttribute('y', textY);
|
||||||
|
text.setAttribute('text-anchor', 'middle');
|
||||||
|
text.setAttribute('fill', 'white');
|
||||||
|
text.setAttribute('font-size', items.length > 8 ? '12' : '14');
|
||||||
|
text.setAttribute('font-weight', 'bold');
|
||||||
|
text.setAttribute('transform', `rotate(${(midAngle * 180 / Math.PI + 90)}, ${textX}, ${textY})`);
|
||||||
|
|
||||||
|
const displayText = item.length > 12 ? item.substring(0, 12) + '...' : item;
|
||||||
|
text.textContent = displayText;
|
||||||
|
|
||||||
|
svg.appendChild(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
wheel.appendChild(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转动转盘
|
||||||
|
spinBtn.addEventListener('click', () => {
|
||||||
|
if (isSpinning || sortedItems.length === 0) return;
|
||||||
|
|
||||||
|
isSpinning = true;
|
||||||
|
spinBtn.disabled = true;
|
||||||
|
currentItem.textContent = '转盘中...';
|
||||||
|
|
||||||
|
// 随机选择一个索引(添加多圈旋转效果)
|
||||||
|
const randomIndex = Math.floor(Math.random() * sortedItems.length);
|
||||||
|
const spins = 3; // 转3圈
|
||||||
|
const anglePerItem = 360 / sortedItems.length;
|
||||||
|
// 计算目标角度:多转几圈 + 指向选中项
|
||||||
|
const targetAngle = spins * 360 + (360 - (randomIndex * anglePerItem) - anglePerItem / 2);
|
||||||
|
|
||||||
|
// 获取当前角度
|
||||||
|
const svg = wheel.querySelector('svg');
|
||||||
|
const currentAngle = getCurrentRotation(svg);
|
||||||
|
|
||||||
|
// 计算总旋转角度(考虑当前角度)
|
||||||
|
const totalRotation = currentAngle + targetAngle;
|
||||||
|
|
||||||
|
svg.style.transform = `rotate(${totalRotation}deg)`;
|
||||||
|
|
||||||
|
// 转盘停止后显示结果
|
||||||
|
setTimeout(() => {
|
||||||
|
currentItem.textContent = `${randomIndex + 1}. ${sortedItems[randomIndex]}`;
|
||||||
|
currentSpinIndex = randomIndex;
|
||||||
|
isSpinning = false;
|
||||||
|
spinBtn.disabled = false;
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取当前旋转角度
|
||||||
|
function getCurrentRotation(element) {
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const transform = style.transform;
|
||||||
|
if (transform === 'none') return 0;
|
||||||
|
|
||||||
|
const matrix = new DOMMatrix(transform);
|
||||||
|
const angle = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
|
||||||
|
return angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示排序结果
|
||||||
|
function displaySortedItems(items) {
|
||||||
|
sortedList.innerHTML = '';
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const itemDiv = document.createElement('div');
|
||||||
|
itemDiv.className = 'sorted-item';
|
||||||
|
|
||||||
|
const numberSpan = document.createElement('span');
|
||||||
|
numberSpan.className = 'sorted-item-number';
|
||||||
|
numberSpan.textContent = index + 1;
|
||||||
|
|
||||||
|
const textSpan = document.createElement('span');
|
||||||
|
textSpan.className = 'sorted-item-text';
|
||||||
|
textSpan.textContent = item;
|
||||||
|
|
||||||
|
itemDiv.appendChild(numberSpan);
|
||||||
|
itemDiv.appendChild(textSpan);
|
||||||
|
sortedList.appendChild(itemDiv);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
extractedItems = [];
|
||||||
|
sortedItems = [];
|
||||||
|
currentSpinIndex = 0;
|
||||||
|
|
||||||
|
textInput.value = '';
|
||||||
|
textInput.disabled = false;
|
||||||
|
|
||||||
|
extractedSection.style.display = 'none';
|
||||||
|
wheelSection.style.display = 'none';
|
||||||
|
resultSection.style.display = 'none';
|
||||||
|
|
||||||
|
wheel.innerHTML = '';
|
||||||
|
const svg = wheel.querySelector('svg');
|
||||||
|
if (svg) {
|
||||||
|
svg.style.transform = 'rotate(0deg)';
|
||||||
|
}
|
||||||
|
currentItem.textContent = '';
|
||||||
|
|
||||||
|
isSpinning = false;
|
||||||
|
spinBtn.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
42
templates/index.html
Normal file
42
templates/index.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>个性化饮食推荐助手</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>个性化饮食推荐助手</h1>
|
||||||
|
<p class="subtitle">智能饮食推荐 + 背诵排序工具</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-item active">首页</a>
|
||||||
|
<a href="/recitation" class="nav-item">背诵排序</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="welcome-section">
|
||||||
|
<h2>欢迎使用</h2>
|
||||||
|
<p>这是一个集成了智能饮食推荐和背诵排序功能的网页应用。</p>
|
||||||
|
|
||||||
|
<div class="feature-cards">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-icon">🎯</div>
|
||||||
|
<h3>背诵排序</h3>
|
||||||
|
<p>输入你的背诵内容,系统会自动识别知识点并随机排序,帮助你更好地复习。</p>
|
||||||
|
<a href="/recitation" class="btn btn-primary">开始使用</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
72
templates/recitation.html
Normal file
72
templates/recitation.html
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>背诵排序 - 转盘抽背</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/recitation.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<header class="header">
|
||||||
|
<h1>🎯 背诵排序工具</h1>
|
||||||
|
<p class="subtitle">随机抽背,高效复习</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav">
|
||||||
|
<a href="/" class="nav-item">首页</a>
|
||||||
|
<a href="/recitation" class="nav-item active">背诵排序</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<div class="recitation-container">
|
||||||
|
<!-- 输入区域 -->
|
||||||
|
<div class="input-section">
|
||||||
|
<h2>第一步:输入背诵内容</h2>
|
||||||
|
<p class="hint">请粘贴包含知识点列表的文本(支持从表格、列表等形式中自动识别)</p>
|
||||||
|
<textarea
|
||||||
|
id="textInput"
|
||||||
|
class="text-input"
|
||||||
|
placeholder="例如: 第一章 西周 夏商学校名称 西周学在官府 国学乡学 六艺 私学兴起的原因与意义 稷下学宫..."
|
||||||
|
rows="10"
|
||||||
|
></textarea>
|
||||||
|
<button id="extractBtn" class="btn btn-primary">识别知识点</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提取结果显示 -->
|
||||||
|
<div id="extractedSection" class="extracted-section" style="display: none;">
|
||||||
|
<h2>第二步:确认知识点列表</h2>
|
||||||
|
<p class="info">已识别到 <span id="itemCount">0</span> 个知识点</p>
|
||||||
|
<div id="itemsList" class="items-list"></div>
|
||||||
|
<button id="sortBtn" class="btn btn-primary">开始随机排序</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 转盘区域 -->
|
||||||
|
<div id="wheelSection" class="wheel-section" style="display: none;">
|
||||||
|
<h2>第三步:转盘抽背</h2>
|
||||||
|
<div class="wheel-container">
|
||||||
|
<div id="wheel" class="wheel"></div>
|
||||||
|
<div class="wheel-pointer"></div>
|
||||||
|
</div>
|
||||||
|
<button id="spinBtn" class="btn btn-spin">转动转盘</button>
|
||||||
|
<div id="currentItem" class="current-item"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 排序结果显示 -->
|
||||||
|
<div id="resultSection" class="result-section" style="display: none;">
|
||||||
|
<h2>随机排序结果</h2>
|
||||||
|
<div id="sortedList" class="sorted-list"></div>
|
||||||
|
<button id="resetBtn" class="btn btn-secondary">重新开始</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p>© 2024 个性化饮食推荐助手</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/recitation.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
205
web_app.py
Normal file
205
web_app.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
网页端应用 - 个性化饮食推荐助手 + 背诵排序功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
import re
|
||||||
|
import random
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['SECRET_KEY'] = 'your-secret-key-here'
|
||||||
|
# 确保模板文件使用UTF-8编码读取
|
||||||
|
app.jinja_env.auto_reload = True
|
||||||
|
app.config['TEMPLATES_AUTO_RELOAD'] = True
|
||||||
|
|
||||||
|
|
||||||
|
class RecitationSorter:
|
||||||
|
"""背诵排序器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
def extract_items(self, text):
|
||||||
|
"""从文本中提取背诵项目"""
|
||||||
|
items = []
|
||||||
|
|
||||||
|
# 方法1: 按行分割,过滤空行和无关行
|
||||||
|
lines = text.strip().split('\n')
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
# 跳过空行
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 跳过明显的表头行(包含"章节"、"知识点"等)
|
||||||
|
if any(keyword in line for keyword in ['章节', '知识点', '选择题', '主观题', '完成', '划']):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 跳过页码行
|
||||||
|
if re.match(r'^第\d+页', line) or re.match(r'^共\d+页', line):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 跳过说明文字
|
||||||
|
if any(keyword in line for keyword in ['使用说明', '祝:', '凯程', '框架', '理解', '背诵']):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 提取知识点的几种模式
|
||||||
|
# 模式1: 以数字或字母开头(如"1. 知识点"或"第一章 内容")
|
||||||
|
match = re.match(r'^[第]?[一二三四五六七八九十\d]+[章节]?\s*[::、]?\s*(.+)', line)
|
||||||
|
if match:
|
||||||
|
item = match.group(1).strip()
|
||||||
|
if item and len(item) > 1: # 至少2个字符才认为是有效知识点
|
||||||
|
items.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 模式2: 以"-"或"•"开头的列表项
|
||||||
|
match = re.match(r'^[-•]\s*(.+)', line)
|
||||||
|
if match:
|
||||||
|
item = match.group(1).strip()
|
||||||
|
if item and len(item) > 1:
|
||||||
|
items.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 模式3: 表格中的知识点(通常不包含特殊标记符)
|
||||||
|
# 如果行中包含常见的中文标点,但不包含表格标记符,可能是知识点
|
||||||
|
if len(line) > 2 and not re.match(r'^[✓×√✗\s]+$', line):
|
||||||
|
# 检查是否包含常见的中文内容
|
||||||
|
if re.search(r'[\u4e00-\u9fff]', line): # 包含中文
|
||||||
|
# 排除明显的表格分隔符
|
||||||
|
if not re.match(r'^[|+\-\s]+$', line):
|
||||||
|
items.append(line)
|
||||||
|
|
||||||
|
# 去重
|
||||||
|
unique_items = []
|
||||||
|
seen = set()
|
||||||
|
for item in items:
|
||||||
|
# 标准化:去除首尾空格,统一标点
|
||||||
|
normalized = item.strip()
|
||||||
|
if normalized and normalized not in seen:
|
||||||
|
seen.add(normalized)
|
||||||
|
unique_items.append(normalized)
|
||||||
|
|
||||||
|
return unique_items
|
||||||
|
|
||||||
|
def random_sort(self, items):
|
||||||
|
"""随机排序项目"""
|
||||||
|
shuffled = items.copy()
|
||||||
|
random.shuffle(shuffled)
|
||||||
|
return shuffled
|
||||||
|
|
||||||
|
|
||||||
|
# 创建全局排序器实例
|
||||||
|
sorter = RecitationSorter()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""首页"""
|
||||||
|
return render_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/recitation')
|
||||||
|
def recitation():
|
||||||
|
"""背诵排序页面"""
|
||||||
|
return render_template('recitation.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/extract', methods=['POST'])
|
||||||
|
def extract_items():
|
||||||
|
"""提取背诵项目API"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
text = data.get('text', '')
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请输入要处理的文本'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 提取项目
|
||||||
|
items = sorter.extract_items(text)
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '未能识别到背诵内容,请检查文本格式'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
logger.info(f"提取到 {len(items)} 个背诵项目")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'items': items,
|
||||||
|
'count': len(items)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"提取项目失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'处理失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/sort', methods=['POST'])
|
||||||
|
def sort_items():
|
||||||
|
"""随机排序API"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
items = data.get('items', [])
|
||||||
|
|
||||||
|
if not items:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': '请先提取背诵项目'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# 随机排序
|
||||||
|
sorted_items = sorter.random_sort(items)
|
||||||
|
|
||||||
|
logger.info(f"对 {len(sorted_items)} 个项目进行随机排序")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'items': sorted_items,
|
||||||
|
'count': len(sorted_items)
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"排序失败: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'排序失败: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/health')
|
||||||
|
def health():
|
||||||
|
"""健康检查"""
|
||||||
|
return jsonify({'status': 'ok'})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# 创建必要的目录
|
||||||
|
Path('templates').mkdir(exist_ok=True)
|
||||||
|
Path('static').mkdir(exist_ok=True)
|
||||||
|
Path('logs').mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
Reference in New Issue
Block a user