first commit

This commit is contained in:
superlishunqin 2025-03-17 22:43:53 +08:00
commit f434b83090
39 changed files with 3817 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# 指定忽略的文件夹
/model/
/ziyao/
/venv/
/uploads/
/temp/
/__pycache__/
*/__pycache__/
**/__pycache__/
*.py[cod]
*$py.class

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View 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>

View 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
View 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
View 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
View 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
View 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
View File

0
README.md Normal file
View File

64
app.py Normal file
View 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
View 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
View 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
View File

28
requirements.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}

View 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
View 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
View 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'
});
}
});

View 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
View 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
View 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
View 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
View 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
View 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>

0
utils.py Normal file
View File

BIN
utils/.DS_Store vendored Normal file

Binary file not shown.

0
utils/__init__.py Normal file
View File

22
utils/db.py Normal file
View 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
View 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
View 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
View 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'))