From 8b5063a0928ec7fe43817f209bf45938fa4ac5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B0=20Jie=20Zhao=20=EF=BC=88=E9=9B=84?= =?UTF-8?q?=E7=8B=AE=E6=B1=BD=E8=BD=A6=E7=A7=91=E6=8A=80=EF=BC=89?= <00061074@chery.local> Date: Sun, 2 Nov 2025 20:44:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A8=A3=E8=AF=B2=E5=A7=9E=E7=BC=83?= =?UTF-8?q?=E6=88=A6=E3=80=89=E9=90=97=E5=A0=9D=E7=B0=B2=E9=90=A2=E3=84=A5?= =?UTF-8?q?=E6=8B=B0=E9=91=B3=E5=B2=83=EE=87=AA=E9=8E=BA=E6=8E=91=E7=B0=AD?= =?UTF-8?q?=E9=8D=94=E7=86=BB=E5=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 鍒涘缓Flask缃戦〉搴旂敤妗嗘灦(web_app.py) - 娣诲姞鑳岃鎺掑簭鍔熻兘锛氱煡璇嗙偣璇嗗埆鍜岄殢鏈烘帓搴?- 瀹炵幇杞洏鎶借儗鍔熻兘(鍩轰簬SVG) - 鍒涘缓鍓嶇椤甸潰锛氶椤靛拰鑳岃鎺掑簭椤甸潰 - 娣诲姞鍝嶅簲寮廋SS鏍峰紡 - 鍒涘缓鍚姩鑴氭湰(start_web.py) - 鏇存柊requirements.txt娣诲姞Flask渚濊禆 - 娣诲姞缃戦〉鐗堜娇鐢ㄨ鏄?README_WEB.md) --- README_WEB.md | 203 +++++++++++++++++++++++++++ requirements.txt | 4 +- start_web.py | 57 ++++++++ static/css/recitation.css | 212 ++++++++++++++++++++++++++++ static/css/style.css | 179 ++++++++++++++++++++++++ static/js/recitation.js | 285 ++++++++++++++++++++++++++++++++++++++ templates/index.html | 42 ++++++ templates/recitation.html | 72 ++++++++++ web_app.py | 205 +++++++++++++++++++++++++++ 9 files changed, 1258 insertions(+), 1 deletion(-) create mode 100644 README_WEB.md create mode 100644 start_web.py create mode 100644 static/css/recitation.css create mode 100644 static/css/style.css create mode 100644 static/js/recitation.js create mode 100644 templates/index.html create mode 100644 templates/recitation.html create mode 100644 web_app.py diff --git a/README_WEB.md b/README_WEB.md new file mode 100644 index 0000000..bd81bc0 --- /dev/null +++ b/README_WEB.md @@ -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 + +## ʹò + +### һ뱳 + +ıճ֪ʶбı֧¸ʽ + +- бʽֿͷ +- ʽӱиƣ +- ͨıÿһ֪ʶ㣩 + +ʾ +``` +һ +ѧУ +ѧڹٸ +ѧѧ + +˽ѧԭ +ѧ +``` + +### ڶʶ֪ʶ + +"ʶ֪ʶ"ťϵͳԶ +- ޹ݣͷҳȣ +- ȡЧ֪ʶ +- ʾʶ + +### + +"ʼ"ťϵͳ᣺ +- ֪ʶ +- б +- ת̽ + +### 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/requirements.txt b/requirements.txt index 9e99d50..40f83da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,4 +27,6 @@ easyocr>=1.7.0 # 移动端支持 (可选) kivy>=2.1.0 -kivymd>=1.1.1 \ No newline at end of file +kivymd>=1.1.1 +# 缃戦〉绔敮鎸乣nFlask>=3.0.0 +Werkzeug>=3.0.0 diff --git a/start_web.py b/start_web.py new file mode 100644 index 0000000..55ecf98 --- /dev/null +++ b/start_web.py @@ -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) diff --git a/static/css/recitation.css b/static/css/recitation.css new file mode 100644 index 0000000..a67aca2 --- /dev/null +++ b/static/css/recitation.css @@ -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; + } +} + diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..de45c28 --- /dev/null +++ b/static/css/style.css @@ -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; + } +} + diff --git a/static/js/recitation.js b/static/js/recitation.js new file mode 100644 index 0000000..bbccc6f --- /dev/null +++ b/static/js/recitation.js @@ -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; // ת̰뾶DZ߿ + 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; +}); + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a36e378 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,42 @@ + + + + + + 个性化饮食推荐助手 + + + +
+
+

个性化饮食推荐助手

+

智能饮食推荐 + 背诵排序工具

+
+ + + +
+
+

欢迎使用

+

这是一个集成了智能饮食推荐和背诵排序功能的网页应用。

+ +
+
+
🎯
+

背诵排序

+

输入你的背诵内容,系统会自动识别知识点并随机排序,帮助你更好地复习。

+ 开始使用 +
+
+
+
+ + +
+ + diff --git a/templates/recitation.html b/templates/recitation.html new file mode 100644 index 0000000..b79729e --- /dev/null +++ b/templates/recitation.html @@ -0,0 +1,72 @@ + + + + + + 背诵排序 - 转盘抽背 + + + + +
+
+

🎯 背诵排序工具

+

随机抽背,高效复习

+
+ + + +
+
+ +
+

第一步:输入背诵内容

+

请粘贴包含知识点列表的文本(支持从表格、列表等形式中自动识别)

+ + +
+ + + + + + + + + +
+
+ + +
+ + + + diff --git a/web_app.py b/web_app.py new file mode 100644 index 0000000..4e2ea0b --- /dev/null +++ b/web_app.py @@ -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)