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