diff --git a/all_file_output.py b/all_file_output.py
new file mode 100644
index 0000000..ef4d03d
--- /dev/null
+++ b/all_file_output.py
@@ -0,0 +1,64 @@
+import os
+import sys
+
+
+def collect_code_files(output_file="code_collection.txt"):
+ # 定义代码文件扩展名
+ code_extensions = [
+ '.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs',
+ '.js', '.html', '.css', '.php', '.go', '.rb',
+ '.swift', '.kt', '.ts', '.sh', '.pl', '.r'
+ ]
+
+ # 定义要排除的目录
+ excluded_dirs = [
+ 'venv', 'env', '.venv', '.env', 'virtualenv',
+ '__pycache__', 'node_modules', '.git', '.idea',
+ 'dist', 'build', 'target', 'bin'
+ ]
+
+ # 计数器
+ file_count = 0
+
+ # 打开输出文件
+ with open(output_file, 'w', encoding='utf-8') as out_file:
+ # 遍历当前目录及所有子目录
+ for root, dirs, files in os.walk('.'):
+ # 从dirs中移除排除的目录,这会阻止os.walk进入这些目录
+ dirs[:] = [d for d in dirs if d not in excluded_dirs]
+
+ for file in files:
+ # 获取文件扩展名
+ _, ext = os.path.splitext(file)
+
+ # 检查是否为代码文件
+ if ext.lower() in code_extensions:
+ file_path = os.path.join(root, file)
+ file_count += 1
+
+ # 写入文件路径作为分隔
+ out_file.write(f"\n{'=' * 80}\n")
+ out_file.write(f"File: {file_path}\n")
+ out_file.write(f"{'=' * 80}\n\n")
+
+ # 尝试读取文件内容并写入
+ try:
+ with open(file_path, 'r', encoding='utf-8') as code_file:
+ out_file.write(code_file.read())
+ except UnicodeDecodeError:
+ # 尝试用不同的编码
+ try:
+ with open(file_path, 'r', encoding='latin-1') as code_file:
+ out_file.write(code_file.read())
+ except Exception as e:
+ out_file.write(f"无法读取文件内容: {str(e)}\n")
+ except Exception as e:
+ out_file.write(f"读取文件时出错: {str(e)}\n")
+
+ print(f"已成功收集 {file_count} 个代码文件到 {output_file}")
+
+
+if __name__ == "__main__":
+ # 如果提供了命令行参数,则使用它作为输出文件名
+ output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt"
+ collect_code_files(output_file)
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..e95008d
--- /dev/null
+++ b/app.py
@@ -0,0 +1,6 @@
+from app import create_app
+
+app = create_app()
+
+if __name__ == '__main__':
+ app.run(debug=True, host='0.0.0.0', port=49666)
diff --git a/app/__init__.py b/app/__init__.py
new file mode 100644
index 0000000..41e38d3
--- /dev/null
+++ b/app/__init__.py
@@ -0,0 +1,85 @@
+from flask import Flask, render_template, session, g
+from app.models.user import db, User
+from app.controllers.user import user_bp
+import os
+
+
+def create_app():
+ app = Flask(__name__)
+
+ # 配置应用
+ app.config.from_mapping(
+ SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'),
+ SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system',
+ SQLALCHEMY_TRACK_MODIFICATIONS=False,
+ PERMANENT_SESSION_LIFETIME=86400 * 7, # 7天
+
+ # 邮件配置
+ EMAIL_HOST='smtp.qq.com',
+ EMAIL_PORT=587,
+ EMAIL_ENCRYPTION='starttls',
+ EMAIL_USERNAME='3399560459@qq.com',
+ EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码,不是邮箱密码
+ EMAIL_FROM='3399560459@qq.com',
+ EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
+ )
+
+ # 实例配置,如果存在
+ app.config.from_pyfile('config.py', silent=True)
+
+ # 初始化数据库
+ db.init_app(app)
+
+ # 注册蓝图
+ app.register_blueprint(user_bp, url_prefix='/user')
+
+ # 创建数据库表
+ with app.app_context():
+ db.create_all()
+
+ # 创建默认角色
+ from app.models.user import Role
+ if not Role.query.filter_by(id=1).first():
+ admin_role = Role(id=1, role_name='管理员', description='系统管理员')
+ db.session.add(admin_role)
+
+ if not Role.query.filter_by(id=2).first():
+ user_role = Role(id=2, role_name='普通用户', description='普通用户')
+ db.session.add(user_role)
+
+ # 创建管理员账号
+ if not User.query.filter_by(username='admin').first():
+ admin = User(
+ username='admin',
+ password='admin123',
+ email='admin@example.com',
+ role_id=1,
+ nickname='系统管理员'
+ )
+ db.session.add(admin)
+
+ db.session.commit()
+
+ # 请求前处理
+ @app.before_request
+ def load_logged_in_user():
+ user_id = session.get('user_id')
+
+ if user_id is None:
+ g.user = None
+ else:
+ g.user = User.query.get(user_id)
+
+ # 首页路由
+ @app.route('/')
+ def index():
+ if not g.user:
+ return render_template('login.html')
+ return render_template('index.html', current_user=g.user)
+
+ # 错误处理
+ @app.errorhandler(404)
+ def page_not_found(e):
+ return render_template('404.html'), 404
+
+ return app
\ No newline at end of file
diff --git a/app/controllers/__init__.py b/app/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/announcement.py b/app/controllers/announcement.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/book.py b/app/controllers/book.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/borrow.py b/app/controllers/borrow.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/inventory.py b/app/controllers/inventory.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/log.py b/app/controllers/log.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/statistics.py b/app/controllers/statistics.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/user.py b/app/controllers/user.py
new file mode 100644
index 0000000..a0561f4
--- /dev/null
+++ b/app/controllers/user.py
@@ -0,0 +1,181 @@
+from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
+from werkzeug.security import generate_password_hash, check_password_hash
+from app.models.user import User, db
+from app.utils.email import send_verification_email, generate_verification_code
+import logging
+from functools import wraps
+import time
+from datetime import datetime, timedelta
+
+# 创建蓝图
+user_bp = Blueprint('user', __name__)
+
+
+# 使用内存字典代替Redis存储验证码
+class VerificationStore:
+ def __init__(self):
+ self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}}
+
+ def setex(self, email, seconds, code):
+ """设置验证码并指定过期时间"""
+ expiry = datetime.now() + timedelta(seconds=seconds)
+ self.codes[email] = {'code': code, 'expires': expiry}
+ return True
+
+ def get(self, email):
+ """获取验证码,如果过期则返回None"""
+ if email not in self.codes:
+ return None
+
+ data = self.codes[email]
+ if datetime.now() > data['expires']:
+ # 验证码已过期,删除它
+ self.delete(email)
+ return None
+
+ return data['code']
+
+ def delete(self, email):
+ """删除验证码"""
+ if email in self.codes:
+ del self.codes[email]
+ return True
+
+
+# 使用内存存储验证码
+verification_codes = VerificationStore()
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if 'user_id' not in session:
+ return redirect(url_for('user.login'))
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+
+@user_bp.route('/login', methods=['GET', 'POST'])
+def login():
+ # 保持原代码不变
+ if request.method == 'POST':
+ username = request.form.get('username')
+ password = request.form.get('password')
+ remember_me = request.form.get('remember_me') == 'on'
+
+ if not username or not password:
+ return render_template('login.html', error='用户名和密码不能为空')
+
+ # 检查用户是否存在
+ user = User.query.filter((User.username == username) | (User.email == username)).first()
+
+ if not user or not user.check_password(password):
+ return render_template('login.html', error='用户名或密码错误')
+
+ if user.status == 0:
+ return render_template('login.html', error='账号已被禁用,请联系管理员')
+
+ # 登录成功,保存用户信息到会话
+ session['user_id'] = user.id
+ session['username'] = user.username
+ session['role_id'] = user.role_id
+
+ if remember_me:
+ # 设置会话过期时间为7天
+ session.permanent = True
+
+ # 记录登录日志(可选)
+ # log_user_action('用户登录')
+
+ # 重定向到首页
+ return redirect(url_for('index'))
+
+ return render_template('login.html')
+
+
+@user_bp.route('/register', methods=['GET', 'POST'])
+def register():
+ if request.method == 'POST':
+ username = request.form.get('username')
+ email = request.form.get('email')
+ password = request.form.get('password')
+ confirm_password = request.form.get('confirm_password')
+ verification_code = request.form.get('verification_code')
+
+ # 验证表单数据
+ if not username or not email or not password or not confirm_password or not verification_code:
+ return render_template('register.html', error='所有字段都是必填项')
+
+ if password != confirm_password:
+ return render_template('register.html', error='两次输入的密码不匹配')
+
+ # 检查用户名和邮箱是否已存在
+ if User.query.filter_by(username=username).first():
+ return render_template('register.html', error='用户名已存在')
+
+ if User.query.filter_by(email=email).first():
+ return render_template('register.html', error='邮箱已被注册')
+
+ # 验证验证码
+ stored_code = verification_codes.get(email)
+ if not stored_code or stored_code != verification_code:
+ return render_template('register.html', error='验证码无效或已过期')
+
+ # 创建新用户
+ try:
+ new_user = User(
+ username=username,
+ password=password, # 密码会在模型中自动哈希
+ email=email,
+ nickname=username # 默认昵称与用户名相同
+ )
+ db.session.add(new_user)
+ db.session.commit()
+
+ # 清除验证码
+ verification_codes.delete(email)
+
+ flash('注册成功,请登录', 'success')
+ return redirect(url_for('user.login'))
+ except Exception as e:
+ db.session.rollback()
+ logging.error(f"User registration failed: {str(e)}")
+ return render_template('register.html', error='注册失败,请稍后重试')
+
+ return render_template('register.html')
+
+
+@user_bp.route('/logout')
+def logout():
+ # 清除会话数据
+ session.pop('user_id', None)
+ session.pop('username', None)
+ session.pop('role_id', None)
+ return redirect(url_for('user.login'))
+
+
+@user_bp.route('/send_verification_code', methods=['POST'])
+def send_verification_code():
+ data = request.get_json()
+ email = data.get('email')
+
+ if not email:
+ return jsonify({'success': False, 'message': '请提供邮箱地址'})
+
+ # 检查邮箱格式
+ import re
+ if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
+ return jsonify({'success': False, 'message': '邮箱格式不正确'})
+
+ # 生成验证码
+ code = generate_verification_code()
+
+ # 存储验证码(10分钟有效)
+ verification_codes.setex(email, 600, code) # 10分钟过期
+
+ # 发送验证码邮件
+ if send_verification_email(email, code):
+ return jsonify({'success': True, 'message': '验证码已发送'})
+ else:
+ return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'})
diff --git a/app/models/__init__.py b/app/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/announcement.py b/app/models/announcement.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/book.py b/app/models/book.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/borrow.py b/app/models/borrow.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/inventory.py b/app/models/inventory.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/log.py b/app/models/log.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/notification.py b/app/models/notification.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/user.py b/app/models/user.py
new file mode 100644
index 0000000..d0182cc
--- /dev/null
+++ b/app/models/user.py
@@ -0,0 +1,75 @@
+from flask_sqlalchemy import SQLAlchemy
+from werkzeug.security import generate_password_hash, check_password_hash
+from datetime import datetime
+
+db = SQLAlchemy()
+
+
+class User(db.Model):
+ __tablename__ = 'users'
+
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ username = db.Column(db.String(64), unique=True, nullable=False)
+ password = db.Column(db.String(255), nullable=False)
+ email = db.Column(db.String(128), unique=True, nullable=True)
+ phone = db.Column(db.String(20), unique=True, nullable=True)
+ nickname = db.Column(db.String(64), nullable=True)
+ status = db.Column(db.Integer, default=1) # 1: active, 0: disabled
+ role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2) # 2: 普通用户, 1: 管理员
+ created_at = db.Column(db.DateTime, default=datetime.now)
+ updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
+
+ def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2):
+ self.username = username
+ self.set_password(password)
+ self.email = email
+ self.phone = phone
+ self.nickname = nickname
+ self.role_id = role_id
+
+ def set_password(self, password):
+ """设置密码,使用哈希加密"""
+ self.password = generate_password_hash(password)
+
+ def check_password(self, password):
+ """验证密码"""
+ return check_password_hash(self.password, password)
+
+ def to_dict(self):
+ """转换为字典格式"""
+ return {
+ 'id': self.id,
+ 'username': self.username,
+ 'email': self.email,
+ 'phone': self.phone,
+ 'nickname': self.nickname,
+ 'status': self.status,
+ 'role_id': self.role_id,
+ 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
+ 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
+ }
+
+ @classmethod
+ def create_user(cls, username, password, email=None, phone=None, nickname=None, role_id=2):
+ """创建新用户"""
+ user = User(
+ username=username,
+ password=password,
+ email=email,
+ phone=phone,
+ nickname=nickname,
+ role_id=role_id
+ )
+ db.session.add(user)
+ db.session.commit()
+ return user
+
+
+class Role(db.Model):
+ __tablename__ = 'roles'
+
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ role_name = db.Column(db.String(32), unique=True, nullable=False)
+ description = db.Column(db.String(128))
+
+ users = db.relationship('User', backref='role')
diff --git a/app/services/__init__.py b/app/services/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/services/book_service.py b/app/services/book_service.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/services/borrow_service.py b/app/services/borrow_service.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/services/inventory_service.py b/app/services/inventory_service.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/services/user_service.py b/app/services/user_service.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/static/css/index.css b/app/static/css/index.css
new file mode 100644
index 0000000..f0896b9
--- /dev/null
+++ b/app/static/css/index.css
@@ -0,0 +1,651 @@
+/* index.css - 仅用于图书管理系统首页/仪表板 */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+body {
+ background-color: #f5f7fa;
+ color: #333;
+ font-size: 16px;
+ line-height: 1.6;
+}
+
+a {
+ text-decoration: none;
+ color: #4a89dc;
+}
+
+ul {
+ list-style: none;
+}
+
+/* 应用容器 */
+.app-container {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* 侧边导航栏 */
+.sidebar {
+ width: 250px;
+ background-color: #2c3e50;
+ color: #ecf0f1;
+ padding: 20px 0;
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
+ position: fixed;
+ height: 100vh;
+ overflow-y: auto;
+}
+
+.logo-container {
+ padding: 0 20px 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-bottom: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.logo {
+ width: 60px;
+ height: auto;
+ margin-bottom: 10px;
+}
+
+.logo-container h2 {
+ font-size: 1.2rem;
+ margin: 10px 0;
+ color: #ecf0f1;
+ font-weight: 500;
+}
+
+.nav-links li {
+ margin-bottom: 5px;
+}
+
+.nav-links li a {
+ padding: 10px 20px;
+ display: flex;
+ align-items: center;
+ color: #bdc3c7;
+ transition: all 0.3s ease;
+}
+
+.nav-links li a i {
+ margin-right: 10px;
+ font-size: 1.1rem;
+ width: 20px;
+ text-align: center;
+}
+
+.nav-links li a:hover, .nav-links li.active a {
+ background-color: #34495e;
+ color: #ecf0f1;
+ border-left: 3px solid #4a89dc;
+}
+
+.nav-category {
+ padding: 10px 20px;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ color: #7f8c8d;
+ margin-top: 15px;
+ margin-bottom: 5px;
+}
+
+/* 主内容区 */
+.main-content {
+ flex: 1;
+ margin-left: 250px;
+ padding: 20px;
+}
+
+/* 顶部导航栏 */
+.top-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 30px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ margin-bottom: 20px;
+}
+
+.search-container {
+ position: relative;
+ width: 300px;
+}
+
+.search-input {
+ padding: 10px 15px 10px 40px;
+ width: 100%;
+ border: 1px solid #e1e4e8;
+ border-radius: 20px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+}
+
+.search-input:focus {
+ border-color: #4a89dc;
+ box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.1);
+ outline: none;
+}
+
+.search-icon {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #8492a6;
+}
+
+.user-menu {
+ display: flex;
+ align-items: center;
+}
+
+.notifications {
+ margin-right: 20px;
+ position: relative;
+ cursor: pointer;
+}
+
+.notifications i {
+ font-size: 1.2rem;
+ color: #606266;
+}
+
+.badge {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background-color: #f56c6c;
+ color: white;
+ font-size: 0.7rem;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ position: relative;
+ cursor: pointer;
+}
+
+.user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: #4a89dc;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ margin-right: 10px;
+ font-size: 1.2rem;
+}
+
+.user-details {
+ display: flex;
+ flex-direction: column;
+}
+
+.user-name {
+ font-weight: 500;
+ color: #333;
+}
+
+.user-role {
+ font-size: 0.8rem;
+ color: #8492a6;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ background-color: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
+ padding: 10px 0;
+ min-width: 150px;
+ display: none;
+ z-index: 10;
+}
+
+.user-info.active .dropdown-menu {
+ display: block;
+}
+
+.dropdown-menu a {
+ display: block;
+ padding: 8px 15px;
+ color: #606266;
+ transition: all 0.3s ease;
+}
+
+.dropdown-menu a:hover {
+ background-color: #f5f7fa;
+}
+
+.dropdown-menu a i {
+ margin-right: 8px;
+ width: 16px;
+ text-align: center;
+}
+
+/* 欢迎区域 */
+.welcome-section {
+ background: linear-gradient(to right, #4a89dc, #5d9cec);
+ color: white;
+ padding: 30px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
+}
+
+.welcome-section h1 {
+ font-size: 1.8rem;
+ margin-bottom: 5px;
+}
+
+.welcome-section p {
+ font-size: 1rem;
+ opacity: 0.9;
+}
+
+/* 统计卡片样式 */
+.stats-container {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+}
+
+.stat-icon {
+ font-size: 2rem;
+ color: #4a89dc;
+ margin-right: 15px;
+ width: 40px;
+ text-align: center;
+}
+
+.stat-info h3 {
+ font-size: 0.9rem;
+ color: #606266;
+ margin-bottom: 5px;
+}
+
+.stat-number {
+ font-size: 1.8rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+/* 主要内容区域 */
+.main-sections {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.content-section {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ padding: 20px;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #edf2f7;
+}
+
+.section-header h2 {
+ font-size: 1.2rem;
+ color: #2c3e50;
+}
+
+.view-all {
+ font-size: 0.85rem;
+ color: #4a89dc;
+ display: flex;
+ align-items: center;
+}
+
+.view-all i {
+ margin-left: 5px;
+ transition: transform 0.3s ease;
+}
+
+.view-all:hover i {
+ transform: translateX(3px);
+}
+
+/* 图书卡片样式 */
+.book-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+}
+
+.book-card {
+ display: flex;
+ border: 1px solid #edf2f7;
+ border-radius: 8px;
+ overflow: hidden;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.book-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(0,0,0,0.05);
+}
+
+.book-cover {
+ width: 100px;
+ height: 140px;
+ min-width: 100px;
+ background-color: #f5f7fa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.book-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.book-info {
+ padding: 15px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.book-title {
+ font-size: 1rem;
+ margin-bottom: 5px;
+ color: #2c3e50;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.book-author {
+ font-size: 0.85rem;
+ color: #606266;
+ margin-bottom: 10px;
+}
+
+.book-meta {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.book-category {
+ background-color: #e5f1ff;
+ color: #4a89dc;
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-size: 0.75rem;
+}
+
+.book-status {
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.book-status.available {
+ color: #67c23a;
+}
+
+.book-status.borrowed {
+ color: #e6a23c;
+}
+
+.borrow-btn {
+ background-color: #4a89dc;
+ color: white;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ margin-top: auto;
+ transition: background-color 0.3s ease;
+}
+
+.borrow-btn:hover {
+ background-color: #357bc8;
+}
+
+/* 通知公告样式 */
+.notice-item {
+ display: flex;
+ padding: 15px 0;
+ border-bottom: 1px solid #edf2f7;
+}
+
+.notice-item:last-child {
+ border-bottom: none;
+}
+
+.notice-icon {
+ font-size: 1.5rem;
+ color: #4a89dc;
+ margin-right: 15px;
+ display: flex;
+ align-items: flex-start;
+ padding-top: 5px;
+}
+
+.notice-content h3 {
+ font-size: 1rem;
+ color: #2c3e50;
+ margin-bottom: 5px;
+}
+
+.notice-content p {
+ font-size: 0.9rem;
+ color: #606266;
+ margin-bottom: 10px;
+}
+
+.notice-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.notice-time {
+ font-size: 0.8rem;
+ color: #8492a6;
+}
+
+.renew-btn {
+ background-color: #ecf5ff;
+ color: #4a89dc;
+ border: 1px solid #d9ecff;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.renew-btn:hover {
+ background-color: #4a89dc;
+ color: white;
+ border-color: #4a89dc;
+}
+
+/* 热门图书区域 */
+.popular-section {
+ margin-top: 20px;
+}
+
+.popular-books {
+ display: flex;
+ overflow-x: auto;
+ gap: 15px;
+ padding-bottom: 10px;
+}
+
+.popular-book-item {
+ display: flex;
+ background-color: #f8fafc;
+ border-radius: 8px;
+ padding: 15px;
+ min-width: 280px;
+ position: relative;
+}
+
+.rank-badge {
+ position: absolute;
+ top: -10px;
+ left: 10px;
+ background-color: #4a89dc;
+ color: white;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ font-size: 0.8rem;
+ font-weight: bold;
+}
+
+.book-cover.small {
+ width: 60px;
+ height: 90px;
+ min-width: 60px;
+ margin-right: 15px;
+}
+
+.book-details {
+ flex: 1;
+}
+
+.book-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ margin-top: 10px;
+}
+
+.book-stats span {
+ font-size: 0.8rem;
+ color: #8492a6;
+}
+
+.book-stats i {
+ margin-right: 5px;
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+ .stats-container {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .main-sections {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ width: 70px;
+ overflow: hidden;
+ }
+
+ .logo-container {
+ padding: 10px;
+ }
+
+ .logo-container h2 {
+ display: none;
+ }
+
+ .nav-links li a span {
+ display: none;
+ }
+
+ .nav-links li a i {
+ margin-right: 0;
+ }
+
+ .nav-category {
+ display: none;
+ }
+
+ .main-content {
+ margin-left: 70px;
+ }
+
+ .search-container {
+ width: 180px;
+ }
+
+ .book-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 576px) {
+ .stats-container {
+ grid-template-columns: 1fr;
+ }
+
+ .top-bar {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .search-container {
+ width: 100%;
+ }
+
+ .user-details {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/app/static/css/main.css b/app/static/css/main.css
new file mode 100644
index 0000000..79fdbfa
--- /dev/null
+++ b/app/static/css/main.css
@@ -0,0 +1,469 @@
+/* 主样式文件 - 从登录页面复制过来的样式 */
+/* 从您提供的登录页CSS复制,但省略了不需要的部分 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+}
+
+:root {
+ --primary-color: #4a89dc;
+ --primary-hover: #3b78c4;
+ --secondary-color: #5cb85c;
+ --text-color: #333;
+ --light-text: #666;
+ --bg-color: #f5f7fa;
+ --card-bg: #ffffff;
+ --border-color: #ddd;
+ --error-color: #e74c3c;
+ --success-color: #2ecc71;
+}
+
+body.dark-mode {
+ --primary-color: #5a9aed;
+ --primary-hover: #4a89dc;
+ --secondary-color: #6bc76b;
+ --text-color: #f1f1f1;
+ --light-text: #aaa;
+ --bg-color: #1a1a1a;
+ --card-bg: #2c2c2c;
+ --border-color: #444;
+}
+
+body {
+ background-color: var(--bg-color);
+ background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+ background-size: cover;
+ background-position: center;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ color: var(--text-color);
+ transition: all 0.3s ease;
+}
+
+.theme-toggle {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ z-index: 10;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 50%;
+ background-color: rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(5px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.overlay {
+ background-color: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(5px);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+}
+
+.main-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex: 1;
+ padding: 20px;
+}
+
+.login-container {
+ background-color: var(--card-bg);
+ border-radius: 12px;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ width: 450px;
+ padding: 35px;
+ position: relative;
+ overflow: hidden;
+ animation: fadeIn 0.5s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.logo {
+ text-align: center;
+ margin-bottom: 25px;
+ position: relative;
+}
+
+.logo img {
+ width: 90px;
+ height: 90px;
+ border-radius: 12px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ padding: 5px;
+ background-color: #fff;
+ transition: transform 0.3s ease;
+}
+
+h1 {
+ text-align: center;
+ color: var(--text-color);
+ margin-bottom: 10px;
+ font-weight: 600;
+ font-size: 28px;
+}
+
+.subtitle {
+ text-align: center;
+ color: var(--light-text);
+ margin-bottom: 30px;
+ font-size: 14px;
+}
+
+.form-group {
+ margin-bottom: 22px;
+ position: relative;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--text-color);
+ font-weight: 500;
+ font-size: 14px;
+}
+
+.input-with-icon {
+ position: relative;
+}
+
+.input-icon {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--light-text);
+}
+
+.form-control {
+ width: 100%;
+ height: 48px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0 15px 0 45px;
+ font-size: 15px;
+ transition: all 0.3s ease;
+ background-color: var(--card-bg);
+ color: var(--text-color);
+}
+
+.form-control:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
+ outline: none;
+}
+
+.password-toggle {
+ position: absolute;
+ right: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ cursor: pointer;
+ color: var(--light-text);
+}
+
+.validation-message {
+ margin-top: 6px;
+ font-size: 12px;
+ color: var(--error-color);
+ display: none;
+}
+
+.validation-message.show {
+ display: block;
+ animation: shake 0.5s ease;
+}
+
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
+ 20%, 40%, 60%, 80% { transform: translateX(5px); }
+}
+
+.remember-forgot {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 25px;
+}
+
+.custom-checkbox {
+ position: relative;
+ padding-left: 30px;
+ cursor: pointer;
+ font-size: 14px;
+ user-select: none;
+ color: var(--light-text);
+}
+
+.custom-checkbox input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+}
+
+.checkmark {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 18px;
+ width: 18px;
+ background-color: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+ transition: all 0.2s ease;
+}
+
+.custom-checkbox:hover input ~ .checkmark {
+ border-color: var(--primary-color);
+}
+
+.custom-checkbox input:checked ~ .checkmark {
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.checkmark:after {
+ content: "";
+ position: absolute;
+ display: none;
+}
+
+.custom-checkbox input:checked ~ .checkmark:after {
+ display: block;
+}
+
+.custom-checkbox .checkmark:after {
+ left: 6px;
+ top: 2px;
+ width: 4px;
+ height: 9px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+
+.forgot-password a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-size: 14px;
+ transition: color 0.3s ease;
+}
+
+.forgot-password a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+}
+
+.btn-login {
+ width: 100%;
+ height: 48px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-login:hover {
+ background-color: var(--primary-hover);
+}
+
+.btn-login:active {
+ transform: scale(0.98);
+}
+
+.btn-login .loading {
+ display: none;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.btn-login.loading-state {
+ color: transparent;
+}
+
+.btn-login.loading-state .loading {
+ display: block;
+}
+
+.signup {
+ text-align: center;
+ margin-top: 25px;
+ font-size: 14px;
+ color: var(--light-text);
+}
+
+.signup a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 600;
+ transition: color 0.3s ease;
+}
+
+.signup a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+}
+
+.features {
+ display: flex;
+ justify-content: center;
+ margin-top: 25px;
+ gap: 30px;
+}
+
+.feature-item {
+ text-align: center;
+ font-size: 12px;
+ color: var(--light-text);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.feature-icon {
+ margin-bottom: 5px;
+ font-size: 18px;
+}
+
+footer {
+ text-align: center;
+ padding: 20px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 12px;
+}
+
+footer a {
+ color: rgba(255, 255, 255, 0.9);
+ text-decoration: none;
+}
+
+.alert {
+ padding: 10px;
+ margin-bottom: 15px;
+ border-radius: 4px;
+ color: #721c24;
+ background-color: #f8d7da;
+ border: 1px solid #f5c6cb;
+}
+
+.verification-code-container {
+ display: flex;
+ gap: 10px;
+}
+
+.verification-input {
+ flex: 1;
+ height: 48px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0 15px;
+ font-size: 15px;
+}
+
+.send-code-btn {
+ padding: 0 15px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.register-container {
+ width: 500px;
+}
+
+@media (max-width: 576px) {
+ .login-container {
+ width: 100%;
+ padding: 25px;
+ border-radius: 0;
+ }
+
+ .theme-toggle {
+ top: 10px;
+ }
+
+ .logo img {
+ width: 70px;
+ height: 70px;
+ }
+
+ h1 {
+ font-size: 22px;
+ }
+
+ .main-container {
+ padding: 0;
+ }
+
+ .verification-code-container {
+ flex-direction: column;
+ }
+
+ .register-container {
+ width: 100%;
+ }
+}
+
+.verification-code-container {
+ display: flex;
+ gap: 10px;
+}
+
+.verification-input {
+ flex: 1;
+ height: 48px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0 15px;
+ font-size: 15px;
+ transition: all 0.3s ease;
+ background-color: var(--card-bg);
+ color: var(--text-color);
+}
+
+.send-code-btn {
+ padding: 0 15px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ white-space: nowrap;
+ transition: all 0.3s ease;
+}
+
+.send-code-btn:hover {
+ background-color: var(--primary-hover);
+}
+
+.send-code-btn:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+}
diff --git a/app/static/images/logo.png b/app/static/images/logo.png
new file mode 100644
index 0000000..d1e40dc
Binary files /dev/null and b/app/static/images/logo.png differ
diff --git a/app/static/js/main.js b/app/static/js/main.js
new file mode 100644
index 0000000..95937b2
--- /dev/null
+++ b/app/static/js/main.js
@@ -0,0 +1,340 @@
+// 主JS文件 - 包含登录和注册功能
+document.addEventListener('DOMContentLoaded', function() {
+ // 主题切换
+ const themeToggle = document.getElementById('theme-toggle');
+ if (themeToggle) {
+ const body = document.body;
+
+ themeToggle.addEventListener('click', function() {
+ body.classList.toggle('dark-mode');
+ const isDarkMode = body.classList.contains('dark-mode');
+ localStorage.setItem('dark-mode', isDarkMode);
+ themeToggle.innerHTML = isDarkMode ? '🌙' : '☀️';
+ });
+
+ // 从本地存储中加载主题首选项
+ const savedDarkMode = localStorage.getItem('dark-mode') === 'true';
+ if (savedDarkMode) {
+ body.classList.add('dark-mode');
+ themeToggle.innerHTML = '🌙';
+ }
+ }
+
+ // 密码可见性切换
+ const passwordToggle = document.getElementById('password-toggle');
+ if (passwordToggle) {
+ const passwordInput = document.getElementById('password');
+ passwordToggle.addEventListener('click', function() {
+ const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
+ passwordInput.setAttribute('type', type);
+ passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️🗨️';
+ });
+ }
+
+ // 登录表单验证
+ const loginForm = document.getElementById('login-form');
+ if (loginForm) {
+ const usernameInput = document.getElementById('username');
+ const passwordInput = document.getElementById('password');
+ const usernameError = document.getElementById('username-error');
+ const passwordError = document.getElementById('password-error');
+ const loginButton = document.getElementById('login-button');
+
+ if (usernameInput && usernameError) {
+ usernameInput.addEventListener('input', function() {
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ } else {
+ usernameError.classList.remove('show');
+ }
+ });
+ }
+
+ if (passwordInput && passwordError) {
+ passwordInput.addEventListener('input', function() {
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ } else {
+ passwordError.classList.remove('show');
+ }
+ });
+ }
+
+ loginForm.addEventListener('submit', function(e) {
+ let isValid = true;
+
+ // 验证用户名
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证密码
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ isValid = false;
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ isValid = false;
+ }
+
+ if (!isValid) {
+ e.preventDefault();
+ } else if (loginButton) {
+ loginButton.classList.add('loading-state');
+ }
+ });
+ }
+ // 注册表单验证
+ const registerForm = document.getElementById('register-form');
+ if (registerForm) {
+ const usernameInput = document.getElementById('username');
+ const emailInput = document.getElementById('email');
+ const passwordInput = document.getElementById('password');
+ const confirmPasswordInput = document.getElementById('confirm_password');
+ const verificationCodeInput = document.getElementById('verification_code');
+
+ const usernameError = document.getElementById('username-error');
+ const emailError = document.getElementById('email-error');
+ const passwordError = document.getElementById('password-error');
+ const confirmPasswordError = document.getElementById('confirm-password-error');
+ const verificationCodeError = document.getElementById('verification-code-error');
+
+ const registerButton = document.getElementById('register-button');
+ const sendCodeBtn = document.getElementById('send-code-btn');
+
+ // 用户名验证
+ if (usernameInput && usernameError) {
+ usernameInput.addEventListener('input', function() {
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ } else if (usernameInput.value.length < 3) {
+ usernameError.textContent = '用户名至少3个字符';
+ usernameError.classList.add('show');
+ } else {
+ usernameError.classList.remove('show');
+ }
+ });
+ }
+
+ // 邮箱验证
+ if (emailInput && emailError) {
+ emailInput.addEventListener('input', function() {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (emailInput.value.trim() === '') {
+ emailError.textContent = '邮箱不能为空';
+ emailError.classList.add('show');
+ } else if (!emailRegex.test(emailInput.value)) {
+ emailError.textContent = '请输入有效的邮箱地址';
+ emailError.classList.add('show');
+ } else {
+ emailError.classList.remove('show');
+ }
+ });
+ }
+
+ // 密码验证
+ if (passwordInput && passwordError) {
+ passwordInput.addEventListener('input', function() {
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ } else {
+ passwordError.classList.remove('show');
+ }
+
+ // 检查确认密码是否匹配
+ if (confirmPasswordInput && confirmPasswordInput.value) {
+ if (confirmPasswordInput.value !== passwordInput.value) {
+ confirmPasswordError.textContent = '两次输入的密码不匹配';
+ confirmPasswordError.classList.add('show');
+ } else {
+ confirmPasswordError.classList.remove('show');
+ }
+ }
+ });
+ }
+
+ // 确认密码验证
+ if (confirmPasswordInput && confirmPasswordError) {
+ confirmPasswordInput.addEventListener('input', function() {
+ if (confirmPasswordInput.value.trim() === '') {
+ confirmPasswordError.textContent = '请确认密码';
+ confirmPasswordError.classList.add('show');
+ } else if (confirmPasswordInput.value !== passwordInput.value) {
+ confirmPasswordError.textContent = '两次输入的密码不匹配';
+ confirmPasswordError.classList.add('show');
+ } else {
+ confirmPasswordError.classList.remove('show');
+ }
+ });
+ }
+
+ // 发送验证码按钮
+ if (sendCodeBtn) {
+ sendCodeBtn.addEventListener('click', function() {
+ const email = emailInput.value.trim();
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ if (!email) {
+ emailError.textContent = '请输入邮箱地址';
+ emailError.classList.add('show');
+ return;
+ } else if (!emailRegex.test(email)) {
+ emailError.textContent = '请输入有效的邮箱地址';
+ emailError.classList.add('show');
+ return;
+ }
+
+ // 禁用按钮并显示倒计时
+ let countdown = 60;
+ sendCodeBtn.disabled = true;
+ const originalText = sendCodeBtn.textContent;
+ sendCodeBtn.textContent = `${countdown}秒后重试`;
+
+ const timer = setInterval(() => {
+ countdown--;
+ sendCodeBtn.textContent = `${countdown}秒后重试`;
+
+ if (countdown <= 0) {
+ clearInterval(timer);
+ sendCodeBtn.disabled = false;
+ sendCodeBtn.textContent = originalText;
+ }
+ }, 1000);
+
+ // 发送请求获取验证码
+ fetch('/user/send_verification_code', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email: email }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ console.log("验证码发送响应:", data); // 添加调试日志
+ if (data.success) {
+ showMessage('验证码已发送', '请检查您的邮箱', 'success');
+ } else {
+ showMessage('发送失败', data.message || '请稍后重试', 'error');
+ clearInterval(timer);
+ sendCodeBtn.disabled = false;
+ sendCodeBtn.textContent = originalText;
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ showMessage('发送失败', '网络错误,请稍后重试', 'error');
+ clearInterval(timer);
+ sendCodeBtn.disabled = false;
+ sendCodeBtn.textContent = originalText;
+ });
+ });
+ }
+
+ // 表单提交验证
+ registerForm.addEventListener('submit', function(e) {
+ let isValid = true;
+
+ // 验证用户名
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ isValid = false;
+ } else if (usernameInput.value.length < 3) {
+ usernameError.textContent = '用户名至少3个字符';
+ usernameError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证邮箱
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (emailInput.value.trim() === '') {
+ emailError.textContent = '邮箱不能为空';
+ emailError.classList.add('show');
+ isValid = false;
+ } else if (!emailRegex.test(emailInput.value)) {
+ emailError.textContent = '请输入有效的邮箱地址';
+ emailError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证密码
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ isValid = false;
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证确认密码
+ if (confirmPasswordInput.value.trim() === '') {
+ confirmPasswordError.textContent = '请确认密码';
+ confirmPasswordError.classList.add('show');
+ isValid = false;
+ } else if (confirmPasswordInput.value !== passwordInput.value) {
+ confirmPasswordError.textContent = '两次输入的密码不匹配';
+ confirmPasswordError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证验证码
+ if (verificationCodeInput.value.trim() === '') {
+ verificationCodeError.textContent = '请输入验证码';
+ verificationCodeError.classList.add('show');
+ isValid = false;
+ }
+
+ if (!isValid) {
+ e.preventDefault();
+ } else if (registerButton) {
+ registerButton.classList.add('loading-state');
+ }
+ });
+ }
+
+ // 通知消息显示函数
+ function showMessage(title, message, type) {
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+
+ const icon = type === 'success' ? '✓' : '✗';
+
+ notification.innerHTML = `
+
${icon}
+
+
${title}
+
${message}
+
+ `;
+
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.classList.add('show');
+ }, 10);
+
+ setTimeout(() => {
+ notification.classList.remove('show');
+ setTimeout(() => {
+ document.body.removeChild(notification);
+ }, 300);
+ }, 3000);
+ }
+});
\ No newline at end of file
diff --git a/app/templates/404.html b/app/templates/404.html
new file mode 100644
index 0000000..1a08b9c
--- /dev/null
+++ b/app/templates/404.html
@@ -0,0 +1,49 @@
+
+
+
+
+
+ 页面未找到 - 图书管理系统
+
+
+
+
+
+
+
404
+
页面未找到
+
抱歉,您访问的页面不存在或已被移除。
+
请检查URL是否正确,或返回首页。
+
返回首页
+
+
+
+
diff --git a/app/templates/base.html b/app/templates/base.html
new file mode 100644
index 0000000..e69de29
diff --git a/app/templates/index.html b/app/templates/index.html
new file mode 100644
index 0000000..4745a9a
--- /dev/null
+++ b/app/templates/index.html
@@ -0,0 +1,214 @@
+
+
+
+
+
+ 首页 - 图书管理系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
欢迎回来,{{ current_user.username }}!
+
今天是 ,祝您使用愉快。
+
+
+
+
+
+
+
+
+
+
+
+ {% for i in range(4) %}
+
+
+
 }})
+
+
+
示例图书标题
+
作者名
+
+ 计算机
+ 可借阅
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
关于五一假期图书馆开放时间调整的通知
+
五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。
+
+ 2023-04-28
+
+
+
+
+
+
+
您有2本图书即将到期
+
《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。
+
+ 2023-04-27
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for i in range(5) %}
+
+
{{ i+1 }}
+
+

+
+
+
热门图书标题示例
+
知名作者
+
+ 1024 次浏览
+ 89 次借阅
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/login.html b/app/templates/login.html
new file mode 100644
index 0000000..223d27c
--- /dev/null
+++ b/app/templates/login.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+ 用户登录 - 图书管理系统
+
+
+
+
+
+ ☀️
+
+
+
+
+
 }})
+
+
图书管理系统
+
欢迎回来,请登录您的账户
+
+
+
+
+
+
+
+
+ 🔒
+ 安全登录
+
+
+ 🔐
+ 数据加密
+
+
+ 📚
+ 图书管理
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/templates/register.html b/app/templates/register.html
new file mode 100644
index 0000000..f1b3f90
--- /dev/null
+++ b/app/templates/register.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+ 用户注册 - 图书管理系统
+
+
+
+
+
+ ☀️
+
+
+
+
+
 }})
+
+
图书管理系统
+
创建您的新账户
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/utils/__init__.py b/app/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/utils/auth.py b/app/utils/auth.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/utils/db.py b/app/utils/db.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/utils/email.py b/app/utils/email.py
new file mode 100644
index 0000000..36f95ce
--- /dev/null
+++ b/app/utils/email.py
@@ -0,0 +1,91 @@
+import smtplib
+import random
+import string
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from flask import current_app
+import logging
+
+# 配置日志
+logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger(__name__)
+
+
+# 配置邮件发送功能
+def send_verification_email(to_email, verification_code):
+ """
+ 发送验证码邮件
+ """
+ try:
+ # 从应用配置获取邮件设置
+ email_host = current_app.config['EMAIL_HOST']
+ email_port = current_app.config['EMAIL_PORT']
+ email_username = current_app.config['EMAIL_USERNAME']
+ email_password = current_app.config['EMAIL_PASSWORD']
+ email_from = current_app.config['EMAIL_FROM']
+ email_from_name = current_app.config['EMAIL_FROM_NAME']
+
+ logger.info(f"准备发送邮件到: {to_email}, 验证码: {verification_code}")
+ logger.debug(f"邮件配置: 主机={email_host}, 端口={email_port}")
+
+ # 邮件内容
+ msg = MIMEMultipart()
+ msg['From'] = f"{email_from_name} <{email_from}>"
+ msg['To'] = to_email
+ msg['Subject'] = "图书管理系统 - 验证码"
+
+ # 邮件正文
+ body = f"""
+
+
+
+
图书管理系统 - 邮箱验证
+
您好,
+
感谢您注册图书管理系统,您的验证码是:
+
+ {verification_code}
+
+
该验证码将在10分钟内有效,请勿将验证码分享给他人。
+
如果您没有请求此验证码,请忽略此邮件。
+
+
此邮件为系统自动发送,请勿回复。
+
© 2025 图书管理系统
+
+
+
+
+ """
+
+ msg.attach(MIMEText(body, 'html'))
+
+ logger.debug("尝试连接到SMTP服务器...")
+ # 连接服务器发送邮件
+ server = smtplib.SMTP(email_host, email_port)
+ server.set_debuglevel(1) # 启用详细的SMTP调试输出
+
+ logger.debug("检查是否需要STARTTLS加密...")
+ if current_app.config.get('EMAIL_ENCRYPTION') == 'starttls':
+ logger.debug("启用STARTTLS...")
+ server.starttls()
+
+ logger.debug(f"尝试登录邮箱: {email_username}")
+ server.login(email_username, email_password)
+
+ logger.debug("发送邮件...")
+ server.send_message(msg)
+
+ logger.debug("关闭连接...")
+ server.quit()
+
+ logger.info(f"邮件发送成功: {to_email}")
+ return True
+ except Exception as e:
+ logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
+ return False
+
+
+def generate_verification_code(length=6):
+ """
+ 生成数字验证码
+ """
+ return ''.join(random.choice(string.digits) for _ in range(length))
diff --git a/app/utils/helpers.py b/app/utils/helpers.py
new file mode 100644
index 0000000..e69de29
diff --git a/code_collection.txt b/code_collection.txt
new file mode 100644
index 0000000..22f4572
--- /dev/null
+++ b/code_collection.txt
@@ -0,0 +1,2640 @@
+
+================================================================================
+File: ./config.py
+================================================================================
+
+import os
+
+# 数据库配置
+DB_HOST = os.environ.get('DB_HOST', '27.124.22.104')
+DB_PORT = os.environ.get('DB_PORT', '3306')
+DB_USER = os.environ.get('DB_USER', 'book20250428')
+DB_PASSWORD = os.environ.get('DB_PASSWORD', 'booksystem')
+DB_NAME = os.environ.get('DB_NAME', 'book_system')
+
+# 数据库连接字符串
+SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+
+# 应用密钥
+SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key_replace_in_production')
+
+# 邮件配置
+EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com')
+EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
+EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls')
+EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com')
+EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf')
+EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com')
+EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL')
+
+# 会话配置
+PERMANENT_SESSION_LIFETIME = 86400 * 7
+================================================================================
+File: ./all_file_output.py
+================================================================================
+
+import os
+import sys
+
+
+def collect_code_files(output_file="code_collection.txt"):
+ # 定义代码文件扩展名
+ code_extensions = [
+ '.py', '.java', '.cpp', '.c', '.h', '.hpp', '.cs',
+ '.js', '.html', '.css', '.php', '.go', '.rb',
+ '.swift', '.kt', '.ts', '.sh', '.pl', '.r'
+ ]
+
+ # 定义要排除的目录
+ excluded_dirs = [
+ 'venv', 'env', '.venv', '.env', 'virtualenv',
+ '__pycache__', 'node_modules', '.git', '.idea',
+ 'dist', 'build', 'target', 'bin'
+ ]
+
+ # 计数器
+ file_count = 0
+
+ # 打开输出文件
+ with open(output_file, 'w', encoding='utf-8') as out_file:
+ # 遍历当前目录及所有子目录
+ for root, dirs, files in os.walk('.'):
+ # 从dirs中移除排除的目录,这会阻止os.walk进入这些目录
+ dirs[:] = [d for d in dirs if d not in excluded_dirs]
+
+ for file in files:
+ # 获取文件扩展名
+ _, ext = os.path.splitext(file)
+
+ # 检查是否为代码文件
+ if ext.lower() in code_extensions:
+ file_path = os.path.join(root, file)
+ file_count += 1
+
+ # 写入文件路径作为分隔
+ out_file.write(f"\n{'=' * 80}\n")
+ out_file.write(f"File: {file_path}\n")
+ out_file.write(f"{'=' * 80}\n\n")
+
+ # 尝试读取文件内容并写入
+ try:
+ with open(file_path, 'r', encoding='utf-8') as code_file:
+ out_file.write(code_file.read())
+ except UnicodeDecodeError:
+ # 尝试用不同的编码
+ try:
+ with open(file_path, 'r', encoding='latin-1') as code_file:
+ out_file.write(code_file.read())
+ except Exception as e:
+ out_file.write(f"无法读取文件内容: {str(e)}\n")
+ except Exception as e:
+ out_file.write(f"读取文件时出错: {str(e)}\n")
+
+ print(f"已成功收集 {file_count} 个代码文件到 {output_file}")
+
+
+if __name__ == "__main__":
+ # 如果提供了命令行参数,则使用它作为输出文件名
+ output_file = sys.argv[1] if len(sys.argv) > 1 else "code_collection.txt"
+ collect_code_files(output_file)
+================================================================================
+File: ./app.py
+================================================================================
+
+from app import create_app
+
+app = create_app()
+
+if __name__ == '__main__':
+ app.run(debug=True, host='0.0.0.0', port=49666)
+
+================================================================================
+File: ./main.py
+================================================================================
+
+# 这是一个示例 Python 脚本。
+
+# 按 ⌃R 执行或将其替换为您的代码。
+# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
+
+
+def print_hi(name):
+ # 在下面的代码行中使用断点来调试脚本。
+ print(f'Hi, {name}') # 按 ⌘F8 切换断点。
+
+
+# 按间距中的绿色按钮以运行脚本。
+if __name__ == '__main__':
+ print_hi('PyCharm')
+
+# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
+
+================================================================================
+File: ./app/__init__.py
+================================================================================
+
+from flask import Flask, render_template, session, g
+from app.models.user import db, User
+from app.controllers.user import user_bp
+import os
+
+
+def create_app():
+ app = Flask(__name__)
+
+ # 配置应用
+ app.config.from_mapping(
+ SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'),
+ SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system',
+ SQLALCHEMY_TRACK_MODIFICATIONS=False,
+ PERMANENT_SESSION_LIFETIME=86400 * 7, # 7天
+
+ # 邮件配置
+ EMAIL_HOST='smtp.qq.com',
+ EMAIL_PORT=587,
+ EMAIL_ENCRYPTION='starttls',
+ EMAIL_USERNAME='3399560459@qq.com',
+ EMAIL_PASSWORD='fzwhyirhbqdzcjgf', # 这是你的SMTP授权码,不是邮箱密码
+ EMAIL_FROM='3399560459@qq.com',
+ EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL'
+ )
+
+ # 实例配置,如果存在
+ app.config.from_pyfile('config.py', silent=True)
+
+ # 初始化数据库
+ db.init_app(app)
+
+ # 注册蓝图
+ app.register_blueprint(user_bp, url_prefix='/user')
+
+ # 创建数据库表
+ with app.app_context():
+ db.create_all()
+
+ # 创建默认角色
+ from app.models.user import Role
+ if not Role.query.filter_by(id=1).first():
+ admin_role = Role(id=1, role_name='管理员', description='系统管理员')
+ db.session.add(admin_role)
+
+ if not Role.query.filter_by(id=2).first():
+ user_role = Role(id=2, role_name='普通用户', description='普通用户')
+ db.session.add(user_role)
+
+ # 创建管理员账号
+ if not User.query.filter_by(username='admin').first():
+ admin = User(
+ username='admin',
+ password='admin123',
+ email='admin@example.com',
+ role_id=1,
+ nickname='系统管理员'
+ )
+ db.session.add(admin)
+
+ db.session.commit()
+
+ # 请求前处理
+ @app.before_request
+ def load_logged_in_user():
+ user_id = session.get('user_id')
+
+ if user_id is None:
+ g.user = None
+ else:
+ g.user = User.query.get(user_id)
+
+ # 首页路由
+ @app.route('/')
+ def index():
+ if not g.user:
+ return render_template('login.html')
+ return render_template('index.html', current_user=g.user)
+
+ # 错误处理
+ @app.errorhandler(404)
+ def page_not_found(e):
+ return render_template('404.html'), 404
+
+ return app
+================================================================================
+File: ./app/utils/auth.py
+================================================================================
+
+
+================================================================================
+File: ./app/utils/db.py
+================================================================================
+
+
+================================================================================
+File: ./app/utils/__init__.py
+================================================================================
+
+
+================================================================================
+File: ./app/utils/email.py
+================================================================================
+
+import smtplib
+import random
+import string
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+from flask import current_app
+import logging
+
+# 配置日志
+logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger(__name__)
+
+
+# 配置邮件发送功能
+def send_verification_email(to_email, verification_code):
+ """
+ 发送验证码邮件
+ """
+ try:
+ # 从应用配置获取邮件设置
+ email_host = current_app.config['EMAIL_HOST']
+ email_port = current_app.config['EMAIL_PORT']
+ email_username = current_app.config['EMAIL_USERNAME']
+ email_password = current_app.config['EMAIL_PASSWORD']
+ email_from = current_app.config['EMAIL_FROM']
+ email_from_name = current_app.config['EMAIL_FROM_NAME']
+
+ logger.info(f"准备发送邮件到: {to_email}, 验证码: {verification_code}")
+ logger.debug(f"邮件配置: 主机={email_host}, 端口={email_port}")
+
+ # 邮件内容
+ msg = MIMEMultipart()
+ msg['From'] = f"{email_from_name} <{email_from}>"
+ msg['To'] = to_email
+ msg['Subject'] = "图书管理系统 - 验证码"
+
+ # 邮件正文
+ body = f"""
+
+
+
+
图书管理系统 - 邮箱验证
+
您好,
+
感谢您注册图书管理系统,您的验证码是:
+
+ {verification_code}
+
+
该验证码将在10分钟内有效,请勿将验证码分享给他人。
+
如果您没有请求此验证码,请忽略此邮件。
+
+
此邮件为系统自动发送,请勿回复。
+
© 2025 图书管理系统
+
+
+
+
+ """
+
+ msg.attach(MIMEText(body, 'html'))
+
+ logger.debug("尝试连接到SMTP服务器...")
+ # 连接服务器发送邮件
+ server = smtplib.SMTP(email_host, email_port)
+ server.set_debuglevel(1) # 启用详细的SMTP调试输出
+
+ logger.debug("检查是否需要STARTTLS加密...")
+ if current_app.config.get('EMAIL_ENCRYPTION') == 'starttls':
+ logger.debug("启用STARTTLS...")
+ server.starttls()
+
+ logger.debug(f"尝试登录邮箱: {email_username}")
+ server.login(email_username, email_password)
+
+ logger.debug("发送邮件...")
+ server.send_message(msg)
+
+ logger.debug("关闭连接...")
+ server.quit()
+
+ logger.info(f"邮件发送成功: {to_email}")
+ return True
+ except Exception as e:
+ logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
+ return False
+
+
+def generate_verification_code(length=6):
+ """
+ 生成数字验证码
+ """
+ return ''.join(random.choice(string.digits) for _ in range(length))
+
+================================================================================
+File: ./app/utils/helpers.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/user.py
+================================================================================
+
+from flask_sqlalchemy import SQLAlchemy
+from werkzeug.security import generate_password_hash, check_password_hash
+from datetime import datetime
+
+db = SQLAlchemy()
+
+
+class User(db.Model):
+ __tablename__ = 'users'
+
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ username = db.Column(db.String(64), unique=True, nullable=False)
+ password = db.Column(db.String(255), nullable=False)
+ email = db.Column(db.String(128), unique=True, nullable=True)
+ phone = db.Column(db.String(20), unique=True, nullable=True)
+ nickname = db.Column(db.String(64), nullable=True)
+ status = db.Column(db.Integer, default=1) # 1: active, 0: disabled
+ role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), default=2) # 2: 普通用户, 1: 管理员
+ created_at = db.Column(db.DateTime, default=datetime.now)
+ updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
+
+ def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2):
+ self.username = username
+ self.set_password(password)
+ self.email = email
+ self.phone = phone
+ self.nickname = nickname
+ self.role_id = role_id
+
+ def set_password(self, password):
+ """设置密码,使用哈希加密"""
+ self.password = generate_password_hash(password)
+
+ def check_password(self, password):
+ """验证密码"""
+ return check_password_hash(self.password, password)
+
+ def to_dict(self):
+ """转换为字典格式"""
+ return {
+ 'id': self.id,
+ 'username': self.username,
+ 'email': self.email,
+ 'phone': self.phone,
+ 'nickname': self.nickname,
+ 'status': self.status,
+ 'role_id': self.role_id,
+ 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
+ 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
+ }
+
+ @classmethod
+ def create_user(cls, username, password, email=None, phone=None, nickname=None, role_id=2):
+ """创建新用户"""
+ user = User(
+ username=username,
+ password=password,
+ email=email,
+ phone=phone,
+ nickname=nickname,
+ role_id=role_id
+ )
+ db.session.add(user)
+ db.session.commit()
+ return user
+
+
+class Role(db.Model):
+ __tablename__ = 'roles'
+
+ id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+ role_name = db.Column(db.String(32), unique=True, nullable=False)
+ description = db.Column(db.String(128))
+
+ users = db.relationship('User', backref='role')
+
+================================================================================
+File: ./app/models/log.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/notification.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/__init__.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/book.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/borrow.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/announcement.py
+================================================================================
+
+
+================================================================================
+File: ./app/models/inventory.py
+================================================================================
+
+
+================================================================================
+File: ./app/static/css/index.css
+================================================================================
+
+/* index.css - 仅用于图书管理系统首页/仪表板 */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
+}
+
+body {
+ background-color: #f5f7fa;
+ color: #333;
+ font-size: 16px;
+ line-height: 1.6;
+}
+
+a {
+ text-decoration: none;
+ color: #4a89dc;
+}
+
+ul {
+ list-style: none;
+}
+
+/* 应用容器 */
+.app-container {
+ display: flex;
+ min-height: 100vh;
+}
+
+/* 侧边导航栏 */
+.sidebar {
+ width: 250px;
+ background-color: #2c3e50;
+ color: #ecf0f1;
+ padding: 20px 0;
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
+ position: fixed;
+ height: 100vh;
+ overflow-y: auto;
+}
+
+.logo-container {
+ padding: 0 20px 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ margin-bottom: 20px;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.logo {
+ width: 60px;
+ height: auto;
+ margin-bottom: 10px;
+}
+
+.logo-container h2 {
+ font-size: 1.2rem;
+ margin: 10px 0;
+ color: #ecf0f1;
+ font-weight: 500;
+}
+
+.nav-links li {
+ margin-bottom: 5px;
+}
+
+.nav-links li a {
+ padding: 10px 20px;
+ display: flex;
+ align-items: center;
+ color: #bdc3c7;
+ transition: all 0.3s ease;
+}
+
+.nav-links li a i {
+ margin-right: 10px;
+ font-size: 1.1rem;
+ width: 20px;
+ text-align: center;
+}
+
+.nav-links li a:hover, .nav-links li.active a {
+ background-color: #34495e;
+ color: #ecf0f1;
+ border-left: 3px solid #4a89dc;
+}
+
+.nav-category {
+ padding: 10px 20px;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ color: #7f8c8d;
+ margin-top: 15px;
+ margin-bottom: 5px;
+}
+
+/* 主内容区 */
+.main-content {
+ flex: 1;
+ margin-left: 250px;
+ padding: 20px;
+}
+
+/* 顶部导航栏 */
+.top-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 30px;
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ margin-bottom: 20px;
+}
+
+.search-container {
+ position: relative;
+ width: 300px;
+}
+
+.search-input {
+ padding: 10px 15px 10px 40px;
+ width: 100%;
+ border: 1px solid #e1e4e8;
+ border-radius: 20px;
+ font-size: 14px;
+ transition: all 0.3s ease;
+}
+
+.search-input:focus {
+ border-color: #4a89dc;
+ box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.1);
+ outline: none;
+}
+
+.search-icon {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: #8492a6;
+}
+
+.user-menu {
+ display: flex;
+ align-items: center;
+}
+
+.notifications {
+ margin-right: 20px;
+ position: relative;
+ cursor: pointer;
+}
+
+.notifications i {
+ font-size: 1.2rem;
+ color: #606266;
+}
+
+.badge {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background-color: #f56c6c;
+ color: white;
+ font-size: 0.7rem;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.user-info {
+ display: flex;
+ align-items: center;
+ position: relative;
+ cursor: pointer;
+}
+
+.user-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ background-color: #4a89dc;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ margin-right: 10px;
+ font-size: 1.2rem;
+}
+
+.user-details {
+ display: flex;
+ flex-direction: column;
+}
+
+.user-name {
+ font-weight: 500;
+ color: #333;
+}
+
+.user-role {
+ font-size: 0.8rem;
+ color: #8492a6;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ background-color: white;
+ border-radius: 4px;
+ box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
+ padding: 10px 0;
+ min-width: 150px;
+ display: none;
+ z-index: 10;
+}
+
+.user-info.active .dropdown-menu {
+ display: block;
+}
+
+.dropdown-menu a {
+ display: block;
+ padding: 8px 15px;
+ color: #606266;
+ transition: all 0.3s ease;
+}
+
+.dropdown-menu a:hover {
+ background-color: #f5f7fa;
+}
+
+.dropdown-menu a i {
+ margin-right: 8px;
+ width: 16px;
+ text-align: center;
+}
+
+/* 欢迎区域 */
+.welcome-section {
+ background: linear-gradient(to right, #4a89dc, #5d9cec);
+ color: white;
+ padding: 30px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.05);
+}
+
+.welcome-section h1 {
+ font-size: 1.8rem;
+ margin-bottom: 5px;
+}
+
+.welcome-section p {
+ font-size: 1rem;
+ opacity: 0.9;
+}
+
+/* 统计卡片样式 */
+.stats-container {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.stat-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+}
+
+.stat-icon {
+ font-size: 2rem;
+ color: #4a89dc;
+ margin-right: 15px;
+ width: 40px;
+ text-align: center;
+}
+
+.stat-info h3 {
+ font-size: 0.9rem;
+ color: #606266;
+ margin-bottom: 5px;
+}
+
+.stat-number {
+ font-size: 1.8rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+/* 主要内容区域 */
+.main-sections {
+ display: grid;
+ grid-template-columns: 2fr 1fr;
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.content-section {
+ background-color: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ padding: 20px;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #edf2f7;
+}
+
+.section-header h2 {
+ font-size: 1.2rem;
+ color: #2c3e50;
+}
+
+.view-all {
+ font-size: 0.85rem;
+ color: #4a89dc;
+ display: flex;
+ align-items: center;
+}
+
+.view-all i {
+ margin-left: 5px;
+ transition: transform 0.3s ease;
+}
+
+.view-all:hover i {
+ transform: translateX(3px);
+}
+
+/* 图书卡片样式 */
+.book-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+}
+
+.book-card {
+ display: flex;
+ border: 1px solid #edf2f7;
+ border-radius: 8px;
+ overflow: hidden;
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.book-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 5px 15px rgba(0,0,0,0.05);
+}
+
+.book-cover {
+ width: 100px;
+ height: 140px;
+ min-width: 100px;
+ background-color: #f5f7fa;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.book-cover img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.book-info {
+ padding: 15px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.book-title {
+ font-size: 1rem;
+ margin-bottom: 5px;
+ color: #2c3e50;
+ display: -webkit-box;
+ -webkit-line-clamp: 1;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.book-author {
+ font-size: 0.85rem;
+ color: #606266;
+ margin-bottom: 10px;
+}
+
+.book-meta {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.book-category {
+ background-color: #e5f1ff;
+ color: #4a89dc;
+ padding: 3px 8px;
+ border-radius: 4px;
+ font-size: 0.75rem;
+}
+
+.book-status {
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.book-status.available {
+ color: #67c23a;
+}
+
+.book-status.borrowed {
+ color: #e6a23c;
+}
+
+.borrow-btn {
+ background-color: #4a89dc;
+ color: white;
+ border: none;
+ padding: 6px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ margin-top: auto;
+ transition: background-color 0.3s ease;
+}
+
+.borrow-btn:hover {
+ background-color: #357bc8;
+}
+
+/* 通知公告样式 */
+.notice-item {
+ display: flex;
+ padding: 15px 0;
+ border-bottom: 1px solid #edf2f7;
+}
+
+.notice-item:last-child {
+ border-bottom: none;
+}
+
+.notice-icon {
+ font-size: 1.5rem;
+ color: #4a89dc;
+ margin-right: 15px;
+ display: flex;
+ align-items: flex-start;
+ padding-top: 5px;
+}
+
+.notice-content h3 {
+ font-size: 1rem;
+ color: #2c3e50;
+ margin-bottom: 5px;
+}
+
+.notice-content p {
+ font-size: 0.9rem;
+ color: #606266;
+ margin-bottom: 10px;
+}
+
+.notice-meta {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.notice-time {
+ font-size: 0.8rem;
+ color: #8492a6;
+}
+
+.renew-btn {
+ background-color: #ecf5ff;
+ color: #4a89dc;
+ border: 1px solid #d9ecff;
+ padding: 5px 10px;
+ border-radius: 4px;
+ font-size: 0.8rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.renew-btn:hover {
+ background-color: #4a89dc;
+ color: white;
+ border-color: #4a89dc;
+}
+
+/* 热门图书区域 */
+.popular-section {
+ margin-top: 20px;
+}
+
+.popular-books {
+ display: flex;
+ overflow-x: auto;
+ gap: 15px;
+ padding-bottom: 10px;
+}
+
+.popular-book-item {
+ display: flex;
+ background-color: #f8fafc;
+ border-radius: 8px;
+ padding: 15px;
+ min-width: 280px;
+ position: relative;
+}
+
+.rank-badge {
+ position: absolute;
+ top: -10px;
+ left: 10px;
+ background-color: #4a89dc;
+ color: white;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ font-size: 0.8rem;
+ font-weight: bold;
+}
+
+.book-cover.small {
+ width: 60px;
+ height: 90px;
+ min-width: 60px;
+ margin-right: 15px;
+}
+
+.book-details {
+ flex: 1;
+}
+
+.book-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ margin-top: 10px;
+}
+
+.book-stats span {
+ font-size: 0.8rem;
+ color: #8492a6;
+}
+
+.book-stats i {
+ margin-right: 5px;
+}
+
+/* 响应式调整 */
+@media (max-width: 1200px) {
+ .stats-container {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .main-sections {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ width: 70px;
+ overflow: hidden;
+ }
+
+ .logo-container {
+ padding: 10px;
+ }
+
+ .logo-container h2 {
+ display: none;
+ }
+
+ .nav-links li a span {
+ display: none;
+ }
+
+ .nav-links li a i {
+ margin-right: 0;
+ }
+
+ .nav-category {
+ display: none;
+ }
+
+ .main-content {
+ margin-left: 70px;
+ }
+
+ .search-container {
+ width: 180px;
+ }
+
+ .book-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 576px) {
+ .stats-container {
+ grid-template-columns: 1fr;
+ }
+
+ .top-bar {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .search-container {
+ width: 100%;
+ }
+
+ .user-details {
+ display: none;
+ }
+}
+================================================================================
+File: ./app/static/css/main.css
+================================================================================
+
+/* 主样式文件 - 从登录页面复制过来的样式 */
+/* 从您提供的登录页CSS复制,但省略了不需要的部分 */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+}
+
+:root {
+ --primary-color: #4a89dc;
+ --primary-hover: #3b78c4;
+ --secondary-color: #5cb85c;
+ --text-color: #333;
+ --light-text: #666;
+ --bg-color: #f5f7fa;
+ --card-bg: #ffffff;
+ --border-color: #ddd;
+ --error-color: #e74c3c;
+ --success-color: #2ecc71;
+}
+
+body.dark-mode {
+ --primary-color: #5a9aed;
+ --primary-hover: #4a89dc;
+ --secondary-color: #6bc76b;
+ --text-color: #f1f1f1;
+ --light-text: #aaa;
+ --bg-color: #1a1a1a;
+ --card-bg: #2c2c2c;
+ --border-color: #444;
+}
+
+body {
+ background-color: var(--bg-color);
+ background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+ background-size: cover;
+ background-position: center;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ color: var(--text-color);
+ transition: all 0.3s ease;
+}
+
+.theme-toggle {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ z-index: 10;
+ cursor: pointer;
+ padding: 8px;
+ border-radius: 50%;
+ background-color: rgba(255, 255, 255, 0.2);
+ backdrop-filter: blur(5px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.overlay {
+ background-color: rgba(0, 0, 0, 0.4);
+ backdrop-filter: blur(5px);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: -1;
+}
+
+.main-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex: 1;
+ padding: 20px;
+}
+
+.login-container {
+ background-color: var(--card-bg);
+ border-radius: 12px;
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
+ width: 450px;
+ padding: 35px;
+ position: relative;
+ overflow: hidden;
+ animation: fadeIn 0.5s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.logo {
+ text-align: center;
+ margin-bottom: 25px;
+ position: relative;
+}
+
+.logo img {
+ width: 90px;
+ height: 90px;
+ border-radius: 12px;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ padding: 5px;
+ background-color: #fff;
+ transition: transform 0.3s ease;
+}
+
+h1 {
+ text-align: center;
+ color: var(--text-color);
+ margin-bottom: 10px;
+ font-weight: 600;
+ font-size: 28px;
+}
+
+.subtitle {
+ text-align: center;
+ color: var(--light-text);
+ margin-bottom: 30px;
+ font-size: 14px;
+}
+
+.form-group {
+ margin-bottom: 22px;
+ position: relative;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 8px;
+ color: var(--text-color);
+ font-weight: 500;
+ font-size: 14px;
+}
+
+.input-with-icon {
+ position: relative;
+}
+
+.input-icon {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--light-text);
+}
+
+.form-control {
+ width: 100%;
+ height: 48px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0 15px 0 45px;
+ font-size: 15px;
+ transition: all 0.3s ease;
+ background-color: var(--card-bg);
+ color: var(--text-color);
+}
+
+.form-control:focus {
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
+ outline: none;
+}
+
+.password-toggle {
+ position: absolute;
+ right: 15px;
+ top: 50%;
+ transform: translateY(-50%);
+ cursor: pointer;
+ color: var(--light-text);
+}
+
+.validation-message {
+ margin-top: 6px;
+ font-size: 12px;
+ color: var(--error-color);
+ display: none;
+}
+
+.validation-message.show {
+ display: block;
+ animation: shake 0.5s ease;
+}
+
+@keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
+ 20%, 40%, 60%, 80% { transform: translateX(5px); }
+}
+
+.remember-forgot {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 25px;
+}
+
+.custom-checkbox {
+ position: relative;
+ padding-left: 30px;
+ cursor: pointer;
+ font-size: 14px;
+ user-select: none;
+ color: var(--light-text);
+}
+
+.custom-checkbox input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+}
+
+.checkmark {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 18px;
+ width: 18px;
+ background-color: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 3px;
+ transition: all 0.2s ease;
+}
+
+.custom-checkbox:hover input ~ .checkmark {
+ border-color: var(--primary-color);
+}
+
+.custom-checkbox input:checked ~ .checkmark {
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+.checkmark:after {
+ content: "";
+ position: absolute;
+ display: none;
+}
+
+.custom-checkbox input:checked ~ .checkmark:after {
+ display: block;
+}
+
+.custom-checkbox .checkmark:after {
+ left: 6px;
+ top: 2px;
+ width: 4px;
+ height: 9px;
+ border: solid white;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+
+.forgot-password a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-size: 14px;
+ transition: color 0.3s ease;
+}
+
+.forgot-password a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+}
+
+.btn-login {
+ width: 100%;
+ height: 48px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-login:hover {
+ background-color: var(--primary-hover);
+}
+
+.btn-login:active {
+ transform: scale(0.98);
+}
+
+.btn-login .loading {
+ display: none;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.btn-login.loading-state {
+ color: transparent;
+}
+
+.btn-login.loading-state .loading {
+ display: block;
+}
+
+.signup {
+ text-align: center;
+ margin-top: 25px;
+ font-size: 14px;
+ color: var(--light-text);
+}
+
+.signup a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 600;
+ transition: color 0.3s ease;
+}
+
+.signup a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+}
+
+.features {
+ display: flex;
+ justify-content: center;
+ margin-top: 25px;
+ gap: 30px;
+}
+
+.feature-item {
+ text-align: center;
+ font-size: 12px;
+ color: var(--light-text);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.feature-icon {
+ margin-bottom: 5px;
+ font-size: 18px;
+}
+
+footer {
+ text-align: center;
+ padding: 20px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 12px;
+}
+
+footer a {
+ color: rgba(255, 255, 255, 0.9);
+ text-decoration: none;
+}
+
+.alert {
+ padding: 10px;
+ margin-bottom: 15px;
+ border-radius: 4px;
+ color: #721c24;
+ background-color: #f8d7da;
+ border: 1px solid #f5c6cb;
+}
+
+.verification-code-container {
+ display: flex;
+ gap: 10px;
+}
+
+.verification-input {
+ flex: 1;
+ height: 48px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0 15px;
+ font-size: 15px;
+}
+
+.send-code-btn {
+ padding: 0 15px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+.register-container {
+ width: 500px;
+}
+
+@media (max-width: 576px) {
+ .login-container {
+ width: 100%;
+ padding: 25px;
+ border-radius: 0;
+ }
+
+ .theme-toggle {
+ top: 10px;
+ }
+
+ .logo img {
+ width: 70px;
+ height: 70px;
+ }
+
+ h1 {
+ font-size: 22px;
+ }
+
+ .main-container {
+ padding: 0;
+ }
+
+ .verification-code-container {
+ flex-direction: column;
+ }
+
+ .register-container {
+ width: 100%;
+ }
+}
+
+.verification-code-container {
+ display: flex;
+ gap: 10px;
+}
+
+.verification-input {
+ flex: 1;
+ height: 48px;
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ padding: 0 15px;
+ font-size: 15px;
+ transition: all 0.3s ease;
+ background-color: var(--card-bg);
+ color: var(--text-color);
+}
+
+.send-code-btn {
+ padding: 0 15px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ white-space: nowrap;
+ transition: all 0.3s ease;
+}
+
+.send-code-btn:hover {
+ background-color: var(--primary-hover);
+}
+
+.send-code-btn:disabled {
+ background-color: #ccc;
+ cursor: not-allowed;
+}
+
+================================================================================
+File: ./app/static/js/main.js
+================================================================================
+
+// 主JS文件 - 包含登录和注册功能
+document.addEventListener('DOMContentLoaded', function() {
+ // 主题切换
+ const themeToggle = document.getElementById('theme-toggle');
+ if (themeToggle) {
+ const body = document.body;
+
+ themeToggle.addEventListener('click', function() {
+ body.classList.toggle('dark-mode');
+ const isDarkMode = body.classList.contains('dark-mode');
+ localStorage.setItem('dark-mode', isDarkMode);
+ themeToggle.innerHTML = isDarkMode ? '🌙' : '☀️';
+ });
+
+ // 从本地存储中加载主题首选项
+ const savedDarkMode = localStorage.getItem('dark-mode') === 'true';
+ if (savedDarkMode) {
+ body.classList.add('dark-mode');
+ themeToggle.innerHTML = '🌙';
+ }
+ }
+
+ // 密码可见性切换
+ const passwordToggle = document.getElementById('password-toggle');
+ if (passwordToggle) {
+ const passwordInput = document.getElementById('password');
+ passwordToggle.addEventListener('click', function() {
+ const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
+ passwordInput.setAttribute('type', type);
+ passwordToggle.innerHTML = type === 'password' ? '👁️' : '👁️🗨️';
+ });
+ }
+
+ // 登录表单验证
+ const loginForm = document.getElementById('login-form');
+ if (loginForm) {
+ const usernameInput = document.getElementById('username');
+ const passwordInput = document.getElementById('password');
+ const usernameError = document.getElementById('username-error');
+ const passwordError = document.getElementById('password-error');
+ const loginButton = document.getElementById('login-button');
+
+ if (usernameInput && usernameError) {
+ usernameInput.addEventListener('input', function() {
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ } else {
+ usernameError.classList.remove('show');
+ }
+ });
+ }
+
+ if (passwordInput && passwordError) {
+ passwordInput.addEventListener('input', function() {
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ } else {
+ passwordError.classList.remove('show');
+ }
+ });
+ }
+
+ loginForm.addEventListener('submit', function(e) {
+ let isValid = true;
+
+ // 验证用户名
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证密码
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ isValid = false;
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ isValid = false;
+ }
+
+ if (!isValid) {
+ e.preventDefault();
+ } else if (loginButton) {
+ loginButton.classList.add('loading-state');
+ }
+ });
+ }
+ // 注册表单验证
+ const registerForm = document.getElementById('register-form');
+ if (registerForm) {
+ const usernameInput = document.getElementById('username');
+ const emailInput = document.getElementById('email');
+ const passwordInput = document.getElementById('password');
+ const confirmPasswordInput = document.getElementById('confirm_password');
+ const verificationCodeInput = document.getElementById('verification_code');
+
+ const usernameError = document.getElementById('username-error');
+ const emailError = document.getElementById('email-error');
+ const passwordError = document.getElementById('password-error');
+ const confirmPasswordError = document.getElementById('confirm-password-error');
+ const verificationCodeError = document.getElementById('verification-code-error');
+
+ const registerButton = document.getElementById('register-button');
+ const sendCodeBtn = document.getElementById('send-code-btn');
+
+ // 用户名验证
+ if (usernameInput && usernameError) {
+ usernameInput.addEventListener('input', function() {
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ } else if (usernameInput.value.length < 3) {
+ usernameError.textContent = '用户名至少3个字符';
+ usernameError.classList.add('show');
+ } else {
+ usernameError.classList.remove('show');
+ }
+ });
+ }
+
+ // 邮箱验证
+ if (emailInput && emailError) {
+ emailInput.addEventListener('input', function() {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (emailInput.value.trim() === '') {
+ emailError.textContent = '邮箱不能为空';
+ emailError.classList.add('show');
+ } else if (!emailRegex.test(emailInput.value)) {
+ emailError.textContent = '请输入有效的邮箱地址';
+ emailError.classList.add('show');
+ } else {
+ emailError.classList.remove('show');
+ }
+ });
+ }
+
+ // 密码验证
+ if (passwordInput && passwordError) {
+ passwordInput.addEventListener('input', function() {
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ } else {
+ passwordError.classList.remove('show');
+ }
+
+ // 检查确认密码是否匹配
+ if (confirmPasswordInput && confirmPasswordInput.value) {
+ if (confirmPasswordInput.value !== passwordInput.value) {
+ confirmPasswordError.textContent = '两次输入的密码不匹配';
+ confirmPasswordError.classList.add('show');
+ } else {
+ confirmPasswordError.classList.remove('show');
+ }
+ }
+ });
+ }
+
+ // 确认密码验证
+ if (confirmPasswordInput && confirmPasswordError) {
+ confirmPasswordInput.addEventListener('input', function() {
+ if (confirmPasswordInput.value.trim() === '') {
+ confirmPasswordError.textContent = '请确认密码';
+ confirmPasswordError.classList.add('show');
+ } else if (confirmPasswordInput.value !== passwordInput.value) {
+ confirmPasswordError.textContent = '两次输入的密码不匹配';
+ confirmPasswordError.classList.add('show');
+ } else {
+ confirmPasswordError.classList.remove('show');
+ }
+ });
+ }
+
+ // 发送验证码按钮
+ if (sendCodeBtn) {
+ sendCodeBtn.addEventListener('click', function() {
+ const email = emailInput.value.trim();
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ if (!email) {
+ emailError.textContent = '请输入邮箱地址';
+ emailError.classList.add('show');
+ return;
+ } else if (!emailRegex.test(email)) {
+ emailError.textContent = '请输入有效的邮箱地址';
+ emailError.classList.add('show');
+ return;
+ }
+
+ // 禁用按钮并显示倒计时
+ let countdown = 60;
+ sendCodeBtn.disabled = true;
+ const originalText = sendCodeBtn.textContent;
+ sendCodeBtn.textContent = `${countdown}秒后重试`;
+
+ const timer = setInterval(() => {
+ countdown--;
+ sendCodeBtn.textContent = `${countdown}秒后重试`;
+
+ if (countdown <= 0) {
+ clearInterval(timer);
+ sendCodeBtn.disabled = false;
+ sendCodeBtn.textContent = originalText;
+ }
+ }, 1000);
+
+ // 发送请求获取验证码
+ fetch('/user/send_verification_code', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ email: email }),
+ })
+ .then(response => response.json())
+ .then(data => {
+ console.log("验证码发送响应:", data); // 添加调试日志
+ if (data.success) {
+ showMessage('验证码已发送', '请检查您的邮箱', 'success');
+ } else {
+ showMessage('发送失败', data.message || '请稍后重试', 'error');
+ clearInterval(timer);
+ sendCodeBtn.disabled = false;
+ sendCodeBtn.textContent = originalText;
+ }
+ })
+ .catch(error => {
+ console.error('Error:', error);
+ showMessage('发送失败', '网络错误,请稍后重试', 'error');
+ clearInterval(timer);
+ sendCodeBtn.disabled = false;
+ sendCodeBtn.textContent = originalText;
+ });
+ });
+ }
+
+ // 表单提交验证
+ registerForm.addEventListener('submit', function(e) {
+ let isValid = true;
+
+ // 验证用户名
+ if (usernameInput.value.trim() === '') {
+ usernameError.textContent = '用户名不能为空';
+ usernameError.classList.add('show');
+ isValid = false;
+ } else if (usernameInput.value.length < 3) {
+ usernameError.textContent = '用户名至少3个字符';
+ usernameError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证邮箱
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (emailInput.value.trim() === '') {
+ emailError.textContent = '邮箱不能为空';
+ emailError.classList.add('show');
+ isValid = false;
+ } else if (!emailRegex.test(emailInput.value)) {
+ emailError.textContent = '请输入有效的邮箱地址';
+ emailError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证密码
+ if (passwordInput.value.trim() === '') {
+ passwordError.textContent = '密码不能为空';
+ passwordError.classList.add('show');
+ isValid = false;
+ } else if (passwordInput.value.length < 6) {
+ passwordError.textContent = '密码长度至少6位';
+ passwordError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证确认密码
+ if (confirmPasswordInput.value.trim() === '') {
+ confirmPasswordError.textContent = '请确认密码';
+ confirmPasswordError.classList.add('show');
+ isValid = false;
+ } else if (confirmPasswordInput.value !== passwordInput.value) {
+ confirmPasswordError.textContent = '两次输入的密码不匹配';
+ confirmPasswordError.classList.add('show');
+ isValid = false;
+ }
+
+ // 验证验证码
+ if (verificationCodeInput.value.trim() === '') {
+ verificationCodeError.textContent = '请输入验证码';
+ verificationCodeError.classList.add('show');
+ isValid = false;
+ }
+
+ if (!isValid) {
+ e.preventDefault();
+ } else if (registerButton) {
+ registerButton.classList.add('loading-state');
+ }
+ });
+ }
+
+ // 通知消息显示函数
+ function showMessage(title, message, type) {
+ const notification = document.createElement('div');
+ notification.className = `notification ${type}`;
+
+ const icon = type === 'success' ? '✓' : '✗';
+
+ notification.innerHTML = `
+ ${icon}
+
+
${title}
+
${message}
+
+ `;
+
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ notification.classList.add('show');
+ }, 10);
+
+ setTimeout(() => {
+ notification.classList.remove('show');
+ setTimeout(() => {
+ document.body.removeChild(notification);
+ }, 300);
+ }, 3000);
+ }
+});
+================================================================================
+File: ./app/templates/index.html
+================================================================================
+
+
+
+
+
+
+ 首页 - 图书管理系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
欢迎回来,{{ current_user.username }}!
+
今天是 ,祝您使用愉快。
+
+
+
+
+
+
+
+
+
+
+
+ {% for i in range(4) %}
+
+
+
 }})
+
+
+
示例图书标题
+
作者名
+
+ 计算机
+ 可借阅
+
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
关于五一假期图书馆开放时间调整的通知
+
五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。
+
+ 2023-04-28
+
+
+
+
+
+
+
您有2本图书即将到期
+
《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。
+
+ 2023-04-27
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for i in range(5) %}
+
+
{{ i+1 }}
+
+

+
+
+
热门图书标题示例
+
知名作者
+
+ 1024 次浏览
+ 89 次借阅
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+================================================================================
+File: ./app/templates/base.html
+================================================================================
+
+
+================================================================================
+File: ./app/templates/register.html
+================================================================================
+
+
+
+
+
+
+ 用户注册 - 图书管理系统
+
+
+
+
+
+ ☀️
+
+
+
+
+
 }})
+
+
图书管理系统
+
创建您的新账户
+
+
+
+
+
+
+
+
+
+
+================================================================================
+File: ./app/templates/404.html
+================================================================================
+
+
+
+
+
+
+ 页面未找到 - 图书管理系统
+
+
+
+
+
+
+
404
+
页面未找到
+
抱歉,您访问的页面不存在或已被移除。
+
请检查URL是否正确,或返回首页。
+
返回首页
+
+
+
+
+
+================================================================================
+File: ./app/templates/login.html
+================================================================================
+
+
+
+
+
+
+ 用户登录 - 图书管理系统
+
+
+
+
+
+ ☀️
+
+
+
+
+
 }})
+
+
图书管理系统
+
欢迎回来,请登录您的账户
+
+
+
+
+
+
+
+
+ 🔒
+ 安全登录
+
+
+ 🔐
+ 数据加密
+
+
+ 📚
+ 图书管理
+
+
+
+
+
+
+
+
+
+
+
+================================================================================
+File: ./app/controllers/user.py
+================================================================================
+
+from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify
+from werkzeug.security import generate_password_hash, check_password_hash
+from app.models.user import User, db
+from app.utils.email import send_verification_email, generate_verification_code
+import logging
+from functools import wraps
+import time
+from datetime import datetime, timedelta
+
+# 创建蓝图
+user_bp = Blueprint('user', __name__)
+
+
+# 使用内存字典代替Redis存储验证码
+class VerificationStore:
+ def __init__(self):
+ self.codes = {} # 存储格式: {email: {'code': code, 'expires': timestamp}}
+
+ def setex(self, email, seconds, code):
+ """设置验证码并指定过期时间"""
+ expiry = datetime.now() + timedelta(seconds=seconds)
+ self.codes[email] = {'code': code, 'expires': expiry}
+ return True
+
+ def get(self, email):
+ """获取验证码,如果过期则返回None"""
+ if email not in self.codes:
+ return None
+
+ data = self.codes[email]
+ if datetime.now() > data['expires']:
+ # 验证码已过期,删除它
+ self.delete(email)
+ return None
+
+ return data['code']
+
+ def delete(self, email):
+ """删除验证码"""
+ if email in self.codes:
+ del self.codes[email]
+ return True
+
+
+# 使用内存存储验证码
+verification_codes = VerificationStore()
+
+
+def login_required(f):
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ if 'user_id' not in session:
+ return redirect(url_for('user.login'))
+ return f(*args, **kwargs)
+
+ return decorated_function
+
+
+@user_bp.route('/login', methods=['GET', 'POST'])
+def login():
+ # 保持原代码不变
+ if request.method == 'POST':
+ username = request.form.get('username')
+ password = request.form.get('password')
+ remember_me = request.form.get('remember_me') == 'on'
+
+ if not username or not password:
+ return render_template('login.html', error='用户名和密码不能为空')
+
+ # 检查用户是否存在
+ user = User.query.filter((User.username == username) | (User.email == username)).first()
+
+ if not user or not user.check_password(password):
+ return render_template('login.html', error='用户名或密码错误')
+
+ if user.status == 0:
+ return render_template('login.html', error='账号已被禁用,请联系管理员')
+
+ # 登录成功,保存用户信息到会话
+ session['user_id'] = user.id
+ session['username'] = user.username
+ session['role_id'] = user.role_id
+
+ if remember_me:
+ # 设置会话过期时间为7天
+ session.permanent = True
+
+ # 记录登录日志(可选)
+ # log_user_action('用户登录')
+
+ # 重定向到首页
+ return redirect(url_for('index'))
+
+ return render_template('login.html')
+
+
+@user_bp.route('/register', methods=['GET', 'POST'])
+def register():
+ if request.method == 'POST':
+ username = request.form.get('username')
+ email = request.form.get('email')
+ password = request.form.get('password')
+ confirm_password = request.form.get('confirm_password')
+ verification_code = request.form.get('verification_code')
+
+ # 验证表单数据
+ if not username or not email or not password or not confirm_password or not verification_code:
+ return render_template('register.html', error='所有字段都是必填项')
+
+ if password != confirm_password:
+ return render_template('register.html', error='两次输入的密码不匹配')
+
+ # 检查用户名和邮箱是否已存在
+ if User.query.filter_by(username=username).first():
+ return render_template('register.html', error='用户名已存在')
+
+ if User.query.filter_by(email=email).first():
+ return render_template('register.html', error='邮箱已被注册')
+
+ # 验证验证码
+ stored_code = verification_codes.get(email)
+ if not stored_code or stored_code != verification_code:
+ return render_template('register.html', error='验证码无效或已过期')
+
+ # 创建新用户
+ try:
+ new_user = User(
+ username=username,
+ password=password, # 密码会在模型中自动哈希
+ email=email,
+ nickname=username # 默认昵称与用户名相同
+ )
+ db.session.add(new_user)
+ db.session.commit()
+
+ # 清除验证码
+ verification_codes.delete(email)
+
+ flash('注册成功,请登录', 'success')
+ return redirect(url_for('user.login'))
+ except Exception as e:
+ db.session.rollback()
+ logging.error(f"User registration failed: {str(e)}")
+ return render_template('register.html', error='注册失败,请稍后重试')
+
+ return render_template('register.html')
+
+
+@user_bp.route('/logout')
+def logout():
+ # 清除会话数据
+ session.pop('user_id', None)
+ session.pop('username', None)
+ session.pop('role_id', None)
+ return redirect(url_for('user.login'))
+
+
+@user_bp.route('/send_verification_code', methods=['POST'])
+def send_verification_code():
+ data = request.get_json()
+ email = data.get('email')
+
+ if not email:
+ return jsonify({'success': False, 'message': '请提供邮箱地址'})
+
+ # 检查邮箱格式
+ import re
+ if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
+ return jsonify({'success': False, 'message': '邮箱格式不正确'})
+
+ # 生成验证码
+ code = generate_verification_code()
+
+ # 存储验证码(10分钟有效)
+ verification_codes.setex(email, 600, code) # 10分钟过期
+
+ # 发送验证码邮件
+ if send_verification_email(email, code):
+ return jsonify({'success': True, 'message': '验证码已发送'})
+ else:
+ return jsonify({'success': False, 'message': '邮件发送失败,请稍后重试'})
+
+================================================================================
+File: ./app/controllers/log.py
+================================================================================
+
+
+================================================================================
+File: ./app/controllers/__init__.py
+================================================================================
+
+
+================================================================================
+File: ./app/controllers/book.py
+================================================================================
+
+
+================================================================================
+File: ./app/controllers/statistics.py
+================================================================================
+
+
+================================================================================
+File: ./app/controllers/borrow.py
+================================================================================
+
+
+================================================================================
+File: ./app/controllers/announcement.py
+================================================================================
+
+
+================================================================================
+File: ./app/controllers/inventory.py
+================================================================================
+
+
+================================================================================
+File: ./app/services/borrow_service.py
+================================================================================
+
+
+================================================================================
+File: ./app/services/inventory_service.py
+================================================================================
+
+
+================================================================================
+File: ./app/services/__init__.py
+================================================================================
+
+
+================================================================================
+File: ./app/services/book_service.py
+================================================================================
+
+
+================================================================================
+File: ./app/services/user_service.py
+================================================================================
+
diff --git a/config.py b/config.py
new file mode 100644
index 0000000..9e301c2
--- /dev/null
+++ b/config.py
@@ -0,0 +1,27 @@
+import os
+
+# 数据库配置
+DB_HOST = os.environ.get('DB_HOST', '27.124.22.104')
+DB_PORT = os.environ.get('DB_PORT', '3306')
+DB_USER = os.environ.get('DB_USER', 'book20250428')
+DB_PASSWORD = os.environ.get('DB_PASSWORD', 'booksystem')
+DB_NAME = os.environ.get('DB_NAME', 'book_system')
+
+# 数据库连接字符串
+SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+
+# 应用密钥
+SECRET_KEY = os.environ.get('SECRET_KEY', 'dev_key_replace_in_production')
+
+# 邮件配置
+EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.qq.com')
+EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
+EMAIL_ENCRYPTION = os.environ.get('EMAIL_ENCRYPTION', 'starttls')
+EMAIL_USERNAME = os.environ.get('EMAIL_USERNAME', '3399560459@qq.com')
+EMAIL_PASSWORD = os.environ.get('EMAIL_PASSWORD', 'fzwhyirhbqdzcjgf')
+EMAIL_FROM = os.environ.get('EMAIL_FROM', '3399560459@qq.com')
+EMAIL_FROM_NAME = os.environ.get('EMAIL_FROM_NAME', 'BOOKSYSTEM_OFFICIAL')
+
+# 会话配置
+PERMANENT_SESSION_LIFETIME = 86400 * 7
\ No newline at end of file
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..5b25c56
--- /dev/null
+++ b/main.py
@@ -0,0 +1,16 @@
+# 这是一个示例 Python 脚本。
+
+# 按 ⌃R 执行或将其替换为您的代码。
+# 按 双击 ⇧ 在所有地方搜索类、文件、工具窗口、操作和设置。
+
+
+def print_hi(name):
+ # 在下面的代码行中使用断点来调试脚本。
+ print(f'Hi, {name}') # 按 ⌘F8 切换断点。
+
+
+# 按间距中的绿色按钮以运行脚本。
+if __name__ == '__main__':
+ print_hi('PyCharm')
+
+# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..89e0e90
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,6 @@
+Flask==2.3.3
+Flask-SQLAlchemy==3.1.1
+pymysql==1.1.0
+Werkzeug==2.3.7
+email-validator==2.1.0.post1
+cryptography
diff --git a/sql/book_system.sql b/sql/book_system.sql
new file mode 100644
index 0000000..10148e4
--- /dev/null
+++ b/sql/book_system.sql
@@ -0,0 +1,193 @@
+/*
+ Navicat Premium Dump SQL
+
+ Source Server : Book_system
+ Source Server Type : MySQL
+ Source Server Version : 80400 (8.4.0)
+ Source Host : 27.124.22.104:3306
+ Source Schema : book_system
+
+ Target Server Type : MySQL
+ Target Server Version : 80400 (8.4.0)
+ File Encoding : 65001
+
+ Date: 29/04/2025 00:41:53
+*/
+
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for announcements
+-- ----------------------------
+DROP TABLE IF EXISTS `announcements`;
+CREATE TABLE `announcements` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `title` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
+ `content` text COLLATE utf8mb4_general_ci NOT NULL,
+ `publisher_id` int NOT NULL,
+ `is_top` tinyint DEFAULT '0',
+ `status` tinyint DEFAULT '1',
+ `created_at` datetime NOT NULL,
+ `updated_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `publisher_id` (`publisher_id`),
+ CONSTRAINT `announcements_ibfk_1` FOREIGN KEY (`publisher_id`) REFERENCES `users` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for books
+-- ----------------------------
+DROP TABLE IF EXISTS `books`;
+CREATE TABLE `books` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `title` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
+ `author` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
+ `publisher` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `category_id` int DEFAULT NULL,
+ `tags` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `isbn` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `publish_year` varchar(16) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `description` text COLLATE utf8mb4_general_ci,
+ `cover_url` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `stock` int DEFAULT '0',
+ `price` decimal(10,2) DEFAULT NULL,
+ `status` tinyint DEFAULT '1',
+ `created_at` datetime NOT NULL,
+ `updated_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `isbn` (`isbn`),
+ KEY `category_id` (`category_id`),
+ CONSTRAINT `books_ibfk_1` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for borrow_records
+-- ----------------------------
+DROP TABLE IF EXISTS `borrow_records`;
+CREATE TABLE `borrow_records` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `user_id` int NOT NULL,
+ `book_id` int NOT NULL,
+ `borrow_date` datetime NOT NULL,
+ `due_date` datetime NOT NULL,
+ `return_date` datetime DEFAULT NULL,
+ `renew_count` int DEFAULT '0',
+ `status` tinyint DEFAULT '1',
+ `remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `created_at` datetime NOT NULL,
+ `updated_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `user_id` (`user_id`),
+ KEY `book_id` (`book_id`),
+ CONSTRAINT `borrow_records_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
+ CONSTRAINT `borrow_records_ibfk_2` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for categories
+-- ----------------------------
+DROP TABLE IF EXISTS `categories`;
+CREATE TABLE `categories` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `name` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
+ `parent_id` int DEFAULT NULL,
+ `sort` int DEFAULT '0',
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for inventory_logs
+-- ----------------------------
+DROP TABLE IF EXISTS `inventory_logs`;
+CREATE TABLE `inventory_logs` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `book_id` int NOT NULL,
+ `change_type` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
+ `change_amount` int NOT NULL,
+ `after_stock` int NOT NULL,
+ `operator_id` int DEFAULT NULL,
+ `remark` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `changed_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `book_id` (`book_id`),
+ KEY `operator_id` (`operator_id`),
+ CONSTRAINT `inventory_logs_ibfk_1` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`),
+ CONSTRAINT `inventory_logs_ibfk_2` FOREIGN KEY (`operator_id`) REFERENCES `users` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for logs
+-- ----------------------------
+DROP TABLE IF EXISTS `logs`;
+CREATE TABLE `logs` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `user_id` int DEFAULT NULL,
+ `action` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
+ `target_type` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `target_id` int DEFAULT NULL,
+ `ip_address` varchar(45) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `description` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `created_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `user_id` (`user_id`),
+ CONSTRAINT `logs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for notifications
+-- ----------------------------
+DROP TABLE IF EXISTS `notifications`;
+CREATE TABLE `notifications` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `user_id` int NOT NULL,
+ `title` varchar(128) COLLATE utf8mb4_general_ci NOT NULL,
+ `content` text COLLATE utf8mb4_general_ci NOT NULL,
+ `type` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
+ `status` tinyint DEFAULT '0',
+ `sender_id` int DEFAULT NULL,
+ `created_at` datetime NOT NULL,
+ `read_at` datetime DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `user_id` (`user_id`),
+ KEY `sender_id` (`sender_id`),
+ CONSTRAINT `notifications_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
+ CONSTRAINT `notifications_ibfk_2` FOREIGN KEY (`sender_id`) REFERENCES `users` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for roles
+-- ----------------------------
+DROP TABLE IF EXISTS `roles`;
+CREATE TABLE `roles` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `role_name` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
+ `description` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `role_name` (`role_name`)
+) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+-- ----------------------------
+-- Table structure for users
+-- ----------------------------
+DROP TABLE IF EXISTS `users`;
+CREATE TABLE `users` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `username` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
+ `password` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
+ `email` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `phone` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `nickname` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
+ `status` tinyint DEFAULT '1',
+ `role_id` int NOT NULL DEFAULT '2',
+ `created_at` datetime NOT NULL,
+ `updated_at` datetime NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `username` (`username`),
+ UNIQUE KEY `email` (`email`),
+ UNIQUE KEY `phone` (`phone`),
+ KEY `role_id` (`role_id`),
+ CONSTRAINT `users_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/sql/module1_user.sql b/sql/module1_user.sql
new file mode 100644
index 0000000..3236908
--- /dev/null
+++ b/sql/module1_user.sql
@@ -0,0 +1,28 @@
+-- 角色表
+CREATE TABLE `roles` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `role_name` VARCHAR(32) NOT NULL UNIQUE,
+ `description` VARCHAR(128)
+);
+
+-- 用户表
+CREATE TABLE `users` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `username` VARCHAR(64) NOT NULL UNIQUE,
+ `password` VARCHAR(255) NOT NULL,
+ `email` VARCHAR(128) UNIQUE,
+ `phone` VARCHAR(20) UNIQUE,
+ `nickname` VARCHAR(64),
+ `status` TINYINT DEFAULT 1,
+ `role_id` INT NOT NULL DEFAULT 2,
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`)
+);
+
+-- (可选)初始化角色数据
+INSERT INTO `roles` (`role_name`, `description`) VALUES
+('admin', '管理员'),
+('user', '普通用户');
+
+
diff --git a/sql/module2_book_info.sql b/sql/module2_book_info.sql
new file mode 100644
index 0000000..786a034
--- /dev/null
+++ b/sql/module2_book_info.sql
@@ -0,0 +1,47 @@
+-- 分类表
+CREATE TABLE `categories` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `name` VARCHAR(64) NOT NULL,
+ `parent_id` INT DEFAULT NULL, -- 支持多级分类。顶级分类parent_id为NULL
+ `sort` INT DEFAULT 0 -- 排序字段,可选
+);
+
+-- 图书信息表
+CREATE TABLE `books` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `title` VARCHAR(255) NOT NULL, -- 书名
+ `author` VARCHAR(128) NOT NULL, -- 作者
+ `publisher` VARCHAR(128), -- 出版社
+ `category_id` INT, -- 分类外键
+ `tags` VARCHAR(255), -- 标签(字符串,逗号分隔,可选)
+ `isbn` VARCHAR(32) UNIQUE, -- ISBN
+ `publish_year` VARCHAR(16), -- 出版年份
+ `description` TEXT, -- 简介
+ `cover_url` VARCHAR(255), -- 封面图片地址
+ `stock` INT DEFAULT 0, -- 库存
+ `price` DECIMAL(10,2), -- 定价
+ `status` TINYINT DEFAULT 1, -- 1=正常,0=删除
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ FOREIGN KEY (`category_id`) REFERENCES `categories`(`id`)
+);
+
+INSERT INTO `categories` (`name`, `parent_id`, `sort`) VALUES
+('文学', NULL, 1),
+ ('小说', 1, 1),
+ ('散文', 1, 2),
+('计算机', NULL, 2),
+ ('编程', 4, 1),
+ ('人工智能', 4, 2),
+('历史', NULL, 3),
+('艺术', NULL, 4);
+
+INSERT INTO `books`
+(`title`, `author`, `publisher`, `category_id`, `tags`, `isbn`, `publish_year`, `description`, `cover_url`, `stock`, `price`, `status`, `created_at`, `updated_at`)
+VALUES
+('三体', '刘慈欣', '重庆出版社', 2, '科幻,宇宙', '9787229100605', '2008', '中国著名科幻小说,三体世界的故事。', '/covers/santi.jpg', 10, 45.00, 1, NOW(), NOW()),
+('解忧杂货店', '东野圭吾', '南海出版公司', 1, '治愈,悬疑', '9787544270878', '2014', '通过信件为人们解忧的杂货店故事。', '/covers/jieyou.jpg', 5, 39.80, 1, NOW(), NOW()),
+('Python编程:从入门到实践', 'Eric Matthes', '人民邮电出版社', 5, '编程,Python', '9787115428028', '2016', '一本面向编程初学者的Python实践书籍。', '/covers/python_book.jpg', 8, 59.00, 1, NOW(), NOW()),
+('人工智能简史', '尼克·博斯特罗姆', '浙江人民出版社', 6, 'AI,未来', '9787213064325', '2018', '人工智能发展的历史及其未来展望。', '/covers/ai_history.jpg', 6, 68.00, 1, NOW(), NOW()),
+('百年孤独', '加西亚·马尔克斯', '南海出版公司', 2, '魔幻现实主义', '9787544291170', '2011', '魔幻现实主义经典小说。', '/covers/bainiangudu.jpg', 3, 58.00, 1, NOW(), NOW()),
+('中国通史', '吕思勉', '中华书局', 7, '历史,中国史', '9787101125455', '2017', '中国历史发展脉络全面梳理。', '/covers/zhongguotongshi.jpg', 7, 49.80, 1, NOW(), NOW());
diff --git a/sql/module3_borrow_record.sql b/sql/module3_borrow_record.sql
new file mode 100644
index 0000000..aff55cf
--- /dev/null
+++ b/sql/module3_borrow_record.sql
@@ -0,0 +1,17 @@
+-- 借阅表
+CREATE TABLE `borrow_records` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `user_id` INT NOT NULL, -- 借阅人(用户id)
+ `book_id` INT NOT NULL, -- 图书id
+ `borrow_date` DATETIME NOT NULL, -- 借书时间
+ `due_date` DATETIME NOT NULL, -- 应还日期
+ `return_date` DATETIME DEFAULT NULL, -- 实际归还(未归还为空)
+ `renew_count` INT DEFAULT 0, -- 续借次数
+ `status` TINYINT DEFAULT 1, -- 1:借出 2:已归还 3:逾期未还
+ `remark` VARCHAR(255), -- 管理备注
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
+ FOREIGN KEY (`book_id`) REFERENCES `books`(`id`)
+);
+
diff --git a/sql/module4_inventory_logs.sql b/sql/module4_inventory_logs.sql
new file mode 100644
index 0000000..77cc9c7
--- /dev/null
+++ b/sql/module4_inventory_logs.sql
@@ -0,0 +1,13 @@
+-- 库存变动明细表
+CREATE TABLE `inventory_logs` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `book_id` INT NOT NULL, -- 图书id
+ `change_type` VARCHAR(32) NOT NULL, -- 变动类型
+ `change_amount` INT NOT NULL, -- 变动数量
+ `after_stock` INT NOT NULL, -- 变动后的库存
+ `operator_id` INT, -- 操作人id
+ `remark` VARCHAR(255), -- 备注
+ `changed_at` DATETIME NOT NULL, -- 变动时间
+ FOREIGN KEY (`book_id`) REFERENCES `books`(`id`),
+ FOREIGN KEY (`operator_id`) REFERENCES `users`(`id`)
+);
diff --git a/sql/module5_system_announcement.sql b/sql/module5_system_announcement.sql
new file mode 100644
index 0000000..ad320e4
--- /dev/null
+++ b/sql/module5_system_announcement.sql
@@ -0,0 +1,28 @@
+-- 系统公告表
+CREATE TABLE `announcements` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `title` VARCHAR(128) NOT NULL,
+ `content` TEXT NOT NULL,
+ `publisher_id` INT NOT NULL,
+ `is_top` TINYINT DEFAULT 0, -- 是否置顶
+ `status` TINYINT DEFAULT 1, -- 1有效 0撤回/禁用
+ `created_at` DATETIME NOT NULL,
+ `updated_at` DATETIME NOT NULL,
+ FOREIGN KEY (`publisher_id`) REFERENCES `users`(`id`)
+);
+
+-- 用户消息通知表
+CREATE TABLE `notifications` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `user_id` INT NOT NULL,
+ `title` VARCHAR(128) NOT NULL,
+ `content` TEXT NOT NULL,
+ `type` VARCHAR(32) NOT NULL, -- 消息类型
+ `status` TINYINT DEFAULT 0, -- 0未读 1已读
+ `sender_id` INT, -- 发送人(系统消息可为NULL或0)
+ `created_at` DATETIME NOT NULL,
+ `read_at` DATETIME DEFAULT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
+ FOREIGN KEY (`sender_id`) REFERENCES `users`(`id`)
+);
+
diff --git a/sql/module7_system_log.sql b/sql/module7_system_log.sql
new file mode 100644
index 0000000..2953834
--- /dev/null
+++ b/sql/module7_system_log.sql
@@ -0,0 +1,12 @@
+-- 日志管理表
+CREATE TABLE `logs` (
+ `id` INT AUTO_INCREMENT PRIMARY KEY,
+ `user_id` INT, -- 操作者id
+ `action` VARCHAR(64) NOT NULL, -- 操作名
+ `target_type` VARCHAR(32), -- 对象类型
+ `target_id` INT, -- 对象id
+ `ip_address` VARCHAR(45), -- 操作来源ip
+ `description` VARCHAR(255), -- 补充描述
+ `created_at` DATETIME NOT NULL,
+ FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)
+);
\ No newline at end of file