first commit
This commit is contained in:
		
						commit
						f434b83090
					
				
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| # 指定忽略的文件夹 | ||||
| /model/ | ||||
| /ziyao/ | ||||
| /venv/ | ||||
| /uploads/ | ||||
| /temp/ | ||||
| /__pycache__/ | ||||
| */__pycache__/ | ||||
| **/__pycache__/ | ||||
| *.py[cod] | ||||
| *$py.class | ||||
							
								
								
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/.gitignore
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| # 默认忽略的文件 | ||||
| /shelf/ | ||||
| /workspace.xml | ||||
| # 基于编辑器的 HTTP 客户端请求 | ||||
| /httpRequests/ | ||||
| # Datasource local storage ignored files | ||||
| /dataSources/ | ||||
| /dataSources.local.xml | ||||
							
								
								
									
										45
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| <component name="InspectionProjectProfileManager"> | ||||
|   <profile version="1.0"> | ||||
|     <option name="myName" value="Project Default" /> | ||||
|     <inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||||
|       <Languages> | ||||
|         <language minSize="272" name="Python" /> | ||||
|       </Languages> | ||||
|     </inspection_tool> | ||||
|     <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | ||||
|     <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true"> | ||||
|       <option name="ignoredPackages"> | ||||
|         <value> | ||||
|           <list size="13"> | ||||
|             <item index="0" class="java.lang.String" itemvalue="mysql-connector-python" /> | ||||
|             <item index="1" class="java.lang.String" itemvalue="Flask" /> | ||||
|             <item index="2" class="java.lang.String" itemvalue="pandas" /> | ||||
|             <item index="3" class="java.lang.String" itemvalue="boto3" /> | ||||
|             <item index="4" class="java.lang.String" itemvalue="botocore" /> | ||||
|             <item index="5" class="java.lang.String" itemvalue="flask-mail" /> | ||||
|             <item index="6" class="java.lang.String" itemvalue="flask-cors" /> | ||||
|             <item index="7" class="java.lang.String" itemvalue="python-dotenv" /> | ||||
|             <item index="8" class="java.lang.String" itemvalue="Flask-Bcrypt" /> | ||||
|             <item index="9" class="java.lang.String" itemvalue="pytz" /> | ||||
|             <item index="10" class="java.lang.String" itemvalue="Flask-Session" /> | ||||
|             <item index="11" class="java.lang.String" itemvalue="DBUtils" /> | ||||
|             <item index="12" class="java.lang.String" itemvalue="PyMySQL" /> | ||||
|           </list> | ||||
|         </value> | ||||
|       </option> | ||||
|     </inspection_tool> | ||||
|     <inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||||
|       <option name="ignoredErrors"> | ||||
|         <list> | ||||
|           <option value="E265" /> | ||||
|           <option value="E231" /> | ||||
|           <option value="E262" /> | ||||
|           <option value="E225" /> | ||||
|           <option value="E402" /> | ||||
|           <option value="E271" /> | ||||
|           <option value="E302" /> | ||||
|         </list> | ||||
|       </option> | ||||
|     </inspection_tool> | ||||
|   </profile> | ||||
| </component> | ||||
							
								
								
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/inspectionProfiles/profiles_settings.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <component name="InspectionProjectProfileManager"> | ||||
|   <settings> | ||||
|     <option name="USE_PROJECT_PROFILE" value="false" /> | ||||
|     <version value="1.0" /> | ||||
|   </settings> | ||||
| </component> | ||||
							
								
								
									
										4
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.idea/misc.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (txet-classify-ui)" project-jdk-type="Python SDK" /> | ||||
| </project> | ||||
							
								
								
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.idea/modules.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="ProjectModuleManager"> | ||||
|     <modules> | ||||
|       <module fileurl="file://$PROJECT_DIR$/.idea/txet-classify-ui.iml" filepath="$PROJECT_DIR$/.idea/txet-classify-ui.iml" /> | ||||
|     </modules> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										6
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <project version="4"> | ||||
|   <component name="SqlDialectMappings"> | ||||
|     <file url="file://$PROJECT_DIR$/database/schema.sql" dialect="GenericSQL" /> | ||||
|   </component> | ||||
| </project> | ||||
							
								
								
									
										17
									
								
								.idea/txet-classify-ui.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								.idea/txet-classify-ui.iml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <module type="PYTHON_MODULE" version="4"> | ||||
|   <component name="NewModuleRootManager"> | ||||
|     <content url="file://$MODULE_DIR$"> | ||||
|       <excludeFolder url="file://$MODULE_DIR$/venv/lib/python3.10/site-packages" /> | ||||
|     </content> | ||||
|     <orderEntry type="inheritedJdk" /> | ||||
|     <orderEntry type="sourceFolder" forTests="false" /> | ||||
|   </component> | ||||
|   <component name="TemplatesService"> | ||||
|     <option name="TEMPLATE_FOLDERS"> | ||||
|       <list> | ||||
|         <option value="$MODULE_DIR$/templates" /> | ||||
|       </list> | ||||
|     </option> | ||||
|   </component> | ||||
| </module> | ||||
							
								
								
									
										0
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										64
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| from flask import Flask, jsonify, render_template, session, redirect, url_for, request | ||||
| import os | ||||
| import logging | ||||
| import config | ||||
| from routes import register_blueprints | ||||
| from utils.model_service import text_classifier | ||||
| 
 | ||||
| # 设置日志 | ||||
| logging.basicConfig( | ||||
|     level=logging.INFO, | ||||
|     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' | ||||
| ) | ||||
| 
 | ||||
| # 创建Flask应用 | ||||
| app = Flask(__name__) | ||||
| app.config.from_object(config) | ||||
| app.secret_key = config.SECRET_KEY | ||||
| 
 | ||||
| # 确保上传目录和临时目录存在 | ||||
| os.makedirs(os.path.join(app.root_path, config.UPLOAD_FOLDER), exist_ok=True) | ||||
| os.makedirs(os.path.join(app.root_path, 'temp'), exist_ok=True) | ||||
| 
 | ||||
| # 注册所有路由蓝图 | ||||
| register_blueprints(app) | ||||
| 
 | ||||
| # 初始化文本分类模型 | ||||
| @app.before_first_request | ||||
| def initialize_model(): | ||||
|     if not text_classifier.initialize(): | ||||
|         app.logger.error("模型初始化失败!") | ||||
|     else: | ||||
|         app.logger.info("模型初始化成功!") | ||||
| 
 | ||||
| @app.route('/') | ||||
| def index(): | ||||
|     # 检查用户是否已登录 | ||||
|     if 'user_id' in session: | ||||
|         return redirect(url_for('dashboard')) | ||||
|     return render_template('login.html') | ||||
| 
 | ||||
| @app.route('/register') | ||||
| def register_page(): | ||||
|     return render_template('register.html') | ||||
| 
 | ||||
| @app.route('/dashboard') | ||||
| def dashboard(): | ||||
|     # 检查用户是否已登录 | ||||
|     if 'user_id' not in session: | ||||
|         return redirect(url_for('index')) | ||||
|     return render_template('dashboard.html', user=session.get('user_info')) | ||||
| 
 | ||||
| # 404 错误处理 | ||||
| @app.errorhandler(404) | ||||
| def page_not_found(e): | ||||
|     return render_template('error.html', error='页面未找到'), 404 | ||||
| 
 | ||||
| # 500 错误处理 | ||||
| @app.errorhandler(500) | ||||
| def internal_server_error(e): | ||||
|     return render_template('error.html', error='服务器内部错误'), 500 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     app.run(debug=True, host='0.0.0.0', port=5010) | ||||
| 
 | ||||
							
								
								
									
										35
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | ||||
| import os | ||||
| from datetime import timedelta | ||||
| 
 | ||||
| # Flask 配置 | ||||
| SECRET_KEY = 'c8e7b92a46d64b1f9a5d98c5e17f9e76d2c35a1b4e0f78c9d2b1a3e5f7d9e0b'  # 随机生成的强密钥 | ||||
| DEBUG = True | ||||
| 
 | ||||
| # 数据库配置 | ||||
| DB_HOST = '27.124.22.104' | ||||
| DB_USER = 'ziyao'  # 更改为你的MySQL用户名 | ||||
| DB_PASSWORD = 'ziyao123'  # 更改为你的MySQL密码 | ||||
| DB_NAME = 'text_classification_system' | ||||
| 
 | ||||
| # 邮件配置 | ||||
| EMAIL_HOST = 'mail.sq0715.com' | ||||
| EMAIL_PORT = 587 | ||||
| EMAIL_USERNAME = 'vip@sq0715.com' | ||||
| EMAIL_PASSWORD = 'Lsq12350501.' | ||||
| EMAIL_FROM = 'vip@sq0715.com' | ||||
| EMAIL_FROM_NAME = 'QINAI_OFFICIAL' | ||||
| EMAIL_USE_TLS = True | ||||
| 
 | ||||
| # 会话配置 | ||||
| PERMANENT_SESSION_LIFETIME = timedelta(days=7) | ||||
| 
 | ||||
| # 文件上传配置 | ||||
| UPLOAD_FOLDER = 'uploads' | ||||
| MAX_CONTENT_LENGTH = 10 * 1024 * 1024  # 最大上传文件大小限制为10MB | ||||
| 
 | ||||
| # 允许的文件扩展名 | ||||
| ALLOWED_TEXT_EXTENSIONS = {'txt'}  # 文本分类允许的文件类型 | ||||
| ALLOWED_ARCHIVE_EXTENSIONS = {'zip', 'rar'}  # 批量分类允许的压缩文件类型 | ||||
| 
 | ||||
| # 确保上传文件夹存在 | ||||
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | ||||
							
								
								
									
										64
									
								
								database/schema.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								database/schema.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| -- 创建数据库 | ||||
| CREATE DATABASE IF NOT EXISTS text_classification_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; | ||||
| 
 | ||||
| USE text_classification_system; | ||||
| 
 | ||||
| -- 创建用户表 | ||||
| CREATE TABLE users ( | ||||
|     id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|     email VARCHAR(100) NOT NULL UNIQUE COMMENT '用户邮箱,作为登录用户名', | ||||
|     password VARCHAR(255) NOT NULL COMMENT '加密后的密码', | ||||
|     name VARCHAR(50) COMMENT '用户姓名', | ||||
|     gender ENUM('男', '女', '其他') COMMENT '性别', | ||||
|     birth_date DATE COMMENT '出生日期', | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '账户创建时间', | ||||
|     updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '账户更新时间', | ||||
|     INDEX idx_email (email) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户信息表'; | ||||
| 
 | ||||
| -- 创建分类类别表 | ||||
| CREATE TABLE categories ( | ||||
|     id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|     name VARCHAR(50) NOT NULL UNIQUE COMMENT '类别名称', | ||||
|     description TEXT COMMENT '类别描述', | ||||
|     created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||||
|     INDEX idx_name (name) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文本分类类别表'; | ||||
| 
 | ||||
| -- 创建文档表 | ||||
| CREATE TABLE documents ( | ||||
|     id INT AUTO_INCREMENT PRIMARY KEY, | ||||
|     user_id INT NOT NULL COMMENT '上传用户ID', | ||||
|     original_filename VARCHAR(255) NOT NULL COMMENT '原始文件名', | ||||
|     stored_filename VARCHAR(255) NOT NULL COMMENT '存储的文件名', | ||||
|     file_path VARCHAR(255) NOT NULL COMMENT '文件存储路径', | ||||
|     file_size INT COMMENT '文件大小(字节)', | ||||
|     content_summary TEXT COMMENT '内容摘要', | ||||
|     category_id INT COMMENT '分类类别ID', | ||||
|     upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', | ||||
|     classified_time TIMESTAMP NULL COMMENT '分类完成时间', | ||||
|     status ENUM('待处理', '处理中', '已分类', '处理失败') DEFAULT '待处理' COMMENT '处理状态', | ||||
|     FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, | ||||
|     FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, | ||||
|     INDEX idx_user_id (user_id), | ||||
|     INDEX idx_category_id (category_id), | ||||
|     INDEX idx_status (status) | ||||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户上传文档表'; | ||||
| 
 | ||||
| -- 插入默认的分类类别数据 | ||||
| INSERT INTO categories (name, description) VALUES | ||||
| ('体育', '体育相关新闻和内容'), | ||||
| ('娱乐', '娱乐相关新闻和内容'), | ||||
| ('家居', '家居相关新闻和内容'), | ||||
| ('彩票', '彩票相关新闻和内容'), | ||||
| ('房产', '房产相关新闻和内容'), | ||||
| ('教育', '教育相关新闻和内容'), | ||||
| ('时尚', '时尚相关新闻和内容'), | ||||
| ('时政', '时政相关新闻和内容'), | ||||
| ('星座', '星座相关新闻和内容'), | ||||
| ('游戏', '游戏相关新闻和内容'), | ||||
| ('社会', '社会相关新闻和内容'), | ||||
| ('科技', '科技相关新闻和内容'), | ||||
| ('股票', '股票相关新闻和内容'), | ||||
| ('财经', '财经相关新闻和内容'); | ||||
| 
 | ||||
							
								
								
									
										0
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										28
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| # 核心框架 | ||||
| Flask==2.0.1 | ||||
| Werkzeug==2.0.1 | ||||
| mysql-connector-python==8.0.27 | ||||
| bcrypt==3.2.0 | ||||
| 
 | ||||
| # 模型和数据处理 - 系统适配版本 | ||||
| tensorflow-macos>=2.9.0; platform_system=="Darwin" and platform_machine=="arm64"  # Mac M1/M2 | ||||
| tensorflow-cpu>=2.9.0; platform_system!="Darwin" or platform_machine!="arm64"    # 其他系统 | ||||
| # tensorflow-gpu>=2.9.0  # 如果有GPU,可以取消此注释 | ||||
| 
 | ||||
| # 基础数据处理 | ||||
| numpy>=1.20.0 | ||||
| jieba==0.42.1 | ||||
| scikit-learn>=1.0.0 | ||||
| 
 | ||||
| # 文件处理 | ||||
| rarfile==4.0 | ||||
| python-dateutil==2.8.2 | ||||
| 
 | ||||
| # 邮件服务 | ||||
| Flask-Mail==0.9.1 | ||||
| 
 | ||||
| # 工具类 | ||||
| python-dotenv==0.20.0 | ||||
| 
 | ||||
| # 生产部署 | ||||
| gunicorn==20.1.0 | ||||
							
								
								
									
										11
									
								
								routes/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								routes/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| from flask import Blueprint | ||||
| 
 | ||||
| # 导入各个路由模块 | ||||
| from routes.auth import auth | ||||
| # 后续会添加其他路由模块 | ||||
| 
 | ||||
| # 创建注册蓝图的函数 | ||||
| def register_blueprints(app): | ||||
|     """在Flask应用中注册所有蓝图""" | ||||
|     # 认证相关路由,使用/auth前缀 | ||||
|     app.register_blueprint(auth, url_prefix='/auth') | ||||
							
								
								
									
										231
									
								
								routes/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								routes/auth.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,231 @@ | ||||
| from flask import Blueprint, request, jsonify, session | ||||
| import random | ||||
| import string | ||||
| import re | ||||
| from datetime import datetime, timedelta | ||||
| from utils.db import get_db_connection, close_connection | ||||
| from utils.password import hash_password | ||||
| from utils.email_sender import send_verification_email | ||||
| from utils.password import hash_password, check_password | ||||
| 
 | ||||
| 
 | ||||
| # 创建蓝图 | ||||
| auth = Blueprint('auth', __name__) | ||||
| 
 | ||||
| # 存储临时验证码 | ||||
| verification_codes = {} | ||||
| 
 | ||||
| 
 | ||||
| @auth.route('/register', methods=['POST']) | ||||
| def register(): | ||||
|     """用户注册接口""" | ||||
|     data = request.get_json() | ||||
| 
 | ||||
|     # 检查必要字段 | ||||
|     if not all(k in data for k in ('email', 'password', 'name')): | ||||
|         return jsonify({'success': False, 'message': '缺少必要的注册信息'}), 400 | ||||
| 
 | ||||
|     email = data['email'] | ||||
|     password = data['password'] | ||||
|     name = data['name'] | ||||
|     gender = data.get('gender') | ||||
|     birth_date = data.get('birth_date') | ||||
| 
 | ||||
|     # 验证邮箱格式 | ||||
|     email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' | ||||
|     if not re.match(email_pattern, email): | ||||
|         return jsonify({'success': False, 'message': '邮箱格式不正确'}), 400 | ||||
| 
 | ||||
|     # 验证密码强度 | ||||
|     if len(password) < 8: | ||||
|         return jsonify({'success': False, 'message': '密码长度必须至少为8个字符'}), 400 | ||||
| 
 | ||||
|     # 检查邮箱是否已注册 | ||||
|     conn = get_db_connection() | ||||
|     if not conn: | ||||
|         return jsonify({'success': False, 'message': '数据库连接失败'}), 500 | ||||
| 
 | ||||
|     cursor = conn.cursor(dictionary=True) | ||||
|     cursor.execute("SELECT id FROM users WHERE email = %s", (email,)) | ||||
|     existing_user = cursor.fetchone() | ||||
| 
 | ||||
|     if existing_user: | ||||
|         cursor.close() | ||||
|         close_connection(conn) | ||||
|         return jsonify({'success': False, 'message': '该邮箱已被注册'}), 400 | ||||
| 
 | ||||
|     # 生成验证码 | ||||
|     verification_code = ''.join(random.choices(string.digits, k=6)) | ||||
| 
 | ||||
|     # 存储验证码(实际项目中应使用Redis等带过期功能的存储) | ||||
|     verification_codes[email] = { | ||||
|         'code': verification_code, | ||||
|         'expires': datetime.now() + timedelta(minutes=30), | ||||
|         'user_data': { | ||||
|             'email': email, | ||||
|             'password': password, | ||||
|             'name': name, | ||||
|             'gender': gender, | ||||
|             'birth_date': birth_date | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     # 发送验证邮件 | ||||
|     email_sent = send_verification_email(email, verification_code) | ||||
| 
 | ||||
|     cursor.close() | ||||
|     close_connection(conn) | ||||
| 
 | ||||
|     if not email_sent: | ||||
|         return jsonify({'success': False, 'message': '验证邮件发送失败'}), 500 | ||||
| 
 | ||||
|     return jsonify({ | ||||
|         'success': True, | ||||
|         'message': '验证码已发送到您的邮箱,请查收并完成注册' | ||||
|     }) | ||||
| 
 | ||||
| 
 | ||||
| @auth.route('/verify', methods=['POST']) | ||||
| def verify(): | ||||
|     """验证邮箱验证码""" | ||||
|     data = request.get_json() | ||||
| 
 | ||||
|     if not all(k in data for k in ('email', 'code')): | ||||
|         return jsonify({'success': False, 'message': '缺少必要的验证信息'}), 400 | ||||
| 
 | ||||
|     email = data['email'] | ||||
|     code = data['code'] | ||||
| 
 | ||||
|     # 检查验证码 | ||||
|     if email not in verification_codes: | ||||
|         return jsonify({'success': False, 'message': '验证码不存在或已过期'}), 400 | ||||
| 
 | ||||
|     stored_data = verification_codes[email] | ||||
| 
 | ||||
|     # 检查验证码是否过期 | ||||
|     if datetime.now() > stored_data['expires']: | ||||
|         del verification_codes[email] | ||||
|         return jsonify({'success': False, 'message': '验证码已过期'}), 400 | ||||
| 
 | ||||
|     # 检查验证码是否匹配 | ||||
|     if code != stored_data['code']: | ||||
|         return jsonify({'success': False, 'message': '验证码不正确'}), 400 | ||||
| 
 | ||||
|     # 获取用户数据 | ||||
|     user_data = stored_data['user_data'] | ||||
| 
 | ||||
|     # 处理空日期值 | ||||
|     birth_date = user_data.get('birth_date') | ||||
|     if birth_date == '' or not birth_date: | ||||
|         birth_date = None | ||||
| 
 | ||||
|     # 存储用户信息到数据库 | ||||
|     conn = get_db_connection() | ||||
|     if not conn: | ||||
|         return jsonify({'success': False, 'message': '数据库连接失败'}), 500 | ||||
| 
 | ||||
|     cursor = conn.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         # 哈希处理密码 | ||||
|         hashed_password = hash_password(user_data['password']) | ||||
| 
 | ||||
|         # 插入用户数据 | ||||
|         query = """ | ||||
|         INSERT INTO users (email, password, name, gender, birth_date) | ||||
|         VALUES (%s, %s, %s, %s, %s) | ||||
|         """ | ||||
|         cursor.execute(query, ( | ||||
|             user_data['email'], | ||||
|             hashed_password, | ||||
|             user_data['name'], | ||||
|             user_data['gender'], | ||||
|             birth_date  # 使用处理过的birth_date值 | ||||
|         )) | ||||
| 
 | ||||
|         conn.commit() | ||||
| 
 | ||||
|         # 清除验证码 | ||||
|         del verification_codes[email] | ||||
| 
 | ||||
|         return jsonify({ | ||||
|             'success': True, | ||||
|             'message': '注册成功,请登录系统' | ||||
|         }) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         conn.rollback() | ||||
|         return jsonify({'success': False, 'message': f'注册失败: {str(e)}'}), 500 | ||||
| 
 | ||||
|     finally: | ||||
|         cursor.close() | ||||
|         close_connection(conn) | ||||
| 
 | ||||
| 
 | ||||
| # 在routes/auth.py现有代码的底部添加 | ||||
| 
 | ||||
| @auth.route('/login', methods=['POST']) | ||||
| def login(): | ||||
|     """用户登录接口""" | ||||
|     data = request.get_json() | ||||
| 
 | ||||
|     if not all(k in data for k in ('email', 'password')): | ||||
|         return jsonify({'success': False, 'message': '请输入邮箱和密码'}), 400 | ||||
| 
 | ||||
|     email = data['email'] | ||||
|     password = data['password'] | ||||
| 
 | ||||
|     # 连接数据库 | ||||
|     conn = get_db_connection() | ||||
|     if not conn: | ||||
|         return jsonify({'success': False, 'message': '数据库连接失败'}), 500 | ||||
| 
 | ||||
|     cursor = conn.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         # 查询用户 | ||||
|         cursor.execute("SELECT id, email, password, name FROM users WHERE email = %s", (email,)) | ||||
|         user = cursor.fetchone() | ||||
| 
 | ||||
|         if not user: | ||||
|             return jsonify({'success': False, 'message': '用户不存在'}), 404 | ||||
| 
 | ||||
|         # 验证密码 | ||||
|         if not check_password(user['password'], password): | ||||
|             return jsonify({'success': False, 'message': '密码错误'}), 401 | ||||
| 
 | ||||
|         # 创建会话 | ||||
|         session['user_id'] = user['id'] | ||||
|         session['user_info'] = { | ||||
|             'id': user['id'], | ||||
|             'email': user['email'], | ||||
|             'name': user['name'] | ||||
|         } | ||||
| 
 | ||||
|         return jsonify({ | ||||
|             'success': True, | ||||
|             'message': '登录成功', | ||||
|             'user': { | ||||
|                 'id': user['id'], | ||||
|                 'email': user['email'], | ||||
|                 'name': user['name'] | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         print(f"登录错误: {str(e)}")  # 添加错误日志 | ||||
|         return jsonify({'success': False, 'message': f'登录失败: {str(e)}'}), 500 | ||||
| 
 | ||||
|     finally: | ||||
|         cursor.close() | ||||
|         close_connection(conn) | ||||
| 
 | ||||
| 
 | ||||
| @auth.route('/logout') | ||||
| def logout(): | ||||
|     """用户登出接口""" | ||||
|     from flask import session | ||||
|     # 清除会话 | ||||
|     session.pop('user_id', None) | ||||
|     session.pop('user_info', None) | ||||
|     return jsonify({'success': True, 'message': '已成功登出'}) | ||||
							
								
								
									
										659
									
								
								routes/classify.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										659
									
								
								routes/classify.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,659 @@ | ||||
| # routes/classify.py | ||||
| import os | ||||
| import time | ||||
| import uuid | ||||
| import zipfile | ||||
| import rarfile | ||||
| import shutil | ||||
| from flask import Blueprint, request, jsonify, current_app, send_file, g, session | ||||
| from werkzeug.utils import secure_filename | ||||
| import mysql.connector | ||||
| from utils.model_service import text_classifier | ||||
| from utils.db import get_db, close_db | ||||
| import logging | ||||
| 
 | ||||
| # 创建蓝图 | ||||
| classify_bp = Blueprint('classify', __name__, url_prefix='/api/classify') | ||||
| 
 | ||||
| # 设置日志 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| # 允许的文件扩展名 | ||||
| ALLOWED_TEXT_EXTENSIONS = {'txt'} | ||||
| ALLOWED_ARCHIVE_EXTENSIONS = {'zip', 'rar'} | ||||
| 
 | ||||
| 
 | ||||
| def allowed_text_file(filename): | ||||
|     """检查文本文件扩展名是否允许""" | ||||
|     return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_TEXT_EXTENSIONS | ||||
| 
 | ||||
| 
 | ||||
| def allowed_archive_file(filename): | ||||
|     """检查压缩文件扩展名是否允许""" | ||||
|     return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_ARCHIVE_EXTENSIONS | ||||
| 
 | ||||
| 
 | ||||
| def get_next_document_number(category): | ||||
|     """获取给定类别的下一个文档编号 | ||||
| 
 | ||||
|     Args: | ||||
|         category (str): 文档类别 | ||||
| 
 | ||||
|     Returns: | ||||
|         int: 下一个编号 | ||||
|     """ | ||||
|     db = get_db() | ||||
|     cursor = db.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         # 查询该类别的最大编号 | ||||
|         query = """ | ||||
|         SELECT MAX(CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(stored_filename, '-', 2), '-', -1) AS UNSIGNED)) as max_num | ||||
|         FROM documents | ||||
|         WHERE stored_filename LIKE %s | ||||
|         """ | ||||
|         cursor.execute(query, (f"{category}-%",)) | ||||
|         result = cursor.fetchone() | ||||
| 
 | ||||
|         # 如果没有记录,或者最大编号为None,返回1,否则返回最大编号+1 | ||||
|         if not result or result['max_num'] is None: | ||||
|             return 1 | ||||
|         else: | ||||
|             return result['max_num'] + 1 | ||||
|     except Exception as e: | ||||
|         logger.error(f"获取下一个文档编号时出错: {str(e)}") | ||||
|         return 1 | ||||
|     finally: | ||||
|         cursor.close() | ||||
| 
 | ||||
| 
 | ||||
| def save_classified_document(user_id, original_filename, category, content, file_size=None): | ||||
|     """保存分类后的文档 | ||||
| 
 | ||||
|     Args: | ||||
|         user_id (int): 用户ID | ||||
|         original_filename (str): 原始文件名 | ||||
|         category (str): 分类类别 | ||||
|         content (str): 文档内容 | ||||
|         file_size (int, optional): 文件大小,如果为None则自动计算 | ||||
| 
 | ||||
|     Returns: | ||||
|         tuple: (成功标志, 存储的文件名或错误信息) | ||||
|     """ | ||||
|     try: | ||||
|         # 获取下一个文档编号 | ||||
|         next_num = get_next_document_number(category) | ||||
| 
 | ||||
|         # 安全处理文件名 | ||||
|         safe_original_name = secure_filename(original_filename) | ||||
| 
 | ||||
|         # 生成新文件名 (类别-编号-原文件名) | ||||
|         formatted_num = f"{next_num:04d}"  # 确保编号格式为4位数 | ||||
|         new_filename = f"{category}-{formatted_num}-{safe_original_name}" | ||||
| 
 | ||||
|         # 确保uploads目录存在 | ||||
|         uploads_dir = os.path.join(current_app.root_path, current_app.config['UPLOAD_FOLDER']) | ||||
|         category_dir = os.path.join(uploads_dir, category) | ||||
|         os.makedirs(category_dir, exist_ok=True) | ||||
| 
 | ||||
|         # 文件完整路径 | ||||
|         file_path = os.path.join(category_dir, new_filename) | ||||
| 
 | ||||
|         # 保存文件 | ||||
|         with open(file_path, 'w', encoding='utf-8') as f: | ||||
|             f.write(content) | ||||
| 
 | ||||
|         # 如果文件大小未提供,则计算 | ||||
|         if file_size is None: | ||||
|             file_size = os.path.getsize(file_path) | ||||
| 
 | ||||
|         # 获取db连接 | ||||
|         db = get_db() | ||||
|         cursor = db.cursor() | ||||
| 
 | ||||
|         # 获取分类类别ID | ||||
|         category_query = "SELECT id FROM categories WHERE name = %s" | ||||
|         cursor.execute(category_query, (category,)) | ||||
|         category_result = cursor.fetchone() | ||||
| 
 | ||||
|         if not category_result: | ||||
|             return False, "类别不存在" | ||||
| 
 | ||||
|         category_id = category_result[0] | ||||
| 
 | ||||
|         # 插入数据库记录 | ||||
|         insert_query = """ | ||||
|         INSERT INTO documents  | ||||
|         (user_id, original_filename, stored_filename, file_path, file_size, category_id, status, classified_time) | ||||
|         VALUES (%s, %s, %s, %s, %s, %s, %s, NOW()) | ||||
|         """ | ||||
| 
 | ||||
|         cursor.execute( | ||||
|             insert_query, | ||||
|             (user_id, original_filename, new_filename, file_path, file_size, category_id, '已分类') | ||||
|         ) | ||||
| 
 | ||||
|         # 提交事务 | ||||
|         db.commit() | ||||
| 
 | ||||
|         return True, new_filename | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         logger.error(f"保存分类文档时出错: {str(e)}") | ||||
|         return False, str(e) | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/single', methods=['POST']) | ||||
| def classify_single_file(): | ||||
|     """单文件上传和分类API""" | ||||
|     # 检查用户是否登录 | ||||
|     if 'user_id' not in session: | ||||
|         return jsonify({"success": False, "error": "请先登录"}), 401 | ||||
| 
 | ||||
|     user_id = session['user_id'] | ||||
| 
 | ||||
|     # 检查是否上传了文件 | ||||
|     if 'file' not in request.files: | ||||
|         return jsonify({"success": False, "error": "没有文件"}), 400 | ||||
| 
 | ||||
|     file = request.files['file'] | ||||
| 
 | ||||
|     # 检查文件名 | ||||
|     if file.filename == '': | ||||
|         return jsonify({"success": False, "error": "未选择文件"}), 400 | ||||
| 
 | ||||
|     # 检查文件类型 | ||||
|     if not allowed_text_file(file.filename): | ||||
|         return jsonify({"success": False, "error": "不支持的文件类型,仅支持txt文件"}), 400 | ||||
| 
 | ||||
|     try: | ||||
|         # 创建临时文件以供处理 | ||||
|         temp_dir = os.path.join(current_app.root_path, 'temp') | ||||
|         os.makedirs(temp_dir, exist_ok=True) | ||||
| 
 | ||||
|         temp_filename = f"{uuid.uuid4().hex}.txt" | ||||
|         temp_path = os.path.join(temp_dir, temp_filename) | ||||
| 
 | ||||
|         # 保存上传文件到临时位置 | ||||
|         file.save(temp_path) | ||||
| 
 | ||||
|         # 读取文件内容 | ||||
|         with open(temp_path, 'r', encoding='utf-8') as f: | ||||
|             file_content = f.read() | ||||
| 
 | ||||
|         # 调用模型进行分类 | ||||
|         result = text_classifier.classify_text(file_content) | ||||
| 
 | ||||
|         if not result['success']: | ||||
|             return jsonify({"success": False, "error": result['error']}), 500 | ||||
| 
 | ||||
|         # 保存分类后的文档 | ||||
|         file_size = os.path.getsize(temp_path) | ||||
|         save_success, message = save_classified_document( | ||||
|             user_id, | ||||
|             file.filename, | ||||
|             result['category'], | ||||
|             file_content, | ||||
|             file_size | ||||
|         ) | ||||
| 
 | ||||
|         # 清理临时文件 | ||||
|         if os.path.exists(temp_path): | ||||
|             os.remove(temp_path) | ||||
| 
 | ||||
|         if not save_success: | ||||
|             return jsonify({"success": False, "error": f"保存文档失败: {message}"}), 500 | ||||
| 
 | ||||
|         # 返回分类结果 | ||||
|         return jsonify({ | ||||
|             "success": True, | ||||
|             "filename": file.filename, | ||||
|             "category": result['category'], | ||||
|             "confidence": result['confidence'], | ||||
|             "stored_filename": message | ||||
|         }) | ||||
| 
 | ||||
|     except UnicodeDecodeError: | ||||
|         # 尝试GBK编码 | ||||
|         try: | ||||
|             with open(temp_path, 'r', encoding='gbk') as f: | ||||
|                 file_content = f.read() | ||||
| 
 | ||||
|             # 调用模型进行分类 | ||||
|             result = text_classifier.classify_text(file_content) | ||||
| 
 | ||||
|             if not result['success']: | ||||
|                 return jsonify({"success": False, "error": result['error']}), 500 | ||||
| 
 | ||||
|             # 保存分类后的文档 | ||||
|             file_size = os.path.getsize(temp_path) | ||||
|             save_success, message = save_classified_document( | ||||
|                 user_id, | ||||
|                 file.filename, | ||||
|                 result['category'], | ||||
|                 file_content, | ||||
|                 file_size | ||||
|             ) | ||||
| 
 | ||||
|             # 清理临时文件 | ||||
|             if os.path.exists(temp_path): | ||||
|                 os.remove(temp_path) | ||||
| 
 | ||||
|             if not save_success: | ||||
|                 return jsonify({"success": False, "error": f"保存文档失败: {message}"}), 500 | ||||
| 
 | ||||
|             # 返回分类结果 | ||||
|             return jsonify({ | ||||
|                 "success": True, | ||||
|                 "filename": file.filename, | ||||
|                 "category": result['category'], | ||||
|                 "confidence": result['confidence'], | ||||
|                 "stored_filename": message | ||||
|             }) | ||||
|         except Exception as e: | ||||
|             if os.path.exists(temp_path): | ||||
|                 os.remove(temp_path) | ||||
|             return jsonify({"success": False, "error": f"文件编码错误,请确保文件为UTF-8或GBK编码: {str(e)}"}), 400 | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         # 确保清理临时文件 | ||||
|         if 'temp_path' in locals() and os.path.exists(temp_path): | ||||
|             os.remove(temp_path) | ||||
|         logger.error(f"文件处理过程中发生错误: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"文件处理错误: {str(e)}"}), 500 | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/batch', methods=['POST']) | ||||
| def classify_batch_files(): | ||||
|     """批量文件上传和分类API(压缩包处理)""" | ||||
|     # 检查用户是否登录 | ||||
|     if 'user_id' not in session: | ||||
|         return jsonify({"success": False, "error": "请先登录"}), 401 | ||||
| 
 | ||||
|     user_id = session['user_id'] | ||||
| 
 | ||||
|     # 检查是否上传了文件 | ||||
|     if 'file' not in request.files: | ||||
|         return jsonify({"success": False, "error": "没有文件"}), 400 | ||||
| 
 | ||||
|     file = request.files['file'] | ||||
| 
 | ||||
|     # 检查文件名 | ||||
|     if file.filename == '': | ||||
|         return jsonify({"success": False, "error": "未选择文件"}), 400 | ||||
| 
 | ||||
|     # 检查文件类型 | ||||
|     if not allowed_archive_file(file.filename): | ||||
|         return jsonify({"success": False, "error": "不支持的文件类型,仅支持zip和rar压缩文件"}), 400 | ||||
| 
 | ||||
|     # 检查文件大小 | ||||
|     if request.content_length > 10 * 1024 * 1024:  # 10MB | ||||
|         return jsonify({"success": False, "error": "文件太大,最大支持10MB"}), 400 | ||||
| 
 | ||||
|     try: | ||||
|         # 创建临时目录 | ||||
|         temp_dir = os.path.join(current_app.root_path, 'temp') | ||||
|         extract_dir = os.path.join(temp_dir, f"extract_{uuid.uuid4().hex}") | ||||
|         os.makedirs(extract_dir, exist_ok=True) | ||||
| 
 | ||||
|         # 保存上传的压缩文件 | ||||
|         archive_path = os.path.join(temp_dir, secure_filename(file.filename)) | ||||
|         file.save(archive_path) | ||||
| 
 | ||||
|         # 解压文件 | ||||
|         file_extension = file.filename.rsplit('.', 1)[1].lower() | ||||
|         if file_extension == 'zip': | ||||
|             with zipfile.ZipFile(archive_path, 'r') as zip_ref: | ||||
|                 zip_ref.extractall(extract_dir) | ||||
|         elif file_extension == 'rar': | ||||
|             with rarfile.RarFile(archive_path, 'r') as rar_ref: | ||||
|                 rar_ref.extractall(extract_dir) | ||||
| 
 | ||||
|         # 处理结果统计 | ||||
|         results = { | ||||
|             "total": 0, | ||||
|             "success": 0, | ||||
|             "failed": 0, | ||||
|             "categories": {}, | ||||
|             "failed_files": [] | ||||
|         } | ||||
| 
 | ||||
|         # 递归处理所有txt文件 | ||||
|         for root, dirs, files in os.walk(extract_dir): | ||||
|             for filename in files: | ||||
|                 if filename.lower().endswith('.txt'): | ||||
|                     file_path = os.path.join(root, filename) | ||||
|                     results["total"] += 1 | ||||
| 
 | ||||
|                     try: | ||||
|                         # 读取文件内容 | ||||
|                         try: | ||||
|                             with open(file_path, 'r', encoding='utf-8') as f: | ||||
|                                 file_content = f.read() | ||||
|                         except UnicodeDecodeError: | ||||
|                             # 尝试GBK编码 | ||||
|                             with open(file_path, 'r', encoding='gbk') as f: | ||||
|                                 file_content = f.read() | ||||
| 
 | ||||
|                         # 调用模型进行分类 | ||||
|                         result = text_classifier.classify_text(file_content) | ||||
| 
 | ||||
|                         if not result['success']: | ||||
|                             results["failed"] += 1 | ||||
|                             results["failed_files"].append({ | ||||
|                                 "filename": filename, | ||||
|                                 "error": result['error'] | ||||
|                             }) | ||||
|                             continue | ||||
| 
 | ||||
|                         category = result['category'] | ||||
| 
 | ||||
|                         # 统计类别数量 | ||||
|                         if category not in results["categories"]: | ||||
|                             results["categories"][category] = 0 | ||||
|                         results["categories"][category] += 1 | ||||
| 
 | ||||
|                         # 保存分类后的文档 | ||||
|                         file_size = os.path.getsize(file_path) | ||||
|                         save_success, message = save_classified_document( | ||||
|                             user_id, | ||||
|                             filename, | ||||
|                             category, | ||||
|                             file_content, | ||||
|                             file_size | ||||
|                         ) | ||||
| 
 | ||||
|                         if save_success: | ||||
|                             results["success"] += 1 | ||||
|                         else: | ||||
|                             results["failed"] += 1 | ||||
|                             results["failed_files"].append({ | ||||
|                                 "filename": filename, | ||||
|                                 "error": message | ||||
|                             }) | ||||
| 
 | ||||
|                     except Exception as e: | ||||
|                         results["failed"] += 1 | ||||
|                         results["failed_files"].append({ | ||||
|                             "filename": filename, | ||||
|                             "error": str(e) | ||||
|                         }) | ||||
| 
 | ||||
|         # 清理临时文件 | ||||
|         if os.path.exists(archive_path): | ||||
|             os.remove(archive_path) | ||||
|         if os.path.exists(extract_dir): | ||||
|             shutil.rmtree(extract_dir) | ||||
| 
 | ||||
|         # 返回处理结果 | ||||
|         return jsonify({ | ||||
|             "success": True, | ||||
|             "archive_name": file.filename, | ||||
|             "results": results | ||||
|         }) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         # 确保清理临时文件 | ||||
|         if 'archive_path' in locals() and os.path.exists(archive_path): | ||||
|             os.remove(archive_path) | ||||
|         if 'extract_dir' in locals() and os.path.exists(extract_dir): | ||||
|             shutil.rmtree(extract_dir) | ||||
| 
 | ||||
|         logger.error(f"压缩包处理过程中发生错误: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"压缩包处理错误: {str(e)}"}), 500 | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/documents', methods=['GET']) | ||||
| def get_classified_documents(): | ||||
|     """获取已分类的文档列表""" | ||||
|     # 检查用户是否登录 | ||||
|     if 'user_id' not in session: | ||||
|         return jsonify({"success": False, "error": "请先登录"}), 401 | ||||
| 
 | ||||
|     user_id = session['user_id'] | ||||
| 
 | ||||
|     # 获取查询参数 | ||||
|     category = request.args.get('category', 'all') | ||||
|     page = int(request.args.get('page', 1)) | ||||
|     per_page = int(request.args.get('per_page', 10)) | ||||
| 
 | ||||
|     # 验证每页条数 | ||||
|     if per_page not in [10, 25, 50, 100]: | ||||
|         per_page = 10 | ||||
| 
 | ||||
|     # 计算偏移量 | ||||
|     offset = (page - 1) * per_page | ||||
| 
 | ||||
|     db = get_db() | ||||
|     cursor = db.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         # 构建查询条件 | ||||
|         where_clause = "WHERE d.user_id = %s AND d.status = '已分类'" | ||||
|         params = [user_id] | ||||
| 
 | ||||
|         if category != 'all': | ||||
|             where_clause += " AND c.name = %s" | ||||
|             params.append(category) | ||||
| 
 | ||||
|         # 查询总记录数 | ||||
|         count_query = f""" | ||||
|         SELECT COUNT(*) as total | ||||
|         FROM documents d | ||||
|         JOIN categories c ON d.category_id = c.id | ||||
|         {where_clause} | ||||
|         """ | ||||
|         cursor.execute(count_query, params) | ||||
|         total_count = cursor.fetchone()['total'] | ||||
| 
 | ||||
|         # 计算总页数 | ||||
|         total_pages = (total_count + per_page - 1) // per_page | ||||
| 
 | ||||
|         # 查询分页数据 | ||||
|         query = f""" | ||||
|         SELECT d.id, d.original_filename, d.stored_filename, d.file_size,  | ||||
|                c.name as category, d.upload_time, d.classified_time | ||||
|         FROM documents d | ||||
|         JOIN categories c ON d.category_id = c.id | ||||
|         {where_clause} | ||||
|         ORDER BY d.classified_time DESC | ||||
|         LIMIT %s OFFSET %s | ||||
|         """ | ||||
|         params.extend([per_page, offset]) | ||||
|         cursor.execute(query, params) | ||||
|         documents = cursor.fetchall() | ||||
| 
 | ||||
|         # 获取所有可用类别 | ||||
|         cursor.execute("SELECT name FROM categories ORDER BY name") | ||||
|         categories = [row['name'] for row in cursor.fetchall()] | ||||
| 
 | ||||
|         return jsonify({ | ||||
|             "success": True, | ||||
|             "documents": documents, | ||||
|             "pagination": { | ||||
|                 "total": total_count, | ||||
|                 "per_page": per_page, | ||||
|                 "current_page": page, | ||||
|                 "total_pages": total_pages | ||||
|             }, | ||||
|             "categories": categories, | ||||
|             "current_category": category | ||||
|         }) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         logger.error(f"获取文档列表时出错: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"获取文档列表失败: {str(e)}"}), 500 | ||||
| 
 | ||||
|     finally: | ||||
|         cursor.close() | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/download/<int:document_id>', methods=['GET']) | ||||
| def download_document(document_id): | ||||
|     """下载已分类的文档""" | ||||
|     # 检查用户是否登录 | ||||
|     if 'user_id' not in session: | ||||
|         return jsonify({"success": False, "error": "请先登录"}), 401 | ||||
| 
 | ||||
|     user_id = session['user_id'] | ||||
| 
 | ||||
|     db = get_db() | ||||
|     cursor = db.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         # 查询文档信息 | ||||
|         query = """ | ||||
|         SELECT file_path, original_filename, stored_filename | ||||
|         FROM documents | ||||
|         WHERE id = %s AND user_id = %s | ||||
|         """ | ||||
|         cursor.execute(query, (document_id, user_id)) | ||||
|         document = cursor.fetchone() | ||||
| 
 | ||||
|         if not document: | ||||
|             return jsonify({"success": False, "error": "文档不存在或无权访问"}), 404 | ||||
| 
 | ||||
|         # 检查文件是否存在 | ||||
|         if not os.path.exists(document['file_path']): | ||||
|             return jsonify({"success": False, "error": "文件不存在"}), 404 | ||||
| 
 | ||||
|         # 返回文件下载 | ||||
|         return send_file( | ||||
|             document['file_path'], | ||||
|             as_attachment=True, | ||||
|             download_name=document['original_filename'], | ||||
|             mimetype='text/plain' | ||||
|         ) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         logger.error(f"下载文档时出错: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"下载文档失败: {str(e)}"}), 500 | ||||
| 
 | ||||
|     finally: | ||||
|         cursor.close() | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/download-multiple', methods=['POST']) | ||||
| def download_multiple_documents(): | ||||
|     """下载多个文档(打包为zip)""" | ||||
|     # 检查用户是否登录 | ||||
|     if 'user_id' not in session: | ||||
|         return jsonify({"success": False, "error": "请先登录"}), 401 | ||||
| 
 | ||||
|     user_id = session['user_id'] | ||||
| 
 | ||||
|     # 获取请求数据 | ||||
|     data = request.get_json() | ||||
|     if not data or 'document_ids' not in data: | ||||
|         return jsonify({"success": False, "error": "缺少必要参数"}), 400 | ||||
| 
 | ||||
|     document_ids = data['document_ids'] | ||||
|     if not isinstance(document_ids, list) or not document_ids: | ||||
|         return jsonify({"success": False, "error": "文档ID列表无效"}), 400 | ||||
| 
 | ||||
|     db = get_db() | ||||
|     cursor = db.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         # 创建临时目录用于存放zip文件 | ||||
|         temp_dir = os.path.join(current_app.root_path, 'temp') | ||||
|         os.makedirs(temp_dir, exist_ok=True) | ||||
| 
 | ||||
|         # 创建临时ZIP文件 | ||||
|         zip_filename = f"documents_{int(time.time())}.zip" | ||||
|         zip_path = os.path.join(temp_dir, zip_filename) | ||||
| 
 | ||||
|         # 查询所有符合条件的文档 | ||||
|         placeholders = ', '.join(['%s'] * len(document_ids)) | ||||
|         query = f""" | ||||
|         SELECT id, file_path, original_filename | ||||
|         FROM documents | ||||
|         WHERE id IN ({placeholders}) AND user_id = %s | ||||
|         """ | ||||
|         params = document_ids + [user_id] | ||||
|         cursor.execute(query, params) | ||||
|         documents = cursor.fetchall() | ||||
| 
 | ||||
|         if not documents: | ||||
|             return jsonify({"success": False, "error": "没有找到符合条件的文档"}), 404 | ||||
| 
 | ||||
|         # 创建ZIP文件并添加文档 | ||||
|         with zipfile.ZipFile(zip_path, 'w') as zipf: | ||||
|             for doc in documents: | ||||
|                 if os.path.exists(doc['file_path']): | ||||
|                     # 添加文件到zip,使用原始文件名 | ||||
|                     zipf.write(doc['file_path'], arcname=doc['original_filename']) | ||||
| 
 | ||||
|         # 返回ZIP文件下载 | ||||
|         return send_file( | ||||
|             zip_path, | ||||
|             as_attachment=True, | ||||
|             download_name=zip_filename, | ||||
|             mimetype='application/zip' | ||||
|         ) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|         logger.error(f"下载多个文档时出错: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"下载文档失败: {str(e)}"}), 500 | ||||
| 
 | ||||
|     finally: | ||||
|         cursor.close() | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/classify-text', methods=['POST']) | ||||
| def classify_text_directly(): | ||||
|     """直接对文本进行分类(不保存文件)""" | ||||
|     # 检查用户是否登录 | ||||
|     if 'user_id' not in session: | ||||
|         return jsonify({"success": False, "error": "请先登录"}), 401 | ||||
| 
 | ||||
|     # 获取请求数据 | ||||
|     data = request.get_json() | ||||
|     if not data or 'text' not in data: | ||||
|         return jsonify({"success": False, "error": "缺少必要参数"}), 400 | ||||
| 
 | ||||
|     text = data['text'] | ||||
|     if not text.strip(): | ||||
|         return jsonify({"success": False, "error": "文本内容不能为空"}), 400 | ||||
| 
 | ||||
|     try: | ||||
|         # 调用模型进行分类 | ||||
|         result = text_classifier.classify_text(text) | ||||
| 
 | ||||
|         if not result['success']: | ||||
|             return jsonify({"success": False, "error": result['error']}), 500 | ||||
| 
 | ||||
|         # 返回分类结果 | ||||
|         return jsonify({ | ||||
|             "success": True, | ||||
|             "category": result['category'], | ||||
|             "confidence": result['confidence'], | ||||
|             "all_confidences": result['all_confidences'] | ||||
|         }) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         logger.error(f"文本分类过程中发生错误: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"文本分类错误: {str(e)}"}), 500 | ||||
| 
 | ||||
| 
 | ||||
| @classify_bp.route('/categories', methods=['GET']) | ||||
| def get_categories(): | ||||
|     """获取所有分类类别""" | ||||
|     db = get_db() | ||||
|     cursor = db.cursor(dictionary=True) | ||||
| 
 | ||||
|     try: | ||||
|         cursor.execute("SELECT id, name, description FROM categories ORDER BY name") | ||||
|         categories = cursor.fetchall() | ||||
| 
 | ||||
|         return jsonify({ | ||||
|             "success": True, | ||||
|             "categories": categories | ||||
|         }) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         logger.error(f"获取类别列表时出错: {str(e)}") | ||||
|         return jsonify({"success": False, "error": f"获取类别列表失败: {str(e)}"}), 500 | ||||
| 
 | ||||
|     finally: | ||||
|         cursor.close() | ||||
							
								
								
									
										302
									
								
								static/css/auth.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								static/css/auth.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,302 @@ | ||||
| /* 通用认证页面样式 */ | ||||
| :root { | ||||
|   --primary-color: #4285f4; | ||||
|   --primary-hover: #3367d6; | ||||
|   --success-color: #0f9d58; | ||||
|   --danger-color: #db4437; | ||||
|   --warning-color: #f4b400; | ||||
|   --text-primary: #202124; | ||||
|   --text-secondary: #5f6368; | ||||
|   --bg-main: #ffffff; | ||||
|   --bg-secondary: #f8f9fa; | ||||
|   --border-color: #dadce0; | ||||
|   --shadow-color: rgba(60, 64, 67, 0.15); | ||||
|   --card-bg: #ffffff; | ||||
|   --transition-speed: 0.3s; | ||||
| } | ||||
| 
 | ||||
| .dark-mode { | ||||
|   --primary-color: #8ab4f8; | ||||
|   --primary-hover: #aecbfa; | ||||
|   --success-color: #81c995; | ||||
|   --danger-color: #f28b82; | ||||
|   --warning-color: #fdd663; | ||||
|   --text-primary: #e8eaed; | ||||
|   --text-secondary: #9aa0a6; | ||||
|   --bg-main: #202124; | ||||
|   --bg-secondary: #303134; | ||||
|   --border-color: #5f6368; | ||||
|   --shadow-color: rgba(0, 0, 0, 0.3); | ||||
|   --card-bg: #303134; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|   font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | ||||
|   background-color: var(--bg-secondary); | ||||
|   color: var(--text-primary); | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   transition: background-color var(--transition-speed), color var(--transition-speed); | ||||
|   min-height: 100vh; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| } | ||||
| 
 | ||||
| .content-wrapper { | ||||
|   flex: 1; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   padding: 2rem 1rem; | ||||
| } | ||||
| 
 | ||||
| .auth-container { | ||||
|   background-color: var(--card-bg); | ||||
|   border-radius: 8px; | ||||
|   box-shadow: 0 4px 20px var(--shadow-color); | ||||
|   width: 100%; | ||||
|   max-width: 450px; | ||||
|   padding: 2.5rem; | ||||
|   transition: background-color var(--transition-speed), box-shadow var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| .logo-container { | ||||
|   text-align: center; | ||||
|   margin-bottom: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .logo-container img { | ||||
|   height: 60px; | ||||
|   width: auto; | ||||
| } | ||||
| 
 | ||||
| h1, h2 { | ||||
|   color: var(--text-primary); | ||||
|   text-align: center; | ||||
|   font-weight: 500; | ||||
|   margin-bottom: 1.5rem; | ||||
|   transition: color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| .form-group { | ||||
|   margin-bottom: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .form-group label { | ||||
|   display: block; | ||||
|   margin-bottom: 0.5rem; | ||||
|   color: var(--text-secondary); | ||||
|   font-size: 0.9rem; | ||||
|   transition: color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| .form-control { | ||||
|   width: 100%; | ||||
|   padding: 0.75rem 1rem; | ||||
|   font-size: 1rem; | ||||
|   border: 1px solid var(--border-color); | ||||
|   border-radius: 4px; | ||||
|   background-color: var(--bg-main); | ||||
|   color: var(--text-primary); | ||||
|   transition: border-color var(--transition-speed), background-color var(--transition-speed), color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| .form-control:focus { | ||||
|   outline: none; | ||||
|   border-color: var(--primary-color); | ||||
|   box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2); | ||||
| } | ||||
| 
 | ||||
| .btn { | ||||
|   display: block; | ||||
|   width: 100%; | ||||
|   padding: 0.75rem 1.5rem; | ||||
|   font-size: 1rem; | ||||
|   font-weight: 500; | ||||
|   text-align: center; | ||||
|   border: none; | ||||
|   border-radius: 4px; | ||||
|   cursor: pointer; | ||||
|   transition: background-color var(--transition-speed), color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| .btn-primary { | ||||
|   background-color: var(--primary-color); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .btn-primary:hover { | ||||
|   background-color: var(--primary-hover); | ||||
| } | ||||
| 
 | ||||
| .btn-success { | ||||
|   background-color: var(--success-color); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .btn-success:hover { | ||||
|   opacity: 0.9; | ||||
| } | ||||
| 
 | ||||
| .alert { | ||||
|   padding: 0.75rem 1rem; | ||||
|   margin-bottom: 1rem; | ||||
|   border-radius: 4px; | ||||
|   font-size: 0.9rem; | ||||
| } | ||||
| 
 | ||||
| .alert-danger { | ||||
|   background-color: var(--danger-color); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .alert-success { | ||||
|   background-color: var(--success-color); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .alert-info { | ||||
|   background-color: var(--primary-color); | ||||
|   color: white; | ||||
| } | ||||
| 
 | ||||
| .alert-warning { | ||||
|   background-color: var(--warning-color); | ||||
|   color: var(--text-primary); | ||||
| } | ||||
| 
 | ||||
| .text-center { | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .mt-3 { | ||||
|   margin-top: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .mb-4 { | ||||
|   margin-bottom: 2rem; | ||||
| } | ||||
| 
 | ||||
| .d-none { | ||||
|   display: none !important; | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|   color: var(--primary-color); | ||||
|   text-decoration: none; | ||||
|   transition: color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|   text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| .helper-text { | ||||
|   font-size: 0.8rem; | ||||
|   color: var(--text-secondary); | ||||
|   margin-top: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| /* 响应式设计 */ | ||||
| @media (max-width: 576px) { | ||||
|   .auth-container { | ||||
|     padding: 1.5rem; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /* 页脚样式 */ | ||||
| footer { | ||||
|   text-align: center; | ||||
|   padding: 1.5rem; | ||||
|   font-size: 0.9rem; | ||||
|   color: var(--text-secondary); | ||||
|   background-color: var(--bg-main); | ||||
|   border-top: 1px solid var(--border-color); | ||||
|   transition: background-color var(--transition-speed), color var(--transition-speed), border-color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| /* 主题切换器 */ | ||||
| .theme-switcher { | ||||
|   position: absolute; | ||||
|   top: 1rem; | ||||
|   right: 4rem; | ||||
|   cursor: pointer; | ||||
|   font-size: 1.5rem; | ||||
|   color: var(--text-secondary); | ||||
|   transition: color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| /* 语言切换器 */ | ||||
| .language-switcher { | ||||
|   position: absolute; | ||||
|   top: 1rem; | ||||
|   right: 1rem; | ||||
|   cursor: pointer; | ||||
|   font-size: 1rem; | ||||
|   color: var(--text-secondary); | ||||
|   transition: color var(--transition-speed); | ||||
| } | ||||
| 
 | ||||
| /* 表单验证样式 */ | ||||
| .form-control.is-invalid { | ||||
|   border-color: var(--danger-color); | ||||
| } | ||||
| 
 | ||||
| .invalid-feedback { | ||||
|   display: none; | ||||
|   color: var(--danger-color); | ||||
|   font-size: 0.8rem; | ||||
|   margin-top: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .form-control.is-invalid + .invalid-feedback, | ||||
| .form-control.is-invalid ~ .invalid-feedback { | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| /* 性别单选按钮 */ | ||||
| .radio-group { | ||||
|   display: flex; | ||||
|   gap: 1rem; | ||||
| } | ||||
| 
 | ||||
| .radio-option { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
| 
 | ||||
| .radio-option input[type="radio"] { | ||||
|   margin-right: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| /* 密码框样式 */ | ||||
| .password-container { | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .password-toggle { | ||||
|   position: absolute; | ||||
|   right: 0.75rem; | ||||
|   top: 50%; | ||||
|   transform: translateY(-50%); | ||||
|   cursor: pointer; | ||||
|   color: var(--text-secondary); | ||||
| } | ||||
| 
 | ||||
| /* 加载动画 */ | ||||
| .spinner { | ||||
|   display: inline-block; | ||||
|   width: 1rem; | ||||
|   height: 1rem; | ||||
|   border: 0.15rem solid rgba(255, 255, 255, 0.3); | ||||
|   border-radius: 50%; | ||||
|   border-top-color: #fff; | ||||
|   animation: spin 1s linear infinite; | ||||
|   margin-right: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| @keyframes spin { | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										206
									
								
								static/css/dashboard.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								static/css/dashboard.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,206 @@ | ||||
| /* static/css/dashboard.css */ | ||||
| :root { | ||||
|     --sidebar-width: 250px; | ||||
| } | ||||
| 
 | ||||
| body { | ||||
|     font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; | ||||
|     background-color: #f8f9fa; | ||||
| } | ||||
| 
 | ||||
| /* 顶部导航 */ | ||||
| .top-navbar { | ||||
|     height: 60px; | ||||
|     background-color: #343a40; | ||||
|     color: white; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
|     z-index: 1030; | ||||
| } | ||||
| 
 | ||||
| /* 侧边栏 */ | ||||
| .sidebar { | ||||
|     width: var(--sidebar-width); | ||||
|     position: fixed; | ||||
|     top: 60px; | ||||
|     left: 0; | ||||
|     bottom: 0; | ||||
|     background-color: #343a40; | ||||
|     color: white; | ||||
|     padding-top: 20px; | ||||
|     overflow-y: auto; | ||||
|     transition: all 0.3s; | ||||
|     z-index: 1020; | ||||
| } | ||||
| 
 | ||||
| .sidebar-link { | ||||
|     padding: 10px 15px; | ||||
|     color: rgba(255, 255, 255, 0.8); | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     text-decoration: none; | ||||
|     transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .sidebar-link:hover { | ||||
|     color: white; | ||||
|     background-color: rgba(255, 255, 255, 0.1); | ||||
| } | ||||
| 
 | ||||
| .sidebar-link.active { | ||||
|     color: white; | ||||
|     background-color: rgba(255, 255, 255, 0.2); | ||||
|     border-left: 4px solid #007bff; | ||||
| } | ||||
| 
 | ||||
| .sidebar-link i { | ||||
|     margin-right: 10px; | ||||
|     width: 20px; | ||||
|     text-align: center; | ||||
| } | ||||
| 
 | ||||
| /* 主内容区 */ | ||||
| .main-content { | ||||
|     margin-left: var(--sidebar-width); | ||||
|     margin-top: 60px; | ||||
|     padding: 20px; | ||||
|     transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| /* 响应式设计 */ | ||||
| @media (max-width: 768px) { | ||||
|     .sidebar { | ||||
|         margin-left: calc(-1 * var(--sidebar-width)); | ||||
|     } | ||||
| 
 | ||||
|     .sidebar.active { | ||||
|         margin-left: 0; | ||||
|     } | ||||
| 
 | ||||
|     .main-content { | ||||
|         margin-left: 0; | ||||
|     } | ||||
| 
 | ||||
|     .main-content.sidebar-active { | ||||
|         margin-left: var(--sidebar-width); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /* 用户下拉菜单 */ | ||||
| .user-dropdown .dropdown-toggle::after { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| .user-dropdown .dropdown-menu { | ||||
|     right: 0; | ||||
|     left: auto; | ||||
| } | ||||
| 
 | ||||
| /* 内容卡片 */ | ||||
| .content-card { | ||||
|     background-color: white; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | ||||
|     padding: 20px; | ||||
|     margin-bottom: 20px; | ||||
| } | ||||
| 
 | ||||
| /* 加载指示器 */ | ||||
| .spinner-overlay { | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     bottom: 0; | ||||
|     background-color: rgba(255, 255, 255, 0.7); | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     z-index: 2000; | ||||
|     visibility: hidden; | ||||
|     opacity: 0; | ||||
|     transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .spinner-overlay.show { | ||||
|     visibility: visible; | ||||
|     opacity: 1; | ||||
| } | ||||
| 
 | ||||
| /* 结果容器 */ | ||||
| #classification-result.result-success { | ||||
|     border-color: #28a745 !important; | ||||
| } | ||||
| 
 | ||||
| #classification-result.result-error { | ||||
|     border-color: #dc3545 !important; | ||||
| } | ||||
| 
 | ||||
| /* 表格样式 */ | ||||
| .table th { | ||||
|     background-color: #f8f9fa; | ||||
|     font-weight: 600; | ||||
| } | ||||
| 
 | ||||
| /* 文件大小显示 */ | ||||
| .file-size { | ||||
|     font-family: monospace; | ||||
|     color: #6c757d; | ||||
| } | ||||
| 
 | ||||
| /* 分类标签样式 */ | ||||
| .category-badge { | ||||
|     font-size: 0.85rem; | ||||
|     font-weight: 500; | ||||
|     padding: 0.35em 0.65em; | ||||
| } | ||||
| 
 | ||||
| /* 分页控件 */ | ||||
| .pagination { | ||||
|     margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .pagination .page-item.active .page-link { | ||||
|     background-color: #007bff; | ||||
|     border-color: #007bff; | ||||
| } | ||||
| 
 | ||||
| /* 批量上传区域 */ | ||||
| .upload-area { | ||||
|     border: 2px dashed #dee2e6; | ||||
|     border-radius: 8px; | ||||
|     transition: all 0.3s; | ||||
| } | ||||
| 
 | ||||
| .upload-area:hover { | ||||
|     border-color: #007bff; | ||||
| } | ||||
| 
 | ||||
| /* 文件操作按钮 */ | ||||
| .file-action-btn { | ||||
|     padding: 0.25rem 0.5rem; | ||||
|     font-size: 0.875rem; | ||||
| } | ||||
| 
 | ||||
| /* 结果统计卡片 */ | ||||
| .stat-card { | ||||
|     padding: 1rem; | ||||
|     border-radius: 6px; | ||||
|     margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .stat-value { | ||||
|     font-size: 2rem; | ||||
|     font-weight: 700; | ||||
| } | ||||
| 
 | ||||
| /* 置信度进度条 */ | ||||
| .confidence-bar { | ||||
|     height: 10px; | ||||
|     margin-top: 5px; | ||||
| } | ||||
| 
 | ||||
| /* 类别分布图 */ | ||||
| .category-chart-container { | ||||
|     height: 300px; | ||||
|     margin: 1rem 0; | ||||
| } | ||||
							
								
								
									
										4
									
								
								static/images/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								static/images/logo.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"> | ||||
|   <circle cx="50" cy="50" r="45" fill="#3498db" /> | ||||
|   <text x="50" y="65" font-family="Arial" font-size="50" font-weight="bold" text-anchor="middle" fill="white">子</text> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 242 B | 
							
								
								
									
										927
									
								
								static/js/dashboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										927
									
								
								static/js/dashboard.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,927 @@ | ||||
| // static/js/dashboard.js
 | ||||
| 
 | ||||
| // 全局变量和状态管理
 | ||||
| const state = { | ||||
|     currentPage: 'classify',  // 当前显示的页面
 | ||||
|     documentsData: {          // 文档列表数据
 | ||||
|         items: [],            // 当前页的文档条目
 | ||||
|         pagination: {         // 分页信息
 | ||||
|             total: 0, | ||||
|             per_page: 10, | ||||
|             current_page: 1, | ||||
|             total_pages: 0 | ||||
|         }, | ||||
|         categories: [],       // 所有类别
 | ||||
|         currentCategory: 'all',  // 当前选择的类别
 | ||||
|         selectedIds: []       // 选中的文档ID
 | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| // DOM元素缓存
 | ||||
| const elements = { | ||||
|     // 侧边栏和导航
 | ||||
|     sidebar: document.querySelector('.sidebar'), | ||||
|     sidebarToggle: document.getElementById('sidebar-toggle'), | ||||
|     sidebarLinks: document.querySelectorAll('.sidebar-link'), | ||||
|     mainContent: document.querySelector('.main-content'), | ||||
| 
 | ||||
|     // 页面sections
 | ||||
|     pageSections: document.querySelectorAll('.page-section'), | ||||
|     classifySection: document.getElementById('classify-section'), | ||||
|     documentsSection: document.getElementById('documents-section'), | ||||
|     batchSection: document.getElementById('batch-section'), | ||||
| 
 | ||||
|     // 文本分类相关
 | ||||
|     textInput: document.getElementById('text-input'), | ||||
|     classifyTextBtn: document.getElementById('classify-text-btn'), | ||||
|     clearTextBtn: document.getElementById('clear-text-btn'), | ||||
|     classificationResult: document.getElementById('classification-result'), | ||||
|     fileUpload: document.getElementById('file-upload'), | ||||
|     uploadFileBtn: document.getElementById('upload-file-btn'), | ||||
|     fileResult: document.getElementById('file-result'), | ||||
| 
 | ||||
|     // 已处理文本相关
 | ||||
|     categoryFilter: document.getElementById('category-filter'), | ||||
|     perPageSelect: document.getElementById('per-page-select'), | ||||
|     downloadSelectedBtn: document.getElementById('download-selected-btn'), | ||||
|     documentsTable: document.getElementById('documents-table'), | ||||
|     selectAllDocs: document.getElementById('select-all-docs'), | ||||
|     paginationContainer: document.getElementById('pagination-container'), | ||||
| 
 | ||||
|     // 批量处理相关
 | ||||
|     batchFileUpload: document.getElementById('batch-file-upload'), | ||||
|     uploadBatchBtn: document.getElementById('upload-batch-btn'), | ||||
|     batchResult: document.getElementById('batch-result'), | ||||
| 
 | ||||
|     // 加载指示器
 | ||||
|     loadingOverlay: document.getElementById('loading-overlay') | ||||
| }; | ||||
| 
 | ||||
| // 初始化
 | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     // 绑定事件
 | ||||
|     bindEvents(); | ||||
| 
 | ||||
|     // 初始化分类模型
 | ||||
|     initializeModel(); | ||||
| }); | ||||
| 
 | ||||
| // 事件绑定
 | ||||
| function bindEvents() { | ||||
|     // 侧边栏切换
 | ||||
|     elements.sidebarToggle?.addEventListener('click', toggleSidebar); | ||||
| 
 | ||||
|     // 侧边栏导航链接
 | ||||
|     elements.sidebarLinks.forEach(link => { | ||||
|         link.addEventListener('click', (e) => { | ||||
|             e.preventDefault(); | ||||
|             const targetPage = link.dataset.page; | ||||
|             navigateToPage(targetPage); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     // 文本分类相关
 | ||||
|     elements.classifyTextBtn?.addEventListener('click', classifyTextContent); | ||||
|     elements.clearTextBtn?.addEventListener('click', clearTextContent); | ||||
|     elements.uploadFileBtn?.addEventListener('click', uploadAndClassifyFile); | ||||
| 
 | ||||
|     // 已处理文本相关
 | ||||
|     elements.categoryFilter?.addEventListener('change', filterDocumentsByCategory); | ||||
|     elements.perPageSelect?.addEventListener('change', changePageSize); | ||||
|     elements.selectAllDocs?.addEventListener('change', toggleSelectAllDocuments); | ||||
|     elements.downloadSelectedBtn?.addEventListener('click', downloadSelectedDocuments); | ||||
| 
 | ||||
|     // 批量处理相关
 | ||||
|     elements.uploadBatchBtn?.addEventListener('click', uploadAndClassifyBatch); | ||||
| } | ||||
| 
 | ||||
| // 侧边栏切换
 | ||||
| function toggleSidebar() { | ||||
|     elements.sidebar.classList.toggle('active'); | ||||
|     elements.mainContent.classList.toggle('sidebar-active'); | ||||
| } | ||||
| 
 | ||||
| // 页面导航
 | ||||
| function navigateToPage(targetPage) { | ||||
|     // 保存当前页面
 | ||||
|     state.currentPage = targetPage; | ||||
| 
 | ||||
|     // 更新活动链接
 | ||||
|     elements.sidebarLinks.forEach(link => { | ||||
|         if (link.dataset.page === targetPage) { | ||||
|             link.classList.add('active'); | ||||
|         } else { | ||||
|             link.classList.remove('active'); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // 显示对应页面
 | ||||
|     elements.pageSections.forEach(section => { | ||||
|         if (section.id === `${targetPage}-section`) { | ||||
|             section.classList.remove('d-none'); | ||||
|         } else { | ||||
|             section.classList.add('d-none'); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // 加载页面特定数据
 | ||||
|     if (targetPage === 'documents') { | ||||
|         loadDocuments(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 显示加载指示器
 | ||||
| function showLoading() { | ||||
|     elements.loadingOverlay.classList.add('show'); | ||||
| } | ||||
| 
 | ||||
| // 隐藏加载指示器
 | ||||
| function hideLoading() { | ||||
|     elements.loadingOverlay.classList.remove('show'); | ||||
| } | ||||
| 
 | ||||
| // API请求公共方法
 | ||||
| async function apiRequest(url, options = {}) { | ||||
|     try { | ||||
|         const response = await fetch(url, { | ||||
|             ...options, | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json', | ||||
|                 ...options.headers | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         const data = await response.json(); | ||||
| 
 | ||||
|         if (!response.ok) { | ||||
|             throw new Error(data.error || '请求失败'); | ||||
|         } | ||||
| 
 | ||||
|         return data; | ||||
|     } catch (error) { | ||||
|         console.error('API请求失败:', error); | ||||
|         throw error; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 初始化分类模型
 | ||||
| async function initializeModel() { | ||||
|     try { | ||||
|         await apiRequest('/api/classify/categories'); | ||||
|         console.log('模型加载成功'); | ||||
|     } catch (error) { | ||||
|         console.error('模型初始化失败:', error); | ||||
|         alert('模型加载失败,请刷新页面重试'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 文本分类函数
 | ||||
| async function classifyTextContent() { | ||||
|     const text = elements.textInput.value.trim(); | ||||
| 
 | ||||
|     if (!text) { | ||||
|         alert('请输入需要分类的文本'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     showLoading(); | ||||
| 
 | ||||
|     try { | ||||
|         const result = await apiRequest('/api/classify/classify-text', { | ||||
|             method: 'POST', | ||||
|             body: JSON.stringify({ text }) | ||||
|         }); | ||||
| 
 | ||||
|         // 显示分类结果
 | ||||
|         displayTextClassificationResult(result); | ||||
|     } catch (error) { | ||||
|         elements.classificationResult.innerHTML = ` | ||||
|             <div class="alert alert-danger"> | ||||
|                 <i class="fas fa-exclamation-circle me-2"></i> | ||||
|                 分类失败: ${error.message} | ||||
|             </div> | ||||
|         `;
 | ||||
|         elements.classificationResult.classList.remove('result-success'); | ||||
|         elements.classificationResult.classList.add('result-error'); | ||||
|     } finally { | ||||
|         hideLoading(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 清空文本内容
 | ||||
| function clearTextContent() { | ||||
|     elements.textInput.value = ''; | ||||
|     elements.classificationResult.innerHTML = ` | ||||
|         <h5 class="text-center text-muted"> | ||||
|             <i class="fas fa-info-circle me-2"></i> | ||||
|             分类结果将在这里显示 | ||||
|         </h5> | ||||
|         <div class="text-center mt-4"> | ||||
|             <i class="fas fa-arrow-left text-muted me-2"></i> | ||||
|             请在左侧输入文本或上传文件 | ||||
|         </div> | ||||
|     `;
 | ||||
|     elements.classificationResult.classList.remove('result-success', 'result-error'); | ||||
| } | ||||
| 
 | ||||
| // 显示文本分类结果
 | ||||
| function displayTextClassificationResult(result) { | ||||
|     if (!result.success) { | ||||
|         elements.classificationResult.innerHTML = ` | ||||
|             <div class="alert alert-danger"> | ||||
|                 <i class="fas fa-exclamation-circle me-2"></i> | ||||
|                 分类失败: ${result.error} | ||||
|             </div> | ||||
|         `;
 | ||||
|         elements.classificationResult.classList.remove('result-success'); | ||||
|         elements.classificationResult.classList.add('result-error'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // 获取所有类别的置信度,并排序
 | ||||
|     const confidences = Object.entries(result.all_confidences) | ||||
|         .map(([category, value]) => ({ category, value })) | ||||
|         .sort((a, b) => b.value - a.value); | ||||
| 
 | ||||
|     // 创建置信度条形图HTML
 | ||||
|     const confidenceBarsHtml = confidences.slice(0, 5).map(item => { | ||||
|         const percentage = (item.value * 100).toFixed(2); | ||||
|         return ` | ||||
|             <div class="mb-2"> | ||||
|                 <div class="d-flex justify-content-between align-items-center"> | ||||
|                     <span>${item.category}</span> | ||||
|                     <span>${percentage}%</span> | ||||
|                 </div> | ||||
|                 <div class="progress confidence-bar"> | ||||
|                     <div class="progress-bar ${item.category === result.category ? 'bg-success' : 'bg-primary'}"  | ||||
|                          role="progressbar"  | ||||
|                          style="width: ${percentage}%"  | ||||
|                          aria-valuenow="${percentage}"  | ||||
|                          aria-valuemin="0"  | ||||
|                          aria-valuemax="100"> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         `;
 | ||||
|     }).join(''); | ||||
| 
 | ||||
|     // 更新结果显示
 | ||||
|     elements.classificationResult.innerHTML = ` | ||||
|         <div class="text-center mb-4"> | ||||
|             <div class="display-4 fw-bold text-success">${result.category}</div> | ||||
|             <div class="text-muted">置信度: ${(result.confidence * 100).toFixed(2)}%</div> | ||||
|         </div> | ||||
|          | ||||
|         <h5 class="mb-3">类别置信度分布:</h5> | ||||
|         ${confidenceBarsHtml} | ||||
|          | ||||
|         <div class="alert alert-info mt-4"> | ||||
|             <i class="fas fa-info-circle me-2"></i> | ||||
|             该文本被成功分类为 <strong>${result.category}</strong> 类别 | ||||
|         </div> | ||||
|     `;
 | ||||
| 
 | ||||
|     elements.classificationResult.classList.add('result-success'); | ||||
|     elements.classificationResult.classList.remove('result-error'); | ||||
| } | ||||
| 
 | ||||
| // 上传并分类文件
 | ||||
| async function uploadAndClassifyFile() { | ||||
|     const fileInput = elements.fileUpload; | ||||
| 
 | ||||
|     if (!fileInput.files || fileInput.files.length === 0) { | ||||
|         alert('请选择要上传的文件'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const file = fileInput.files[0]; | ||||
| 
 | ||||
|     // 检查文件类型
 | ||||
|     if (!file.name.toLowerCase().endsWith('.txt')) { | ||||
|         alert('只支持上传 .txt 格式的文本文件'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     showLoading(); | ||||
| 
 | ||||
|     try { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('file', file); | ||||
| 
 | ||||
|         const response = await fetch('/api/classify/single', { | ||||
|             method: 'POST', | ||||
|             body: formData | ||||
|         }); | ||||
| 
 | ||||
|         const result = await response.json(); | ||||
| 
 | ||||
|         if (!result.success) { | ||||
|             throw new Error(result.error || '文件分类失败'); | ||||
|         } | ||||
| 
 | ||||
|         // 显示文件分类结果
 | ||||
|         displayFileClassificationResult(result); | ||||
|     } catch (error) { | ||||
|         elements.fileResult.innerHTML = ` | ||||
|             <div class="alert alert-danger"> | ||||
|                 <i class="fas fa-exclamation-circle me-2"></i> | ||||
|                 文件分类失败: ${error.message} | ||||
|             </div> | ||||
|         `;
 | ||||
|         elements.fileResult.style.display = 'block'; | ||||
|     } finally { | ||||
|         hideLoading(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // static/js/dashboard.js (继续)
 | ||||
| 
 | ||||
| // 显示文件分类结果
 | ||||
| function displayFileClassificationResult(result) { | ||||
|     elements.fileResult.innerHTML = ` | ||||
|         <div class="alert alert-success"> | ||||
|             <div class="d-flex justify-content-between align-items-center"> | ||||
|                 <div> | ||||
|                     <i class="fas fa-check-circle me-2"></i> | ||||
|                     文件 <strong>${result.filename}</strong> 已成功分类为 <strong>${result.category}</strong> 类别 | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <span class="badge bg-success">${(result.confidence * 100).toFixed(2)}%</span> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="mt-2"> | ||||
|             <div class="text-muted mb-2">文件已保存为: ${result.stored_filename}</div> | ||||
|             <button class="btn btn-sm btn-outline-primary view-documents-btn"> | ||||
|                 <i class="fas fa-list me-1"></i> 查看已处理文档 | ||||
|             </button> | ||||
|         </div> | ||||
|     `;
 | ||||
|     elements.fileResult.style.display = 'block'; | ||||
| 
 | ||||
|     // 添加查看已处理文档按钮事件
 | ||||
|     document.querySelector('.view-documents-btn')?.addEventListener('click', () => { | ||||
|         navigateToPage('documents'); | ||||
|     }); | ||||
| 
 | ||||
|     // 清空文件上传控件
 | ||||
|     elements.fileUpload.value = ''; | ||||
| } | ||||
| 
 | ||||
| // 上传并批量分类文件
 | ||||
| async function uploadAndClassifyBatch() { | ||||
|     const fileInput = elements.batchFileUpload; | ||||
| 
 | ||||
|     if (!fileInput.files || fileInput.files.length === 0) { | ||||
|         alert('请选择要上传的压缩文件'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const file = fileInput.files[0]; | ||||
| 
 | ||||
|     // 检查文件类型
 | ||||
|     const fileName = file.name.toLowerCase(); | ||||
|     if (!fileName.endsWith('.zip') && !fileName.endsWith('.rar')) { | ||||
|         alert('只支持上传 .zip 或 .rar 格式的压缩文件'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // 检查文件大小
 | ||||
|     if (file.size > 10 * 1024 * 1024) { // 10MB
 | ||||
|         alert('文件大小不能超过10MB'); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     showLoading(); | ||||
| 
 | ||||
|     try { | ||||
|         const formData = new FormData(); | ||||
|         formData.append('file', file); | ||||
| 
 | ||||
|         const response = await fetch('/api/classify/batch', { | ||||
|             method: 'POST', | ||||
|             body: formData | ||||
|         }); | ||||
| 
 | ||||
|         const result = await response.json(); | ||||
| 
 | ||||
|         if (!result.success) { | ||||
|             throw new Error(result.error || '批量分类失败'); | ||||
|         } | ||||
| 
 | ||||
|         // 显示批量分类结果
 | ||||
|         displayBatchClassificationResult(result); | ||||
|     } catch (error) { | ||||
|         elements.batchResult.innerHTML = ` | ||||
|             <div class="alert alert-danger"> | ||||
|                 <i class="fas fa-exclamation-circle me-2"></i> | ||||
|                 批量分类失败: ${error.message} | ||||
|             </div> | ||||
|         `;
 | ||||
|         elements.batchResult.style.display = 'block'; | ||||
|     } finally { | ||||
|         hideLoading(); | ||||
|         // 清空文件上传控件
 | ||||
|         elements.batchFileUpload.value = ''; | ||||
|     } | ||||
| } | ||||
| // 显示批量分类结果
 | ||||
| function displayBatchClassificationResult(result) { | ||||
|     const { archive_name, results } = result; | ||||
| 
 | ||||
|     // 计算成功率
 | ||||
|     const successRate = results.total > 0 ? ((results.success / results.total) * 100).toFixed(1) : 0; | ||||
| 
 | ||||
|     // 构建类别分布HTML
 | ||||
|     let categoriesHtml = ''; | ||||
|     if (results.categories && Object.keys(results.categories).length > 0) { | ||||
|         categoriesHtml = ` | ||||
|             <div class="mt-4"> | ||||
|                 <h5>分类类别分布:</h5> | ||||
|                 <div class="row"> | ||||
|                     ${Object.entries(results.categories).map(([category, count]) => ` | ||||
|                         <div class="col-md-3 col-sm-6 mb-3"> | ||||
|                             <div class="card stat-card bg-light"> | ||||
|                                 <div class="card-body p-3"> | ||||
|                                     <h6 class="card-title">${category}</h6> | ||||
|                                     <div class="stat-value">${count}</div> | ||||
|                                     <div class="text-muted">文件数</div> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     `).join('')}
 | ||||
|                 </div> | ||||
|             </div> | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     // 构建失败文件列表HTML
 | ||||
|     let failedFilesHtml = ''; | ||||
|     if (results.failed > 0 && results.failed_files && results.failed_files.length > 0) { | ||||
|         failedFilesHtml = ` | ||||
|             <div class="mt-4"> | ||||
|                 <h5>处理失败的文件:</h5> | ||||
|                 <div class="table-responsive"> | ||||
|                     <table class="table table-sm table-striped"> | ||||
|                         <thead> | ||||
|                             <tr> | ||||
|                                 <th>文件名</th> | ||||
|                                 <th>错误原因</th> | ||||
|                             </tr> | ||||
|                         </thead> | ||||
|                         <tbody> | ||||
|                             ${results.failed_files.map(file => ` | ||||
|                                 <tr> | ||||
|                                     <td>${file.filename}</td> | ||||
|                                     <td>${file.error}</td> | ||||
|                                 </tr> | ||||
|                             `).join('')}
 | ||||
|                         </tbody> | ||||
|                     </table> | ||||
|                 </div> | ||||
|             </div> | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     // 构建完整的结果HTML
 | ||||
|     elements.batchResult.innerHTML = ` | ||||
|         <div class="alert alert-info"> | ||||
|             <i class="fas fa-info-circle me-2"></i> | ||||
|             压缩包 <strong>${archive_name}</strong> 的处理结果 | ||||
|         </div> | ||||
|          | ||||
|         <div class="row text-center mt-4"> | ||||
|             <div class="col-md-3 col-sm-6 mb-3"> | ||||
|                 <div class="card stat-card bg-light"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="card-title">总文件数</h6> | ||||
|                         <div class="stat-value">${results.total}</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-md-3 col-sm-6 mb-3"> | ||||
|                 <div class="card stat-card bg-success text-white"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="card-title">成功处理</h6> | ||||
|                         <div class="stat-value">${results.success}</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-md-3 col-sm-6 mb-3"> | ||||
|                 <div class="card stat-card ${results.failed > 0 ? 'bg-danger text-white' : 'bg-light'}"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="card-title">处理失败</h6> | ||||
|                         <div class="stat-value">${results.failed}</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-md-3 col-sm-6 mb-3"> | ||||
|                 <div class="card stat-card bg-info text-white"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="card-title">成功率</h6> | ||||
|                         <div class="stat-value">${successRate}%</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|          | ||||
|         ${categoriesHtml} | ||||
|         ${failedFilesHtml} | ||||
|          | ||||
|         <div class="mt-4 text-center"> | ||||
|             <button class="btn btn-primary view-documents-btn"> | ||||
|                 <i class="fas fa-list me-1"></i> 查看所有已处理文档 | ||||
|             </button> | ||||
|         </div> | ||||
|     `;
 | ||||
| 
 | ||||
|     elements.batchResult.style.display = 'block'; | ||||
| 
 | ||||
|     // 添加查看已处理文档按钮事件
 | ||||
|     document.querySelector('#batch-result .view-documents-btn')?.addEventListener('click', () => { | ||||
|         navigateToPage('documents'); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| // 加载已分类文档列表
 | ||||
| async function loadDocuments() { | ||||
|     showLoading(); | ||||
| 
 | ||||
|     try { | ||||
|         const category = state.documentsData.currentCategory; | ||||
|         const page = state.documentsData.pagination.current_page; | ||||
|         const perPage = state.documentsData.pagination.per_page; | ||||
| 
 | ||||
|         const result = await apiRequest(`/api/classify/documents?category=${category}&page=${page}&per_page=${perPage}`); | ||||
| 
 | ||||
|         if (!result.success) { | ||||
|             throw new Error(result.error || '加载文档失败'); | ||||
|         } | ||||
| 
 | ||||
|         // 更新状态
 | ||||
|         state.documentsData.items = result.documents; | ||||
|         state.documentsData.pagination = result.pagination; | ||||
|         state.documentsData.categories = result.categories; | ||||
|         state.documentsData.currentCategory = result.current_category; | ||||
| 
 | ||||
|         // 更新类别筛选下拉菜单
 | ||||
|         updateCategoryFilter(result.categories, result.current_category); | ||||
| 
 | ||||
|         // 更新文档表格
 | ||||
|         updateDocumentsTable(result.documents); | ||||
| 
 | ||||
|         // 更新分页控件
 | ||||
|         updatePagination(result.pagination); | ||||
| 
 | ||||
|         // 重置选中状态
 | ||||
|         state.documentsData.selectedIds = []; | ||||
|         updateDownloadButtonState(); | ||||
| 
 | ||||
|     } catch (error) { | ||||
|         console.error('加载文档失败:', error); | ||||
|         elements.documentsTable.querySelector('tbody').innerHTML = ` | ||||
|             <tr> | ||||
|                 <td colspan="6" class="text-center py-4 text-danger"> | ||||
|                     <i class="fas fa-exclamation-circle me-2"></i> | ||||
|                     加载文档失败: ${error.message} | ||||
|                 </td> | ||||
|             </tr> | ||||
|         `;
 | ||||
|     } finally { | ||||
|         hideLoading(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 更新类别筛选下拉菜单
 | ||||
| function updateCategoryFilter(categories, currentCategory) { | ||||
|     const select = elements.categoryFilter; | ||||
| 
 | ||||
|     // 保存第一个全部选项
 | ||||
|     const allOption = select.querySelector('option[value="all"]'); | ||||
| 
 | ||||
|     // 清空现有选项
 | ||||
|     select.innerHTML = ''; | ||||
| 
 | ||||
|     // 添加全部选项
 | ||||
|     select.appendChild(allOption); | ||||
| 
 | ||||
|     // 添加类别选项
 | ||||
|     categories.forEach(category => { | ||||
|         const option = document.createElement('option'); | ||||
|         option.value = category; | ||||
|         option.textContent = category; | ||||
|         select.appendChild(option); | ||||
|     }); | ||||
| 
 | ||||
|     // 设置当前选中值
 | ||||
|     select.value = currentCategory; | ||||
| } | ||||
| 
 | ||||
| // 更新文档表格
 | ||||
| function updateDocumentsTable(documents) { | ||||
|     const tbody = elements.documentsTable.querySelector('tbody'); | ||||
| 
 | ||||
|     if (!documents || documents.length === 0) { | ||||
|         tbody.innerHTML = ` | ||||
|             <tr> | ||||
|                 <td colspan="6" class="text-center py-4 text-muted"> | ||||
|                     <i class="fas fa-folder-open me-2"></i> | ||||
|                     没有找到文档 | ||||
|                 </td> | ||||
|             </tr> | ||||
|         `;
 | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     tbody.innerHTML = documents.map(doc => { | ||||
|         // 格式化文件大小
 | ||||
|         const fileSize = formatFileSize(doc.file_size); | ||||
| 
 | ||||
|         // 格式化日期
 | ||||
|         const classifiedDate = new Date(doc.classified_time).toLocaleString('zh-CN'); | ||||
| 
 | ||||
|         return ` | ||||
|             <tr> | ||||
|                 <td> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input document-checkbox" type="checkbox" value="${doc.id}" data-id="${doc.id}"> | ||||
|                     </div> | ||||
|                 </td> | ||||
|                 <td>${doc.original_filename}</td> | ||||
|                 <td> | ||||
|                     <span class="badge category-badge bg-primary">${doc.category}</span> | ||||
|                 </td> | ||||
|                 <td class="file-size">${fileSize}</td> | ||||
|                 <td>${classifiedDate}</td> | ||||
|                 <td> | ||||
|                     <a href="/api/classify/download/${doc.id}" class="btn btn-sm btn-outline-primary file-action-btn"> | ||||
|                         <i class="fas fa-download"></i> | ||||
|                     </a> | ||||
|                 </td> | ||||
|             </tr> | ||||
|         `;
 | ||||
|     }).join(''); | ||||
| 
 | ||||
|     // 添加复选框事件监听
 | ||||
|     tbody.querySelectorAll('.document-checkbox').forEach(checkbox => { | ||||
|         checkbox.addEventListener('change', handleDocumentCheckboxChange); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| // 格式化文件大小
 | ||||
| function formatFileSize(bytes) { | ||||
|     if (bytes === 0) return '0 Bytes'; | ||||
| 
 | ||||
|     const k = 1024; | ||||
|     const sizes = ['Bytes', 'KB', 'MB', 'GB']; | ||||
|     const i = Math.floor(Math.log(bytes) / Math.log(k)); | ||||
| 
 | ||||
|     return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | ||||
| } | ||||
| 
 | ||||
| // 更新分页控件
 | ||||
| function updatePagination(pagination) { | ||||
|     const { total, per_page, current_page, total_pages } = pagination; | ||||
| 
 | ||||
|     if (total_pages <= 1) { | ||||
|         elements.paginationContainer.innerHTML = ''; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // 构建分页按钮
 | ||||
|     let paginationHtml = ` | ||||
|         <div> | ||||
|             显示 ${total} 条记录中的 ${Math.min((current_page - 1) * per_page + 1, total)} 至 ${Math.min(current_page * per_page, total)} 条 | ||||
|         </div> | ||||
|         <ul class="pagination"> | ||||
|     `;
 | ||||
| 
 | ||||
|     // 上一页按钮
 | ||||
|     paginationHtml += ` | ||||
|         <li class="page-item ${current_page === 1 ? 'disabled' : ''}"> | ||||
|             <a class="page-link" href="#" data-page="${current_page - 1}" aria-label="上一页"> | ||||
|                 <i class="fas fa-chevron-left"></i> | ||||
|             </a> | ||||
|         </li> | ||||
|     `;
 | ||||
| 
 | ||||
|     // 页码按钮
 | ||||
|     const maxVisiblePages = 5; | ||||
|     let startPage = Math.max(1, current_page - Math.floor(maxVisiblePages / 2)); | ||||
|     let endPage = Math.min(total_pages, startPage + maxVisiblePages - 1); | ||||
| 
 | ||||
|     // 调整startPage以显示maxVisiblePages个页码
 | ||||
|     if (endPage - startPage + 1 < maxVisiblePages) { | ||||
|         startPage = Math.max(1, endPage - maxVisiblePages + 1); | ||||
|     } | ||||
| 
 | ||||
|     // 第一页
 | ||||
|     if (startPage > 1) { | ||||
|         paginationHtml += ` | ||||
|             <li class="page-item"> | ||||
|                 <a class="page-link" href="#" data-page="1">1</a> | ||||
|             </li> | ||||
|         `;
 | ||||
| 
 | ||||
|         if (startPage > 2) { | ||||
|             paginationHtml += ` | ||||
|                 <li class="page-item disabled"> | ||||
|                     <a class="page-link" href="#">...</a> | ||||
|                 </li> | ||||
|             `;
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // 中间页码
 | ||||
|     for (let i = startPage; i <= endPage; i++) { | ||||
|         paginationHtml += ` | ||||
|             <li class="page-item ${i === current_page ? 'active' : ''}"> | ||||
|                 <a class="page-link" href="#" data-page="${i}">${i}</a> | ||||
|             </li> | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     // 最后页
 | ||||
|     if (endPage < total_pages) { | ||||
|         if (endPage < total_pages - 1) { | ||||
|             paginationHtml += ` | ||||
|                 <li class="page-item disabled"> | ||||
|                     <a class="page-link" href="#">...</a> | ||||
|                 </li> | ||||
|             `;
 | ||||
|         } | ||||
| 
 | ||||
|         paginationHtml += ` | ||||
|             <li class="page-item"> | ||||
|                 <a class="page-link" href="#" data-page="${total_pages}">${total_pages}</a> | ||||
|             </li> | ||||
|         `;
 | ||||
|     } | ||||
| 
 | ||||
|     // 下一页按钮
 | ||||
|     paginationHtml += ` | ||||
|         <li class="page-item ${current_page === total_pages ? 'disabled' : ''}"> | ||||
|             <a class="page-link" href="#" data-page="${current_page + 1}" aria-label="下一页"> | ||||
|                 <i class="fas fa-chevron-right"></i> | ||||
|             </a> | ||||
|         </li> | ||||
|     `;
 | ||||
| 
 | ||||
|     paginationHtml += '</ul>'; | ||||
| 
 | ||||
|     // 更新分页控件
 | ||||
|     elements.paginationContainer.innerHTML = paginationHtml; | ||||
| 
 | ||||
|     // 添加分页点击事件
 | ||||
|     elements.paginationContainer.querySelectorAll('.page-link').forEach(link => { | ||||
|         link.addEventListener('click', function(e) { | ||||
|             e.preventDefault(); | ||||
| 
 | ||||
|             if (this.parentElement.classList.contains('disabled')) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const page = parseInt(this.dataset.page, 10); | ||||
|             if (page && page !== current_page) { | ||||
|                 state.documentsData.pagination.current_page = page; | ||||
|                 loadDocuments(); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| // 处理文档复选框变化
 | ||||
| function handleDocumentCheckboxChange(e) { | ||||
|     const checkbox = e.target; | ||||
|     const documentId = parseInt(checkbox.dataset.id, 10); | ||||
| 
 | ||||
|     if (checkbox.checked) { | ||||
|         // 添加到选中列表
 | ||||
|         if (!state.documentsData.selectedIds.includes(documentId)) { | ||||
|             state.documentsData.selectedIds.push(documentId); | ||||
|         } | ||||
|     } else { | ||||
|         // 从选中列表移除
 | ||||
|         state.documentsData.selectedIds = state.documentsData.selectedIds.filter(id => id !== documentId); | ||||
| 
 | ||||
|         // 取消全选复选框
 | ||||
|         elements.selectAllDocs.checked = false; | ||||
|     } | ||||
| 
 | ||||
|     // 更新下载按钮状态
 | ||||
|     updateDownloadButtonState(); | ||||
| } | ||||
| 
 | ||||
| // 全选/取消全选文档
 | ||||
| function toggleSelectAllDocuments(e) { | ||||
|     const isChecked = e.target.checked; | ||||
| 
 | ||||
|     // 获取当前页面所有文档复选框
 | ||||
|     const checkboxes = elements.documentsTable.querySelectorAll('.document-checkbox'); | ||||
| 
 | ||||
|     checkboxes.forEach(checkbox => { | ||||
|         checkbox.checked = isChecked; | ||||
| 
 | ||||
|         const documentId = parseInt(checkbox.dataset.id, 10); | ||||
| 
 | ||||
|         if (isChecked) { | ||||
|             // 添加到选中列表
 | ||||
|             if (!state.documentsData.selectedIds.includes(documentId)) { | ||||
|                 state.documentsData.selectedIds.push(documentId); | ||||
|             } | ||||
|         } else { | ||||
|             // 从选中列表移除
 | ||||
|             state.documentsData.selectedIds = state.documentsData.selectedIds.filter(id => id !== documentId); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // 更新下载按钮状态
 | ||||
|     updateDownloadButtonState(); | ||||
| } | ||||
| 
 | ||||
| // 更新下载按钮状态
 | ||||
| function updateDownloadButtonState() { | ||||
|     const downloadBtn = elements.downloadSelectedBtn; | ||||
| 
 | ||||
|     if (state.documentsData.selectedIds.length > 0) { | ||||
|         downloadBtn.classList.remove('disabled'); | ||||
|         downloadBtn.textContent = `下载选中文件 (${state.documentsData.selectedIds.length})`; | ||||
|     } else { | ||||
|         downloadBtn.classList.add('disabled'); | ||||
|         downloadBtn.innerHTML = '<i class="fas fa-download me-1"></i> 下载选中文件'; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 下载选中文档
 | ||||
| async function downloadSelectedDocuments() { | ||||
|     if (state.documentsData.selectedIds.length === 0) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     showLoading(); | ||||
| 
 | ||||
|     try { | ||||
|         // 单个文件直接下载,多个文件使用API下载压缩包
 | ||||
|         if (state.documentsData.selectedIds.length === 1) { | ||||
|             const documentId = state.documentsData.selectedIds[0]; | ||||
|             window.location.href = `/api/classify/download/${documentId}`; | ||||
|             hideLoading(); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // 多个文件,发送批量下载请求
 | ||||
|         const response = await fetch('/api/classify/download-multiple', { | ||||
|             method: 'POST', | ||||
|             headers: { | ||||
|                 'Content-Type': 'application/json' | ||||
|             }, | ||||
|             body: JSON.stringify({ | ||||
|                 document_ids: state.documentsData.selectedIds | ||||
|             }) | ||||
|         }); | ||||
| 
 | ||||
|         if (!response.ok) { | ||||
|             const error = await response.json(); | ||||
|             throw new Error(error.error || '下载失败'); | ||||
|         } | ||||
| 
 | ||||
|         // 获取blob数据
 | ||||
|         const blob = await response.blob(); | ||||
| 
 | ||||
|         // 创建下载链接
 | ||||
|         const url = window.URL.createObjectURL(blob); | ||||
|         const a = document.createElement('a'); | ||||
|         a.style.display = 'none'; | ||||
|         a.href = url; | ||||
|         a.download = `documents_${new Date().getTime()}.zip`; | ||||
|         document.body.appendChild(a); | ||||
|         a.click(); | ||||
| 
 | ||||
|         // 清理
 | ||||
|         window.URL.revokeObjectURL(url); | ||||
|         document.body.removeChild(a); | ||||
| 
 | ||||
|     } catch (error) { | ||||
|         console.error('下载文档失败:', error); | ||||
|         alert(`下载文档失败: ${error.message}`); | ||||
|     } finally { | ||||
|         hideLoading(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // 按类别筛选文档
 | ||||
| function filterDocumentsByCategory(e) { | ||||
|     const category = e.target.value; | ||||
|     state.documentsData.currentCategory = category; | ||||
|     state.documentsData.pagination.current_page = 1; | ||||
|     loadDocuments(); | ||||
| } | ||||
| 
 | ||||
| // 修改每页显示数量
 | ||||
| function changePageSize(e) { | ||||
|     const perPage = parseInt(e.target.value, 10); | ||||
|     state.documentsData.pagination.per_page = perPage; | ||||
|     state.documentsData.pagination.current_page = 1; | ||||
|     loadDocuments(); | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										142
									
								
								static/js/language-switcher.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								static/js/language-switcher.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,142 @@ | ||||
| // 语言切换功能
 | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|   const languageSwitcher = document.getElementById('language-switcher'); | ||||
| 
 | ||||
|   // 中文词汇表
 | ||||
|   const zhTranslations = { | ||||
|     'Login': '登录', | ||||
|     'Register': '注册', | ||||
|     'Email': '邮箱', | ||||
|     'Password': '密码', | ||||
|     'Confirm Password': '确认密码', | ||||
|     'Name': '姓名', | ||||
|     'Gender': '性别', | ||||
|     'Male': '男', | ||||
|     'Female': '女', | ||||
|     'Other': '其他', | ||||
|     'Birth Date': '出生日期', | ||||
|     'Verification Code': '验证码', | ||||
|     'Don\'t have an account?': '还没有账号?', | ||||
|     'Sign up now': '立即注册', | ||||
|     'Already have an account?': '已有账号?', | ||||
|     'Sign in now': '立即登录', | ||||
|     'Text Classification System': '中文文本分类系统', | ||||
|     'Password must be at least 8 characters long': '密码长度至少为8个字符', | ||||
|     'Passwords do not match': '两次输入的密码不一致', | ||||
|     'A verification code has been sent to your email. Please enter the 6-digit code below to complete registration.': '验证码已发送到您的邮箱,请在下方输入6位数验证码完成注册。', | ||||
|     'Verify': '验证', | ||||
|     'Logging in...': '登录中...', | ||||
|     'Sending verification code...': '发送验证码...', | ||||
|     'Verifying...': '验证中...', | ||||
|     'Ziyao Text Classification System': '子尧中文文本分类系统' | ||||
|   }; | ||||
| 
 | ||||
|   // 英文词汇表
 | ||||
|   const enTranslations = { | ||||
|     '登录': 'Login', | ||||
|     '注册': 'Register', | ||||
|     '邮箱': 'Email', | ||||
|     '邮箱(用作登录名)': 'Email (used as login name)', | ||||
|     '密码': 'Password', | ||||
|     '确认密码': 'Confirm Password', | ||||
|     '姓名': 'Name', | ||||
|     '性别': 'Gender', | ||||
|     '男': 'Male', | ||||
|     '女': 'Female', | ||||
|     '其他': 'Other', | ||||
|     '出生日期': 'Birth Date', | ||||
|     '验证码': 'Verification Code', | ||||
|     '还没有账号?': 'Don\'t have an account?', | ||||
|     '立即注册': 'Sign up now', | ||||
|     '已有账号?': 'Already have an account?', | ||||
|     '立即登录': 'Sign in now', | ||||
|     '中文文本分类系统': 'Chinese Text Classification System', | ||||
|     '文本分类系统登录': 'Text Classification System - Login', | ||||
|     '注册新账号': 'Register New Account', | ||||
|     '密码长度至少为8个字符': 'Password must be at least 8 characters long', | ||||
|     '两次输入的密码不一致': 'Passwords do not match', | ||||
|     '验证码已发送到您的邮箱,请在下方输入6位数验证码完成注册。': 'A verification code has been sent to your email. Please enter the 6-digit code below to complete registration.', | ||||
|     '验证': 'Verify', | ||||
|     '登录中...': 'Logging in...', | ||||
|     '发送验证码...': 'Sending verification code...', | ||||
|     '验证中...': 'Verifying...', | ||||
|     '子尧中文文本分类系统': 'Ziyao Text Classification System' | ||||
|   }; | ||||
| 
 | ||||
|   // 检查本地存储中的语言偏好
 | ||||
|   let currentLang = localStorage.getItem('language') || 'zh'; | ||||
|   updateLanguageButton(currentLang); | ||||
| 
 | ||||
|   // 如果当前是英文,则翻译页面
 | ||||
|   if (currentLang === 'en') { | ||||
|     translatePage(true); | ||||
|   } | ||||
| 
 | ||||
|   // 语言切换事件
 | ||||
|   languageSwitcher.addEventListener('click', function() { | ||||
|     currentLang = currentLang === 'zh' ? 'en' : 'zh'; | ||||
|     localStorage.setItem('language', currentLang); | ||||
|     updateLanguageButton(currentLang); | ||||
|     translatePage(currentLang === 'en'); | ||||
|   }); | ||||
| 
 | ||||
|   function updateLanguageButton(lang) { | ||||
|     languageSwitcher.textContent = lang === 'zh' ? 'EN' : 'CN'; | ||||
|   } | ||||
| 
 | ||||
|   function translatePage(toEnglish) { | ||||
|     // 获取所有文本节点
 | ||||
|     const translationMap = toEnglish ? zhTranslations : enTranslations; | ||||
| 
 | ||||
|     // 翻译标题
 | ||||
|     document.title = translateText(document.title, translationMap); | ||||
| 
 | ||||
|     // 翻译所有文本内容
 | ||||
|     translateNode(document.body, translationMap); | ||||
| 
 | ||||
|     // 翻译属性
 | ||||
|     translateAttributes(translationMap); | ||||
|   } | ||||
| 
 | ||||
|   function translateNode(node, translationMap) { | ||||
|     if (node.nodeType === 3) { // 文本节点
 | ||||
|       const text = node.nodeValue.trim(); | ||||
|       if (text && text.length > 0) { | ||||
|         const translated = translateText(text, translationMap); | ||||
|         if (translated !== text) { | ||||
|           node.nodeValue = node.nodeValue.replace(text, translated); | ||||
|         } | ||||
|       } | ||||
|     } else if (node.nodeType === 1) { // 元素节点
 | ||||
|       if (!node.classList.contains('no-translate')) { | ||||
|         // 遍历子节点
 | ||||
|         for (let i = 0; i < node.childNodes.length; i++) { | ||||
|           translateNode(node.childNodes[i], translationMap); | ||||
|         } | ||||
| 
 | ||||
|         // 翻译 placeholder
 | ||||
|         if (node.hasAttribute('placeholder')) { | ||||
|           const placeholder = node.getAttribute('placeholder'); | ||||
|           const translatedPlaceholder = translateText(placeholder, translationMap); | ||||
|           if (translatedPlaceholder !== placeholder) { | ||||
|             node.setAttribute('placeholder', translatedPlaceholder); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   function translateText(text, translationMap) { | ||||
|     return translationMap[text] || text; | ||||
|   } | ||||
| 
 | ||||
|   function translateAttributes(translationMap) { | ||||
|     // 翻译按钮文本、标题等
 | ||||
|     document.querySelectorAll('[data-translate]').forEach(el => { | ||||
|       const key = el.getAttribute('data-translate'); | ||||
|       if (key && translationMap[key]) { | ||||
|         el.textContent = translationMap[key]; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										87
									
								
								static/js/login.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								static/js/login.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| // 登录页面特定的JavaScript
 | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|   const loginForm = document.getElementById('loginForm'); | ||||
|   const alertBox = document.getElementById('alertBox'); | ||||
|   const passwordInput = document.getElementById('password'); | ||||
|   const togglePassword = document.getElementById('togglePassword'); | ||||
| 
 | ||||
|   // 显示/隐藏密码
 | ||||
|   if (togglePassword) { | ||||
|     togglePassword.addEventListener('click', function() { | ||||
|       const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; | ||||
|       passwordInput.setAttribute('type', type); | ||||
| 
 | ||||
|       // 切换图标
 | ||||
|       togglePassword.classList.toggle('fa-eye'); | ||||
|       togglePassword.classList.toggle('fa-eye-slash'); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // 登录表单提交
 | ||||
|   if (loginForm) { | ||||
|     loginForm.addEventListener('submit', function(e) { | ||||
|       e.preventDefault(); | ||||
| 
 | ||||
|       const email = document.getElementById('email').value; | ||||
|       const password = passwordInput.value; | ||||
|       const submitBtn = document.querySelector('button[type="submit"]'); | ||||
|       const currentLang = localStorage.getItem('language') || 'zh'; | ||||
| 
 | ||||
|       // 表单验证
 | ||||
|       if (!email || !password) { | ||||
|         showAlert('danger', currentLang === 'zh' ? '请填写所有必填字段' : 'Please fill in all required fields'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // 显示加载状态
 | ||||
|       const originalBtnText = submitBtn.innerHTML; | ||||
|       const loadingText = currentLang === 'zh' ? '登录中...' : 'Logging in...'; | ||||
|       submitBtn.disabled = true; | ||||
|       submitBtn.innerHTML = `<span class="spinner"></span>${loadingText}`; | ||||
| 
 | ||||
|       // 发送登录请求
 | ||||
|       $.ajax({ | ||||
|         url: '/auth/login', | ||||
|         type: 'POST', | ||||
|         contentType: 'application/json', | ||||
|         data: JSON.stringify({ | ||||
|           email: email, | ||||
|           password: password | ||||
|         }), | ||||
|         success: function(response) { | ||||
|           if (response.success) { | ||||
|             // 登录成功,跳转到主页
 | ||||
|             window.location.href = '/dashboard'; | ||||
|           } else { | ||||
|             // 显示错误信息
 | ||||
|             showAlert('danger', response.message); | ||||
|             submitBtn.disabled = false; | ||||
|             submitBtn.innerHTML = originalBtnText; | ||||
|           } | ||||
|         }, | ||||
|         error: function(xhr) { | ||||
|           let errorMsg = currentLang === 'zh' ? '登录失败,请稍后再试' : 'Login failed, please try again later'; | ||||
|           if (xhr.responseJSON && xhr.responseJSON.message) { | ||||
|             errorMsg = xhr.responseJSON.message; | ||||
|           } | ||||
|           showAlert('danger', errorMsg); | ||||
|           submitBtn.disabled = false; | ||||
|           submitBtn.innerHTML = originalBtnText; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // 显示提示信息
 | ||||
|   function showAlert(type, message) { | ||||
|     alertBox.className = `alert alert-${type}`; | ||||
|     alertBox.textContent = message; | ||||
|     alertBox.classList.remove('d-none'); | ||||
| 
 | ||||
|     // 滚动到顶部
 | ||||
|     window.scrollTo({ | ||||
|       top: 0, | ||||
|       behavior: 'smooth' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										201
									
								
								static/js/register.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								static/js/register.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,201 @@ | ||||
| // 注册页面特定的JavaScript
 | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|   const userInfoForm = document.getElementById('userInfoForm'); | ||||
|   const codeForm = document.getElementById('codeForm'); | ||||
|   const alertBox = document.getElementById('alertBox'); | ||||
|   const passwordInput = document.getElementById('password'); | ||||
|   const confirmPasswordInput = document.getElementById('confirmPassword'); | ||||
|   const togglePassword = document.getElementById('togglePassword'); | ||||
|   const toggleConfirmPassword = document.getElementById('toggleConfirmPassword'); | ||||
| 
 | ||||
|   let registeredEmail = ''; | ||||
| 
 | ||||
|   // 显示/隐藏密码
 | ||||
|   if (togglePassword) { | ||||
|     togglePassword.addEventListener('click', function() { | ||||
|       const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password'; | ||||
|       passwordInput.setAttribute('type', type); | ||||
| 
 | ||||
|       // 切换图标
 | ||||
|       togglePassword.classList.toggle('fa-eye'); | ||||
|       togglePassword.classList.toggle('fa-eye-slash'); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   if (toggleConfirmPassword) { | ||||
|     toggleConfirmPassword.addEventListener('click', function() { | ||||
|       const type = confirmPasswordInput.getAttribute('type') === 'password' ? 'text' : 'password'; | ||||
|       confirmPasswordInput.setAttribute('type', type); | ||||
| 
 | ||||
|       // 切换图标
 | ||||
|       toggleConfirmPassword.classList.toggle('fa-eye'); | ||||
|       toggleConfirmPassword.classList.toggle('fa-eye-slash'); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // 密码一致性检查
 | ||||
|   if (confirmPasswordInput) { | ||||
|     confirmPasswordInput.addEventListener('input', function() { | ||||
|       const password = passwordInput.value; | ||||
|       const confirmPassword = confirmPasswordInput.value; | ||||
| 
 | ||||
|       if (password !== confirmPassword) { | ||||
|         confirmPasswordInput.classList.add('is-invalid'); | ||||
|         document.getElementById('passwordMismatch').style.display = 'block'; | ||||
|       } else { | ||||
|         confirmPasswordInput.classList.remove('is-invalid'); | ||||
|         document.getElementById('passwordMismatch').style.display = 'none'; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // 注册表单提交
 | ||||
|   if (userInfoForm) { | ||||
|     userInfoForm.addEventListener('submit', function(e) { | ||||
|       e.preventDefault(); | ||||
| 
 | ||||
|       const email = document.getElementById('email').value; | ||||
|       const password = passwordInput.value; | ||||
|       const confirmPassword = confirmPasswordInput.value; | ||||
|       const name = document.getElementById('name').value; | ||||
|       const gender = document.querySelector('input[name="gender"]:checked')?.value; | ||||
|       const birthDate = document.getElementById('birthDate').value; | ||||
|       const registerBtn = document.getElementById('registerBtn'); | ||||
|       const currentLang = localStorage.getItem('language') || 'zh'; | ||||
| 
 | ||||
|       // 表单验证
 | ||||
|       if (!email || !password || !confirmPassword || !name) { | ||||
|         showAlert('danger', currentLang === 'zh' ? '请填写所有必填字段' : 'Please fill in all required fields'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (password !== confirmPassword) { | ||||
|         showAlert('danger', currentLang === 'zh' ? '两次输入的密码不一致' : 'Passwords do not match'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (password.length < 8) { | ||||
|         showAlert('danger', currentLang === 'zh' ? '密码长度至少为8个字符' : 'Password must be at least 8 characters long'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // 显示加载状态
 | ||||
|       const loadingText = currentLang === 'zh' ? '发送验证码...' : 'Sending verification code...'; | ||||
|       registerBtn.disabled = true; | ||||
|       registerBtn.innerHTML = `<span class="spinner"></span>${loadingText}`; | ||||
| 
 | ||||
|       // 收集表单数据
 | ||||
|       const formData = { | ||||
|         email: email, | ||||
|         password: password, | ||||
|         name: name, | ||||
|         gender: gender || '其他', | ||||
|         birth_date: birthDate | ||||
|       }; | ||||
| 
 | ||||
|       registeredEmail = email; | ||||
| 
 | ||||
|       // 发送注册请求
 | ||||
|       $.ajax({ | ||||
|         url: '/auth/register', | ||||
|         type: 'POST', | ||||
|         contentType: 'application/json', | ||||
|         data: JSON.stringify(formData), | ||||
|         success: function(response) { | ||||
|           if (response.success) { | ||||
|             // 显示验证码输入表单
 | ||||
|             showAlert('success', response.message); | ||||
|             document.getElementById('registrationForm').classList.add('d-none'); | ||||
|             document.getElementById('verificationForm').classList.remove('d-none'); | ||||
|           } else { | ||||
|             // 显示错误信息
 | ||||
|             showAlert('danger', response.message); | ||||
|             registerBtn.disabled = false; | ||||
|             registerBtn.innerHTML = currentLang === 'zh' ? '注册' : 'Register'; | ||||
|           } | ||||
|         }, | ||||
|         error: function(xhr) { | ||||
|           let errorMsg = currentLang === 'zh' ? '注册失败,请稍后再试' : 'Registration failed, please try again later'; | ||||
|           if (xhr.responseJSON && xhr.responseJSON.message) { | ||||
|             errorMsg = xhr.responseJSON.message; | ||||
|           } | ||||
|           showAlert('danger', errorMsg); | ||||
|           registerBtn.disabled = false; | ||||
|           registerBtn.innerHTML = currentLang === 'zh' ? '注册' : 'Register'; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // 验证码表单提交
 | ||||
|   if (codeForm) { | ||||
|     codeForm.addEventListener('submit', function(e) { | ||||
|       e.preventDefault(); | ||||
| 
 | ||||
|       const verificationCode = document.getElementById('verificationCode').value; | ||||
|       const verifyBtn = document.getElementById('verifyBtn'); | ||||
|       const currentLang = localStorage.getItem('language') || 'zh'; | ||||
| 
 | ||||
|       // 表单验证
 | ||||
|       if (!verificationCode) { | ||||
|         showAlert('danger', currentLang === 'zh' ? '请输入验证码' : 'Please enter the verification code'); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // 显示加载状态
 | ||||
|       const loadingText = currentLang === 'zh' ? '验证中...' : 'Verifying...'; | ||||
|       verifyBtn.disabled = true; | ||||
|       verifyBtn.innerHTML = `<span class="spinner"></span>${loadingText}`; | ||||
| 
 | ||||
|       // 收集验证码数据
 | ||||
|       const verificationData = { | ||||
|         email: registeredEmail, | ||||
|         code: verificationCode | ||||
|       }; | ||||
| 
 | ||||
|       // 发送验证请求
 | ||||
|       $.ajax({ | ||||
|         url: '/auth/verify', | ||||
|         type: 'POST', | ||||
|         contentType: 'application/json', | ||||
|         data: JSON.stringify(verificationData), | ||||
|         success: function(response) { | ||||
|           if (response.success) { | ||||
|             // 注册成功,显示成功消息并跳转到登录页
 | ||||
|             showAlert('success', response.message); | ||||
|             setTimeout(function() { | ||||
|               window.location.href = '/'; | ||||
|             }, 2000); | ||||
|           } else { | ||||
|             // 显示错误信息
 | ||||
|             showAlert('danger', response.message); | ||||
|             verifyBtn.disabled = false; | ||||
|             verifyBtn.innerHTML = currentLang === 'zh' ? '验证' : 'Verify'; | ||||
|           } | ||||
|         }, | ||||
|         error: function(xhr) { | ||||
|           let errorMsg = currentLang === 'zh' ? '验证失败,请稍后再试' : 'Verification failed, please try again later'; | ||||
|           if (xhr.responseJSON && xhr.responseJSON.message) { | ||||
|             errorMsg = xhr.responseJSON.message; | ||||
|           } | ||||
|           showAlert('danger', errorMsg); | ||||
|           verifyBtn.disabled = false; | ||||
|           verifyBtn.innerHTML = currentLang === 'zh' ? '验证' : 'Verify'; | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // 显示提示信息
 | ||||
|   function showAlert(type, message) { | ||||
|     alertBox.className = `alert alert-${type}`; | ||||
|     alertBox.textContent = message; | ||||
|     alertBox.classList.remove('d-none'); | ||||
| 
 | ||||
|     // 滚动到顶部
 | ||||
|     window.scrollTo({ | ||||
|       top: 0, | ||||
|       behavior: 'smooth' | ||||
|     }); | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										38
									
								
								static/js/theme-switcher.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								static/js/theme-switcher.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| // 主题切换功能
 | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|   const themeSwitcher = document.getElementById('theme-switcher'); | ||||
|   const themeIcon = document.getElementById('theme-icon'); | ||||
| 
 | ||||
|   // 检查本地存储中的主题偏好
 | ||||
|   const savedTheme = localStorage.getItem('theme'); | ||||
| 
 | ||||
|   // 检查系统偏好
 | ||||
|   const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||||
| 
 | ||||
|   // 设置初始主题
 | ||||
|   if (savedTheme) { | ||||
|     document.body.classList.toggle('dark-mode', savedTheme === 'dark'); | ||||
|     updateThemeIcon(savedTheme === 'dark'); | ||||
|   } else { | ||||
|     document.body.classList.toggle('dark-mode', prefersDarkMode); | ||||
|     updateThemeIcon(prefersDarkMode); | ||||
|   } | ||||
| 
 | ||||
|   // 主题切换事件
 | ||||
|   themeSwitcher.addEventListener('click', function() { | ||||
|     const isDarkMode = document.body.classList.toggle('dark-mode'); | ||||
|     localStorage.setItem('theme', isDarkMode ? 'dark' : 'light'); | ||||
|     updateThemeIcon(isDarkMode); | ||||
|   }); | ||||
| 
 | ||||
|   function updateThemeIcon(isDarkMode) { | ||||
|     if (isDarkMode) { | ||||
|       themeIcon.classList.remove('fa-moon'); | ||||
|       themeIcon.classList.add('fa-sun'); | ||||
|     } else { | ||||
|       themeIcon.classList.remove('fa-sun'); | ||||
|       themeIcon.classList.add('fa-moon'); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
							
								
								
									
										20
									
								
								static/js/theme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								static/js/theme.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| // static/js/theme.js
 | ||||
| document.addEventListener('DOMContentLoaded', function() { | ||||
|     // Create theme toggle button
 | ||||
|     const themeToggle = document.createElement('button'); | ||||
|     themeToggle.className = 'theme-toggle'; | ||||
|     themeToggle.innerHTML = '🌓'; | ||||
|     document.body.appendChild(themeToggle); | ||||
| 
 | ||||
|     // Theme toggle functionality
 | ||||
|     themeToggle.addEventListener('click', function() { | ||||
|         const currentTheme = document.documentElement.getAttribute('data-theme'); | ||||
|         const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; | ||||
|         document.documentElement.setAttribute('data-theme', newTheme); | ||||
|         localStorage.setItem('theme', newTheme); | ||||
|     }); | ||||
| 
 | ||||
|     // Set initial theme
 | ||||
|     const savedTheme = localStorage.getItem('theme') || 'light'; | ||||
|     document.documentElement.setAttribute('data-theme', savedTheme); | ||||
| }); | ||||
							
								
								
									
										238
									
								
								templates/dashboard.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								templates/dashboard.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,238 @@ | ||||
| <!-- templates/dashboard.html --> | ||||
| <!DOCTYPE html> | ||||
| <html lang="zh-CN"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>中文文本分类系统</title> | ||||
|     <!-- Bootstrap 5 CSS --> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <!-- Font Awesome 图标 --> | ||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | ||||
|     <!-- 自定义CSS --> | ||||
|     <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> | ||||
| </head> | ||||
| <body> | ||||
|     <!-- 顶部导航栏 --> | ||||
|     <nav class="navbar navbar-expand-lg fixed-top top-navbar"> | ||||
|         <div class="container-fluid"> | ||||
|             <!-- 侧边栏切换按钮 --> | ||||
|             <button class="btn btn-link text-white me-3 d-lg-none" id="sidebar-toggle"> | ||||
|                 <i class="fas fa-bars"></i> | ||||
|             </button> | ||||
| 
 | ||||
|             <!-- 网站标题 --> | ||||
|             <a class="navbar-brand text-white" href="#"> | ||||
|                 <i class="fas fa-file-alt me-2"></i> | ||||
|                 中文文本分类系统 | ||||
|             </a> | ||||
| 
 | ||||
|             <!-- 右侧用户信息 --> | ||||
|             <div class="ms-auto d-flex align-items-center user-dropdown"> | ||||
|                 <div class="dropdown"> | ||||
|                     <a class="nav-link dropdown-toggle text-white" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> | ||||
|                         <i class="fas fa-user-circle me-1"></i> | ||||
|                         <span>{{ session.user_name if session.user_name else session.user_email }}</span> | ||||
|                     </a> | ||||
|                     <ul class="dropdown-menu" aria-labelledby="userDropdown"> | ||||
|                         <li><a class="dropdown-item" href="#"><i class="fas fa-user me-2"></i>个人资料</a></li> | ||||
|                         <li><hr class="dropdown-divider"></li> | ||||
|                         <li><a class="dropdown-item" href="{{ url_for('auth.logout') }}"><i class="fas fa-sign-out-alt me-2"></i>退出登录</a></li> | ||||
|                     </ul> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </nav> | ||||
| 
 | ||||
|     <!-- 侧边栏 --> | ||||
|     <div class="sidebar"> | ||||
|         <div class="px-3 mb-3"> | ||||
|             <div class="text-muted small">导航菜单</div> | ||||
|         </div> | ||||
| 
 | ||||
|         <ul class="nav flex-column mb-auto"> | ||||
|             <li class="nav-item"> | ||||
|                 <a href="#" class="sidebar-link active" data-page="classify"> | ||||
|                     <i class="fas fa-file-alt"></i> | ||||
|                     <span>文本分类</span> | ||||
|                 </a> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|                 <a href="#" class="sidebar-link" data-page="documents"> | ||||
|                     <i class="fas fa-list-alt"></i> | ||||
|                     <span>已处理文本</span> | ||||
|                 </a> | ||||
|             </li> | ||||
|             <li class="nav-item"> | ||||
|                 <a href="#" class="sidebar-link" data-page="batch"> | ||||
|                     <i class="fas fa-layer-group"></i> | ||||
|                     <span>批量文本分类</span> | ||||
|                 </a> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- 主内容区 --> | ||||
|     <div class="main-content"> | ||||
|         <!-- 内容区域 - 根据选择的页面显示不同内容 --> | ||||
|         <div id="page-content"> | ||||
|             <!-- 文本分类页面 (默认显示) --> | ||||
|             <div class="page-section" id="classify-section"> | ||||
|                 <h2 class="mb-4">文本分类</h2> | ||||
| 
 | ||||
|                 <div class="content-card"> | ||||
|                     <div class="row"> | ||||
|                         <div class="col-md-6"> | ||||
|                             <div class="form-group mb-3"> | ||||
|                                 <label for="text-input" class="form-label">输入文本内容</label> | ||||
|                                 <textarea class="form-control" id="text-input" rows="10" placeholder="请输入或粘贴需要分类的中文文本..."></textarea> | ||||
|                             </div> | ||||
|                             <div class="mb-3"> | ||||
|                                 <button id="classify-text-btn" class="btn btn-primary me-2"> | ||||
|                                     <i class="fas fa-search me-1"></i> 分类文本 | ||||
|                                 </button> | ||||
|                                 <button id="clear-text-btn" class="btn btn-outline-secondary"> | ||||
|                                     <i class="fas fa-eraser me-1"></i> 清空 | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div class="col-md-6"> | ||||
|                             <div class="border rounded p-3 h-100" id="classification-result"> | ||||
|                                 <h5 class="text-center text-muted"> | ||||
|                                     <i class="fas fa-info-circle me-2"></i> | ||||
|                                     分类结果将在这里显示 | ||||
|                                 </h5> | ||||
|                                 <div class="text-center mt-4"> | ||||
|                                     <i class="fas fa-arrow-left text-muted me-2"></i> | ||||
|                                     请在左侧输入文本或上传文件 | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
| 
 | ||||
|                 <div class="content-card mt-4"> | ||||
|                     <h4 class="mb-3">上传文本文件</h4> | ||||
|                     <div class="mb-3"> | ||||
|                         <div class="input-group"> | ||||
|                             <input type="file" class="form-control" id="file-upload" accept=".txt"> | ||||
|                             <button class="btn btn-primary" id="upload-file-btn"> | ||||
|                                 <i class="fas fa-upload me-1"></i> 上传并分类 | ||||
|                             </button> | ||||
|                         </div> | ||||
|                         <div class="form-text">支持上传 .txt 格式的文本文件</div> | ||||
|                     </div> | ||||
|                     <div id="file-result" class="mt-3" style="display: none;"> | ||||
|                         <!-- 文件分类结果将在这里显示 --> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- 已处理文本页面 (默认隐藏) --> | ||||
|             <div class="page-section d-none" id="documents-section"> | ||||
|                 <h2 class="mb-4">已处理文本</h2> | ||||
| 
 | ||||
|                 <div class="content-card"> | ||||
|                     <div class="row mb-3"> | ||||
|                         <div class="col-md-6"> | ||||
|                             <div class="input-group"> | ||||
|                                 <label class="input-group-text" for="category-filter">分类筛选</label> | ||||
|                                 <select class="form-select" id="category-filter"> | ||||
|                                     <option value="all" selected>全部</option> | ||||
|                                     <!-- 分类选项将通过JavaScript动态添加 --> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div class="col-md-6 text-md-end"> | ||||
|                             <div class="input-group justify-content-md-end"> | ||||
|                                 <label class="input-group-text" for="per-page-select">每页显示</label> | ||||
|                                 <select class="form-select" style="max-width: 80px;" id="per-page-select"> | ||||
|                                     <option value="10">10</option> | ||||
|                                     <option value="25">25</option> | ||||
|                                     <option value="50">50</option> | ||||
|                                     <option value="100">100</option> | ||||
|                                 </select> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div class="mb-3"> | ||||
|                         <button id="download-selected-btn" class="btn btn-success disabled"> | ||||
|                             <i class="fas fa-download me-1"></i> 下载选中文件 | ||||
|                         </button> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div class="table-responsive"> | ||||
|                         <table class="table table-hover" id="documents-table"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th width="40"> | ||||
|                                         <div class="form-check"> | ||||
|                                             <input class="form-check-input" type="checkbox" id="select-all-docs"> | ||||
|                                         </div> | ||||
|                                     </th> | ||||
|                                     <th>文件名</th> | ||||
|                                     <th>分类</th> | ||||
|                                     <th>大小</th> | ||||
|                                     <th>分类时间</th> | ||||
|                                     <th>操作</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 <!-- 文档列表将通过JavaScript动态添加 --> | ||||
|                                 <tr> | ||||
|                                     <td colspan="6" class="text-center py-4"> | ||||
|                                         正在加载文档... | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div class="d-flex justify-content-between align-items-center mt-3" id="pagination-container"> | ||||
|                         <!-- 分页控件将通过JavaScript动态添加 --> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <!-- 批量文本分类页面 (默认隐藏) --> | ||||
|             <div class="page-section d-none" id="batch-section"> | ||||
|                 <h2 class="mb-4">批量文本分类</h2> | ||||
| 
 | ||||
|                 <div class="content-card"> | ||||
|                     <div class="text-center py-4 mb-4 border rounded"> | ||||
|                         <i class="fas fa-upload fa-3x mb-3 text-primary"></i> | ||||
|                         <h4>上传压缩文件</h4> | ||||
|                         <p class="text-muted">支持ZIP/RAR格式,最大10MB,包含多个TXT文件</p> | ||||
|                         <div class="mb-3 mx-auto" style="max-width: 400px;"> | ||||
|                             <input type="file" class="form-control" id="batch-file-upload" accept=".zip,.rar"> | ||||
|                         </div> | ||||
|                         <button class="btn btn-primary" id="upload-batch-btn"> | ||||
|                             <i class="fas fa-cogs me-1"></i> 上传并批量分类 | ||||
|                         </button> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div id="batch-result" style="display: none;"> | ||||
|                         <!-- 批量处理结果将在这里显示 --> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- 加载指示器 --> | ||||
|     <div class="spinner-overlay" id="loading-overlay"> | ||||
|         <div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;"> | ||||
|             <span class="visually-hidden">加载中...</span> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Bootstrap 5 JS with Popper --> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> | ||||
|     <!-- 自定义JavaScript --> | ||||
|     <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script> | ||||
| </body> | ||||
| </html> | ||||
| 
 | ||||
							
								
								
									
										31
									
								
								templates/error.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								templates/error.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| <!-- templates/error.html --> | ||||
| <!DOCTYPE html> | ||||
| <html lang="zh-CN"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>错误 - 中文文本分类系统</title> | ||||
|     <!-- Bootstrap 5 CSS --> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <!-- Font Awesome 图标 --> | ||||
|     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | ||||
| </head> | ||||
| <body class="bg-light"> | ||||
|     <div class="container py-5"> | ||||
|         <div class="row justify-content-center"> | ||||
|             <div class="col-md-8 col-lg-6"> | ||||
|                 <div class="card shadow-sm border-0"> | ||||
|                     <div class="card-body text-center p-5"> | ||||
|                         <i class="fas fa-exclamation-triangle text-danger fa-4x mb-4"></i> | ||||
|                         <h2 class="mb-4">出错了!</h2> | ||||
|                         <p class="mb-4 lead">{{ error }}</p> | ||||
|                         <a href="{{ url_for('index') }}" class="btn btn-primary"> | ||||
|                             <i class="fas fa-home me-2"></i>返回主页 | ||||
|                         </a> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										59
									
								
								templates/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								templates/login.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| <!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 href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | ||||
|     <link href="/static/css/auth.css" rel="stylesheet"> | ||||
| </head> | ||||
| <body> | ||||
|     <!-- 主题切换器 --> | ||||
|     <div class="theme-switcher" id="theme-switcher"> | ||||
|         <i class="fas fa-moon" id="theme-icon"></i> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- 语言切换器 --> | ||||
|     <div class="language-switcher" id="language-switcher">EN</div> | ||||
| 
 | ||||
|         <div class="content-wrapper"> | ||||
|         <div class="auth-container"> | ||||
|             <div class="logo-container"> | ||||
|                 <img src="https://git.sq0715.com/qin/icon/raw/branch/main/AI-icon.png" alt="Logo"> | ||||
|             </div> | ||||
|             <h2>文本分类系统登录</h2> | ||||
|             <div id="alertBox" class="alert alert-danger d-none" role="alert"></div> | ||||
|             <form id="loginForm"> | ||||
|                 <div class="form-group"> | ||||
|                     <label for="email">邮箱</label> | ||||
|                     <input type="email" class="form-control" id="email" name="email" required> | ||||
|                 </div> | ||||
|                 <div class="form-group"> | ||||
|                     <label for="password">密码</label> | ||||
|                     <div class="password-container"> | ||||
|                         <input type="password" class="form-control" id="password" name="password" required> | ||||
|                         <i class="fas fa-eye-slash password-toggle" id="togglePassword"></i> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="form-group"> | ||||
|                     <button type="submit" class="btn btn-primary">登录</button> | ||||
|                 </div> | ||||
|             </form> | ||||
|             <div class="text-center mt-3"> | ||||
|                 <p>还没有账号?<a href="/register">立即注册</a></p> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <footer> | ||||
|         <p>© 2025 子尧中文文本分类系统 | 本科毕业设计作品</p> | ||||
|     </footer> | ||||
| 
 | ||||
|     <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script> | ||||
|     <script src="/static/js/theme-switcher.js"></script> | ||||
|     <script src="/static/js/language-switcher.js"></script> | ||||
|     <script src="/static/js/login.js"></script> | ||||
| </body> | ||||
| </html> | ||||
| 
 | ||||
							
								
								
									
										114
									
								
								templates/register.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								templates/register.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,114 @@ | ||||
| <!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 href="https://cdn.bootcdn.net/ajax/libs/bootstrap/5.1.3/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <link href="https://cdn.bootcdn.net/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | ||||
|     <link href="/static/css/auth.css" rel="stylesheet"> | ||||
| </head> | ||||
| <body> | ||||
|     <!-- 主题切换器 --> | ||||
|     <div class="theme-switcher" id="theme-switcher"> | ||||
|         <i class="fas fa-moon" id="theme-icon"></i> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- 语言切换器 --> | ||||
|     <div class="language-switcher" id="language-switcher">EN</div> | ||||
| 
 | ||||
|     <div class="content-wrapper"> | ||||
|         <div class="auth-container"> | ||||
|             <div class="logo-container"> | ||||
|                 <img src="https://git.sq0715.com/qin/icon/raw/branch/main/AI-icon.png" alt="Logo"> | ||||
|             </div> | ||||
|             <h2>注册新账号</h2> | ||||
|             <div id="alertBox" class="alert d-none" role="alert"></div> | ||||
| 
 | ||||
|             <div id="registrationForm"> | ||||
|                 <form id="userInfoForm"> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="email">邮箱(用作登录名)</label> | ||||
|                         <input type="email" class="form-control" id="email" name="email" required> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="password">密码</label> | ||||
|                         <div class="password-container"> | ||||
|                             <input type="password" class="form-control" id="password" name="password" required | ||||
|                                 pattern=".{8,}" title="密码长度至少为8个字符"> | ||||
|                             <i class="fas fa-eye-slash password-toggle" id="togglePassword"></i> | ||||
|                         </div> | ||||
|                         <div class="helper-text">密码长度至少为8个字符</div> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="confirmPassword">确认密码</label> | ||||
|                         <div class="password-container"> | ||||
|                             <input type="password" class="form-control" id="confirmPassword" name="confirmPassword" required> | ||||
|                             <i class="fas fa-eye-slash password-toggle" id="toggleConfirmPassword"></i> | ||||
|                         </div> | ||||
|                         <div class="invalid-feedback" id="passwordMismatch">两次输入的密码不一致</div> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="name">姓名</label> | ||||
|                         <input type="text" class="form-control" id="name" name="name" required> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label>性别</label> | ||||
|                         <div class="radio-group"> | ||||
|                             <div class="radio-option"> | ||||
|                                 <input class="form-check-input" type="radio" name="gender" id="male" value="男" checked> | ||||
|                                 <label class="form-check-label" for="male">男</label> | ||||
|                             </div> | ||||
|                             <div class="radio-option"> | ||||
|                                 <input class="form-check-input" type="radio" name="gender" id="female" value="女"> | ||||
|                                 <label class="form-check-label" for="female">女</label> | ||||
|                             </div> | ||||
|                             <div class="radio-option"> | ||||
|                                 <input class="form-check-input" type="radio" name="gender" id="other" value="其他"> | ||||
|                                 <label class="form-check-label" for="other">其他</label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="birthDate">出生日期</label> | ||||
|                         <input type="date" class="form-control" id="birthDate" name="birthDate"> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <button type="submit" id="registerBtn" class="btn btn-primary">注册</button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
| 
 | ||||
|             <div id="verificationForm" class="d-none"> | ||||
|                 <p class="alert alert-info"> | ||||
|                     验证码已发送到您的邮箱,请在下方输入6位数验证码完成注册。 | ||||
|                 </p> | ||||
|                 <form id="codeForm"> | ||||
|                     <div class="form-group"> | ||||
|                         <label for="verificationCode">验证码</label> | ||||
|                         <input type="text" class="form-control" id="verificationCode" name="verificationCode" | ||||
|                             required pattern="[0-9]{6}" maxlength="6"> | ||||
|                     </div> | ||||
|                     <div class="form-group"> | ||||
|                         <button type="submit" id="verifyBtn" class="btn btn-success">验证</button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="text-center mt-3"> | ||||
|                 <p>已有账号?<a href="/">立即登录</a></p> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <footer> | ||||
|         <p>© 2025 子尧中文文本分类系统 | 本科毕业设计作品</p> | ||||
|     </footer> | ||||
| 
 | ||||
|     <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script> | ||||
|     <script src="/static/js/theme-switcher.js"></script> | ||||
|     <script src="/static/js/language-switcher.js"></script> | ||||
|     <script src="/static/js/register.js"></script> | ||||
| </body> | ||||
| </html> | ||||
| 
 | ||||
							
								
								
									
										
											BIN
										
									
								
								utils/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								utils/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										0
									
								
								utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										22
									
								
								utils/db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								utils/db.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| import mysql.connector | ||||
| from mysql.connector import Error | ||||
| import config | ||||
| 
 | ||||
| def get_db_connection(): | ||||
|     """创建数据库连接""" | ||||
|     try: | ||||
|         conn = mysql.connector.connect( | ||||
|             host=config.DB_HOST, | ||||
|             user=config.DB_USER, | ||||
|             password=config.DB_PASSWORD, | ||||
|             database=config.DB_NAME | ||||
|         ) | ||||
|         return conn | ||||
|     except Error as e: | ||||
|         print(f"数据库连接错误: {e}") | ||||
|         return None | ||||
| 
 | ||||
| def close_connection(conn): | ||||
|     """关闭数据库连接""" | ||||
|     if conn: | ||||
|         conn.close() | ||||
							
								
								
									
										64
									
								
								utils/email_sender.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								utils/email_sender.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| import smtplib | ||||
| from email.mime.text import MIMEText | ||||
| from email.mime.multipart import MIMEMultipart | ||||
| from email.header import Header | ||||
| import config | ||||
| import traceback | ||||
| 
 | ||||
| 
 | ||||
| def send_verification_email(recipient_email, verification_code): | ||||
|     """发送验证邮件""" | ||||
|     print(f"尝试向 {recipient_email} 发送验证码: {verification_code}") | ||||
| 
 | ||||
|     # 创建邮件对象 | ||||
|     message = MIMEMultipart() | ||||
|     message['From'] = f"{config.EMAIL_FROM_NAME} <{config.EMAIL_FROM}>" | ||||
|     message['To'] = recipient_email | ||||
|     message['Subject'] = Header('验证您的文本分类系统账户', 'utf-8') | ||||
| 
 | ||||
|     # 邮件正文 | ||||
|     html_content = f""" | ||||
|     <html> | ||||
|     <body> | ||||
|         <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;"> | ||||
|             <h2>欢迎注册文本分类系统</h2> | ||||
|             <p>感谢您注册我们的文本分类系统。请使用下面的验证码完成注册流程:</p> | ||||
|             <div style="background-color: #f5f5f5; padding: 15px; font-size: 24px; text-align: center; letter-spacing: 5px; font-weight: bold; margin: 20px 0;"> | ||||
|                 {verification_code} | ||||
|             </div> | ||||
|             <p>此验证码将在30分钟内有效。</p> | ||||
|             <p>如果您没有注册此账户,请忽略此邮件。</p> | ||||
|             <p>谢谢,<br>文本分类系统团队</p> | ||||
|         </div> | ||||
|     </body> | ||||
|     </html> | ||||
|     """ | ||||
| 
 | ||||
|     message.attach(MIMEText(html_content, 'html', 'utf-8')) | ||||
| 
 | ||||
|     try: | ||||
|         print(f"连接到邮件服务器: {config.EMAIL_HOST}:{config.EMAIL_PORT}") | ||||
| 
 | ||||
|         # 连接邮件服务器 | ||||
|         smtp = smtplib.SMTP(config.EMAIL_HOST, config.EMAIL_PORT, timeout=10) | ||||
| 
 | ||||
|         # 打印服务器响应 | ||||
|         smtp.set_debuglevel(1) | ||||
| 
 | ||||
|         print(f"开始TLS连接: {config.EMAIL_USE_TLS}") | ||||
|         if config.EMAIL_USE_TLS: | ||||
|             smtp.starttls() | ||||
| 
 | ||||
|         print(f"尝试登录: {config.EMAIL_USERNAME}") | ||||
|         smtp.login(config.EMAIL_USERNAME, config.EMAIL_PASSWORD) | ||||
| 
 | ||||
|         print(f"发送邮件从 {config.EMAIL_FROM} 到 {recipient_email}") | ||||
|         # 发送邮件 | ||||
|         smtp.sendmail(config.EMAIL_FROM, recipient_email, message.as_string()) | ||||
|         smtp.quit() | ||||
|         print("邮件发送成功") | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         print(f"邮件发送失败: {e}") | ||||
|         print(traceback.format_exc())  # 打印完整的错误堆栈 | ||||
|         return False | ||||
							
								
								
									
										153
									
								
								utils/model_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								utils/model_service.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,153 @@ | ||||
| # utils/model_service.py | ||||
| import os | ||||
| import jieba | ||||
| import numpy as np | ||||
| import pickle | ||||
| from tensorflow.keras.models import load_model | ||||
| from tensorflow.keras.preprocessing.sequence import pad_sequences | ||||
| import logging | ||||
| 
 | ||||
| 
 | ||||
| class TextClassificationModel: | ||||
|     """中文文本分类模型服务封装类""" | ||||
| 
 | ||||
|     # 类别列表 - 与模型训练时保持一致 | ||||
|     CATEGORIES = ["体育", "娱乐", "家居", "彩票", "房产", "教育", | ||||
|                   "时尚", "时政", "星座", "游戏", "社会", "科技", "股票", "财经"] | ||||
| 
 | ||||
|     def __init__(self, model_path=None, tokenizer_path=None, max_length=500): | ||||
|         """初始化模型服务 | ||||
| 
 | ||||
|         Args: | ||||
|             model_path (str): 模型文件路径,默认为项目根目录下的trained_model.h5 | ||||
|             tokenizer_path (str): 分词器文件路径,默认为项目根目录下的tokenizer.pickle | ||||
|             max_length (int): 文本序列最大长度,默认为500 | ||||
|         """ | ||||
|         self.model_path = model_path or os.path.join(os.path.dirname(os.path.dirname( | ||||
|             os.path.abspath(__file__))), 'model', 'trained_model.h5') | ||||
|         self.tokenizer_path = tokenizer_path or os.path.join(os.path.dirname(os.path.dirname( | ||||
|             os.path.abspath(__file__))), 'model', 'tokenizer.pickle') | ||||
|         self.max_length = max_length | ||||
|         self.model = None | ||||
|         self.tokenizer = None | ||||
|         self.is_initialized = False | ||||
| 
 | ||||
|         # 设置日志 | ||||
|         self.logger = logging.getLogger(__name__) | ||||
| 
 | ||||
|     def initialize(self): | ||||
|         """初始化并加载模型和分词器""" | ||||
|         try: | ||||
|             self.logger.info("开始加载文本分类模型...") | ||||
|             # 加载模型 | ||||
|             self.model = load_model(self.model_path) | ||||
|             self.logger.info("模型加载成功") | ||||
| 
 | ||||
|             # 加载tokenizer | ||||
|             with open(self.tokenizer_path, 'rb') as handle: | ||||
|                 self.tokenizer = pickle.load(handle) | ||||
|             self.logger.info("Tokenizer加载成功") | ||||
| 
 | ||||
|             self.is_initialized = True | ||||
|             self.logger.info("模型初始化完成") | ||||
|             return True | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"模型初始化失败: {str(e)}") | ||||
|             self.is_initialized = False | ||||
|             return False | ||||
| 
 | ||||
|     def preprocess_text(self, text): | ||||
|         """对文本进行预处理 | ||||
| 
 | ||||
|         Args: | ||||
|             text (str): 待处理的原始文本 | ||||
| 
 | ||||
|         Returns: | ||||
|             str: 处理后的文本 | ||||
|         """ | ||||
|         # 使用jieba进行分词 | ||||
|         tokens = jieba.lcut(text) | ||||
|         # 将分词结果用空格连接成字符串 | ||||
|         return " ".join(tokens) | ||||
| 
 | ||||
|     def classify_text(self, text): | ||||
|         """对文本进行分类 | ||||
| 
 | ||||
|         Args: | ||||
|             text (str): 待分类的文本 | ||||
| 
 | ||||
|         Returns: | ||||
|             dict: 分类结果,包含类别标签和置信度 | ||||
|         """ | ||||
|         if not self.is_initialized: | ||||
|             success = self.initialize() | ||||
|             if not success: | ||||
|                 return {"success": False, "error": "模型初始化失败"} | ||||
| 
 | ||||
|         try: | ||||
|             # 文本预处理 | ||||
|             processed_text = self.preprocess_text(text) | ||||
| 
 | ||||
|             # 转换为序列 | ||||
|             sequence = self.tokenizer.texts_to_sequences([processed_text]) | ||||
| 
 | ||||
|             # 填充序列 | ||||
|             padded_sequence = pad_sequences(sequence, maxlen=self.max_length, padding="post") | ||||
| 
 | ||||
|             # 预测 | ||||
|             predictions = self.model.predict(padded_sequence) | ||||
| 
 | ||||
|             # 获取预测类别索引和置信度 | ||||
|             predicted_index = np.argmax(predictions, axis=1)[0] | ||||
|             confidence = float(predictions[0][predicted_index]) | ||||
| 
 | ||||
|             # 获取预测类别标签 | ||||
|             predicted_label = self.CATEGORIES[predicted_index] | ||||
| 
 | ||||
|             # 获取所有类别的置信度 | ||||
|             all_confidences = {cat: float(conf) for cat, conf in zip(self.CATEGORIES, predictions[0])} | ||||
| 
 | ||||
|             return { | ||||
|                 "success": True, | ||||
|                 "category": predicted_label, | ||||
|                 "confidence": confidence, | ||||
|                 "all_confidences": all_confidences | ||||
|             } | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"文本分类过程中发生错误: {str(e)}") | ||||
|             return {"success": False, "error": str(e)} | ||||
| 
 | ||||
|     def classify_file(self, file_path): | ||||
|         """对文件内容进行分类 | ||||
| 
 | ||||
|         Args: | ||||
|             file_path (str): 文件路径 | ||||
| 
 | ||||
|         Returns: | ||||
|             dict: 分类结果,包含类别标签和置信度 | ||||
|         """ | ||||
|         try: | ||||
|             # 读取文件内容 | ||||
|             with open(file_path, 'r', encoding='utf-8') as f: | ||||
|                 text = f.read().strip() | ||||
| 
 | ||||
|             # 调用文本分类函数 | ||||
|             return self.classify_text(text) | ||||
| 
 | ||||
|         except UnicodeDecodeError: | ||||
|             # 如果UTF-8解码失败,尝试其他编码 | ||||
|             try: | ||||
|                 with open(file_path, 'r', encoding='gbk') as f: | ||||
|                     text = f.read().strip() | ||||
|                 return self.classify_text(text) | ||||
|             except Exception as e: | ||||
|                 return {"success": False, "error": f"文件解码失败: {str(e)}"} | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             self.logger.error(f"文件处理过程中发生错误: {str(e)}") | ||||
|             return {"success": False, "error": f"文件处理错误: {str(e)}"} | ||||
| 
 | ||||
| 
 | ||||
| # 创建单例实例,避免重复加载模型 | ||||
| text_classifier = TextClassificationModel() | ||||
| 
 | ||||
							
								
								
									
										12
									
								
								utils/password.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								utils/password.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import bcrypt | ||||
| 
 | ||||
| def hash_password(password): | ||||
|     """对密码进行哈希处理""" | ||||
|     # 对密码进行加盐处理并哈希 | ||||
|     salt = bcrypt.gensalt() | ||||
|     hashed = bcrypt.hashpw(password.encode('utf-8'), salt) | ||||
|     return hashed.decode('utf-8') | ||||
| 
 | ||||
| def check_password(hashed_password, user_password): | ||||
|     """验证密码是否匹配""" | ||||
|     return bcrypt.checkpw(user_password.encode('utf-8'), hashed_password.encode('utf-8')) | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 superlishunqin
						superlishunqin