feat: 娣诲姞缃戦〉鐗堝簲鐢ㄥ拰鑳岃鎺掑簭鍔熻兘

- 鍒涘缓Flask缃戦〉搴旂敤妗嗘灦(web_app.py)
- 娣诲姞鑳岃鎺掑簭鍔熻兘锛氱煡璇嗙偣璇嗗埆鍜岄殢鏈烘帓搴?- 瀹炵幇杞洏鎶借儗鍔熻兘(鍩轰簬SVG)
- 鍒涘缓鍓嶇椤甸潰锛氶椤靛拰鑳岃鎺掑簭椤甸潰
- 娣诲姞鍝嶅簲寮廋SS鏍峰紡
- 鍒涘缓鍚姩鑴氭湰(start_web.py)
- 鏇存柊requirements.txt娣诲姞Flask渚濊禆
- 娣诲姞缃戦〉鐗堜娇鐢ㄨ鏄?README_WEB.md)
This commit is contained in:
赵杰 Jie Zhao (雄狮汽车科技)
2025-11-02 20:44:19 +08:00
parent 14521369b9
commit 8b5063a092
9 changed files with 1258 additions and 1 deletions

203
README_WEB.md Normal file
View 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等

View File

@@ -27,4 +27,6 @@ easyocr>=1.7.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
View 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
View 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
View 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
View 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
View 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>&copy; 2024 个性化饮食推荐助手</p>
</footer>
</div>
</body>
</html>

72
templates/recitation.html Normal file
View 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="例如:&#10;第一章 西周&#10;夏商学校名称&#10;西周学在官府&#10;国学乡学&#10;六艺&#10;私学兴起的原因与意义&#10;稷下学宫..."
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>&copy; 2024 个性化饮食推荐助手</p>
</footer>
</div>
<script src="{{ url_for('static', filename='js/recitation.js') }}"></script>
</body>
</html>

205
web_app.py Normal file
View 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)