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