login_register_dashboard
This commit is contained in:
		
							parent
							
								
									bfe6ec8c6d
								
							
						
					
					
						commit
						568d46f013
					
				
							
								
								
									
										64
									
								
								all_file_output.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								all_file_output.py
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
							
								
								
									
										6
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								app.py
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
							
								
								
									
										85
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										0
									
								
								app/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/controllers/announcement.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/announcement.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/controllers/book.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/book.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/controllers/borrow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/borrow.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/controllers/inventory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/inventory.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/controllers/log.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/log.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/controllers/statistics.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/controllers/statistics.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										181
									
								
								app/controllers/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								app/controllers/user.py
									
									
									
									
									
										Normal file
									
								
							@ -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': '邮件发送失败,请稍后重试'})
 | 
			
		||||
							
								
								
									
										0
									
								
								app/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/models/announcement.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/announcement.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/models/book.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/book.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/models/borrow.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/borrow.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/models/inventory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/inventory.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/models/log.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/log.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/models/notification.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/models/notification.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										75
									
								
								app/models/user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								app/models/user.py
									
									
									
									
									
										Normal file
									
								
							@ -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')
 | 
			
		||||
							
								
								
									
										0
									
								
								app/services/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/services/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/services/book_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/services/book_service.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/services/borrow_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/services/borrow_service.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/services/inventory_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/services/inventory_service.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/services/user_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/services/user_service.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										651
									
								
								app/static/css/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										651
									
								
								app/static/css/index.css
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										469
									
								
								app/static/css/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										469
									
								
								app/static/css/main.css
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								app/static/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/static/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 939 KiB  | 
							
								
								
									
										340
									
								
								app/static/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								app/static/js/main.js
									
									
									
									
									
										Normal file
									
								
							@ -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 = `
 | 
			
		||||
            <div class="notification-icon">${icon}</div>
 | 
			
		||||
            <div class="notification-content">
 | 
			
		||||
                <h3>${title}</h3>
 | 
			
		||||
                <p>${message}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        document.body.appendChild(notification);
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            notification.classList.add('show');
 | 
			
		||||
        }, 10);
 | 
			
		||||
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
            notification.classList.remove('show');
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                document.body.removeChild(notification);
 | 
			
		||||
            }, 300);
 | 
			
		||||
        }, 3000);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										49
									
								
								app/templates/404.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								app/templates/404.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>页面未找到 - 图书管理系统</title>
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
 | 
			
		||||
    <style>
 | 
			
		||||
        .error-container {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            padding: 50px 20px;
 | 
			
		||||
        }
 | 
			
		||||
        .error-code {
 | 
			
		||||
            font-size: 100px;
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
            color: #4a89dc;
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
        }
 | 
			
		||||
        .error-message {
 | 
			
		||||
            font-size: 24px;
 | 
			
		||||
            color: #333;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
        }
 | 
			
		||||
        .back-button {
 | 
			
		||||
            display: inline-block;
 | 
			
		||||
            padding: 10px 20px;
 | 
			
		||||
            background-color: #4a89dc;
 | 
			
		||||
            color: white;
 | 
			
		||||
            text-decoration: none;
 | 
			
		||||
            border-radius: 5px;
 | 
			
		||||
            font-weight: 500;
 | 
			
		||||
        }
 | 
			
		||||
        .back-button:hover {
 | 
			
		||||
            background-color: #3b78c4;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="main-container">
 | 
			
		||||
        <div class="error-container">
 | 
			
		||||
            <div class="error-code">404</div>
 | 
			
		||||
            <div class="error-message">页面未找到</div>
 | 
			
		||||
            <p>抱歉,您访问的页面不存在或已被移除。</p>
 | 
			
		||||
            <p style="margin-bottom: 30px;">请检查URL是否正确,或返回首页。</p>
 | 
			
		||||
            <a href="{{ url_for('index') }}" class="back-button">返回首页</a>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										0
									
								
								app/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/templates/base.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										214
									
								
								app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								app/templates/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,214 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>首页 - 图书管理系统</title>
 | 
			
		||||
    <!-- 只引用index页面的专用样式 -->
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
 | 
			
		||||
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="app-container">
 | 
			
		||||
        <!-- 侧边导航栏 -->
 | 
			
		||||
        <nav class="sidebar">
 | 
			
		||||
            <div class="logo-container">
 | 
			
		||||
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo" class="logo">
 | 
			
		||||
                <h2>图书管理系统</h2>
 | 
			
		||||
            </div>
 | 
			
		||||
            <ul class="nav-links">
 | 
			
		||||
                <li class="active"><a href="#"><i class="fas fa-home"></i> 首页</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-book"></i> 图书浏览</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-bookmark"></i> 我的借阅</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-bell"></i> 通知公告</a></li>
 | 
			
		||||
                {% if current_user.role_id == 1 %}
 | 
			
		||||
                <li class="nav-category">管理功能</li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-users"></i> 用户管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-layer-group"></i> 图书管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-exchange-alt"></i> 借阅管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-warehouse"></i> 库存管理</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-chart-bar"></i> 统计分析</a></li>
 | 
			
		||||
                <li><a href="#"><i class="fas fa-history"></i> 日志管理</a></li>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </ul>
 | 
			
		||||
        </nav>
 | 
			
		||||
 | 
			
		||||
        <!-- 主内容区 -->
 | 
			
		||||
        <main class="main-content">
 | 
			
		||||
            <!-- 顶部导航 -->
 | 
			
		||||
            <header class="top-bar">
 | 
			
		||||
                <div class="search-container">
 | 
			
		||||
                    <i class="fas fa-search search-icon"></i>
 | 
			
		||||
                    <input type="text" placeholder="搜索图书..." class="search-input">
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="user-menu">
 | 
			
		||||
                    <div class="notifications">
 | 
			
		||||
                        <i class="fas fa-bell"></i>
 | 
			
		||||
                        <span class="badge">3</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="user-info">
 | 
			
		||||
                        <div class="user-avatar">
 | 
			
		||||
                            {{ current_user.username[0] }}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="user-details">
 | 
			
		||||
                            <span class="user-name">{{ current_user.username }}</span>
 | 
			
		||||
                            <span class="user-role">{{ '管理员' if current_user.role_id == 1 else '普通用户' }}</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="dropdown-menu">
 | 
			
		||||
                            <a href="#"><i class="fas fa-user-circle"></i> 个人中心</a>
 | 
			
		||||
                            <a href="#"><i class="fas fa-cog"></i> 设置</a>
 | 
			
		||||
                            <a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </header>
 | 
			
		||||
 | 
			
		||||
            <!-- 欢迎区域 -->
 | 
			
		||||
            <div class="welcome-section">
 | 
			
		||||
                <h1>欢迎回来,{{ current_user.username }}!</h1>
 | 
			
		||||
                <p>今天是 <span id="current-date"></span>,祝您使用愉快。</p>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 快速统计 -->
 | 
			
		||||
            <div class="stats-container">
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-book stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>馆藏总量</h3>
 | 
			
		||||
                        <p class="stat-number">8,567</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-users stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>注册用户</h3>
 | 
			
		||||
                        <p class="stat-number">1,245</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-exchange-alt stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>当前借阅</h3>
 | 
			
		||||
                        <p class="stat-number">352</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="stat-card">
 | 
			
		||||
                    <i class="fas fa-clock stat-icon"></i>
 | 
			
		||||
                    <div class="stat-info">
 | 
			
		||||
                        <h3>待还图书</h3>
 | 
			
		||||
                        <p class="stat-number">{{ 5 }}</p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 主要内容区 -->
 | 
			
		||||
            <div class="main-sections">
 | 
			
		||||
                <!-- 最新图书 -->
 | 
			
		||||
                <div class="content-section book-section">
 | 
			
		||||
                    <div class="section-header">
 | 
			
		||||
                        <h2>最新图书</h2>
 | 
			
		||||
                        <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="book-grid">
 | 
			
		||||
                        {% for i in range(4) %}
 | 
			
		||||
                        <div class="book-card">
 | 
			
		||||
                            <div class="book-cover">
 | 
			
		||||
                                <img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="Book Cover" onerror="this.src='https://via.placeholder.com/150x210?text=No+Cover'">
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <div class="book-info">
 | 
			
		||||
                                <h3 class="book-title">示例图书标题</h3>
 | 
			
		||||
                                <p class="book-author">作者名</p>
 | 
			
		||||
                                <div class="book-meta">
 | 
			
		||||
                                    <span class="book-category">计算机</span>
 | 
			
		||||
                                    <span class="book-status available">可借阅</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <button class="borrow-btn">借阅</button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <!-- 通知公告 -->
 | 
			
		||||
                <div class="content-section notice-section">
 | 
			
		||||
                    <div class="section-header">
 | 
			
		||||
                        <h2>通知公告</h2>
 | 
			
		||||
                        <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="notice-list">
 | 
			
		||||
                        <div class="notice-item">
 | 
			
		||||
                            <div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
 | 
			
		||||
                            <div class="notice-content">
 | 
			
		||||
                                <h3>关于五一假期图书馆开放时间调整的通知</h3>
 | 
			
		||||
                                <p>五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。</p>
 | 
			
		||||
                                <div class="notice-meta">
 | 
			
		||||
                                    <span class="notice-time">2023-04-28</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="notice-item">
 | 
			
		||||
                            <div class="notice-icon"><i class="fas fa-bell"></i></div>
 | 
			
		||||
                            <div class="notice-content">
 | 
			
		||||
                                <h3>您有2本图书即将到期</h3>
 | 
			
		||||
                                <p>《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。</p>
 | 
			
		||||
                                <div class="notice-meta">
 | 
			
		||||
                                    <span class="notice-time">2023-04-27</span>
 | 
			
		||||
                                    <button class="renew-btn">一键续借</button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 热门图书区域 -->
 | 
			
		||||
            <div class="content-section popular-section">
 | 
			
		||||
                <div class="section-header">
 | 
			
		||||
                    <h2>热门图书</h2>
 | 
			
		||||
                    <a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="popular-books">
 | 
			
		||||
                    {% for i in range(5) %}
 | 
			
		||||
                    <div class="popular-book-item">
 | 
			
		||||
                        <div class="rank-badge">{{ i+1 }}</div>
 | 
			
		||||
                        <div class="book-cover small">
 | 
			
		||||
                            <img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="book-details">
 | 
			
		||||
                            <h3 class="book-title">热门图书标题示例</h3>
 | 
			
		||||
                            <p class="book-author">知名作者</p>
 | 
			
		||||
                            <div class="book-stats">
 | 
			
		||||
                                <span><i class="fas fa-eye"></i> 1024 次浏览</span>
 | 
			
		||||
                                <span><i class="fas fa-bookmark"></i> 89 次借阅</span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {% endfor %}
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </main>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
            // 设置当前日期
 | 
			
		||||
            const now = new Date();
 | 
			
		||||
            const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' };
 | 
			
		||||
            document.getElementById('current-date').textContent = now.toLocaleDateString('zh-CN', options);
 | 
			
		||||
 | 
			
		||||
            // 用户菜单下拉
 | 
			
		||||
            const userInfo = document.querySelector('.user-info');
 | 
			
		||||
            userInfo.addEventListener('click', function(e) {
 | 
			
		||||
                userInfo.classList.toggle('active');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            // 点击其他区域关闭下拉菜单
 | 
			
		||||
            document.addEventListener('click', function(e) {
 | 
			
		||||
                if (!userInfo.contains(e.target)) {
 | 
			
		||||
                    userInfo.classList.remove('active');
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										92
									
								
								app/templates/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/templates/login.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,92 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>用户登录 - 图书管理系统</title>
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="overlay"></div>
 | 
			
		||||
 | 
			
		||||
    <div class="theme-toggle" id="theme-toggle">☀️</div>
 | 
			
		||||
 | 
			
		||||
    <div class="main-container">
 | 
			
		||||
        <div class="login-container">
 | 
			
		||||
            <div class="logo">
 | 
			
		||||
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
 | 
			
		||||
            </div>
 | 
			
		||||
            <h1>图书管理系统</h1>
 | 
			
		||||
            <p class="subtitle">欢迎回来,请登录您的账户</p>
 | 
			
		||||
 | 
			
		||||
            <div id="account-login">
 | 
			
		||||
                <form id="login-form" action="{{ url_for('user.login') }}" method="post">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="username">用户名/邮箱</label>
 | 
			
		||||
                        <div class="input-with-icon">
 | 
			
		||||
                            <span class="input-icon">👤</span>
 | 
			
		||||
                            <input type="text" id="username" name="username" class="form-control" placeholder="请输入账号">
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="username-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="password">密码</label>
 | 
			
		||||
                        <div class="input-with-icon">
 | 
			
		||||
                            <span class="input-icon">🔒</span>
 | 
			
		||||
                            <input type="password" id="password" name="password" class="form-control" placeholder="请输入密码">
 | 
			
		||||
                            <span class="password-toggle" id="password-toggle">👁️</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="password-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="remember-forgot">
 | 
			
		||||
                        <label class="custom-checkbox">
 | 
			
		||||
                            <input type="checkbox" name="remember_me">
 | 
			
		||||
                            <span class="checkmark"></span>
 | 
			
		||||
                            记住我 (7天内免登录)
 | 
			
		||||
                        </label>
 | 
			
		||||
                        <div class="forgot-password">
 | 
			
		||||
                            <a href="#">忘记密码?</a>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {% if error %}
 | 
			
		||||
                    <div class="alert alert-danger">{{ error }}</div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                    <button type="submit" class="btn-login" id="login-button">
 | 
			
		||||
                        <span>登录</span>
 | 
			
		||||
                        <span class="loading">⟳</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </form>
 | 
			
		||||
 | 
			
		||||
                <div class="signup">
 | 
			
		||||
                    还没有账号? <a href="{{ url_for('user.register') }}">立即注册</a>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="features">
 | 
			
		||||
                    <div class="feature-item">
 | 
			
		||||
                        <span class="feature-icon">🔒</span>
 | 
			
		||||
                        <span>安全登录</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="feature-item">
 | 
			
		||||
                        <span class="feature-icon">🔐</span>
 | 
			
		||||
                        <span>数据加密</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="feature-item">
 | 
			
		||||
                        <span class="feature-icon">📚</span>
 | 
			
		||||
                        <span>图书管理</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <footer>
 | 
			
		||||
        <p>© 2025 图书管理系统 - 版权所有</p>
 | 
			
		||||
    </footer>
 | 
			
		||||
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										93
									
								
								app/templates/register.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								app/templates/register.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,93 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="zh-CN">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>用户注册 - 图书管理系统</title>
 | 
			
		||||
    <link rel="stylesheet" href="{{ url_for('static', filename='css/main.css') }}">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="overlay"></div>
 | 
			
		||||
 | 
			
		||||
    <div class="theme-toggle" id="theme-toggle">☀️</div>
 | 
			
		||||
 | 
			
		||||
    <div class="main-container">
 | 
			
		||||
        <div class="login-container register-container">
 | 
			
		||||
            <div class="logo">
 | 
			
		||||
                <img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">
 | 
			
		||||
            </div>
 | 
			
		||||
            <h1>图书管理系统</h1>
 | 
			
		||||
            <p class="subtitle">创建您的新账户</p>
 | 
			
		||||
 | 
			
		||||
            <div id="register-form-container">
 | 
			
		||||
                <form id="register-form" action="{{ url_for('user.register') }}" method="post">
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="username">用户名</label>
 | 
			
		||||
                        <div class="input-with-icon">
 | 
			
		||||
                            <span class="input-icon">👤</span>
 | 
			
		||||
                            <input type="text" id="username" name="username" class="form-control" placeholder="请输入用户名" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="username-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="email">邮箱</label>
 | 
			
		||||
                        <div class="input-with-icon">
 | 
			
		||||
                            <span class="input-icon">📧</span>
 | 
			
		||||
                            <input type="email" id="email" name="email" class="form-control" placeholder="请输入邮箱" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="email-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="verification_code">邮箱验证码</label>
 | 
			
		||||
                        <div class="verification-code-container">
 | 
			
		||||
                            <input type="text" id="verification_code" name="verification_code" class="verification-input" placeholder="请输入验证码" required>
 | 
			
		||||
                            <button type="button" id="send-code-btn" class="send-code-btn">发送验证码</button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="verification-code-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="password">密码</label>
 | 
			
		||||
                        <div class="input-with-icon">
 | 
			
		||||
                            <span class="input-icon">🔒</span>
 | 
			
		||||
                            <input type="password" id="password" name="password" class="form-control" placeholder="请设置密码" required>
 | 
			
		||||
                            <span class="password-toggle" id="password-toggle">👁️</span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="password-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    <div class="form-group">
 | 
			
		||||
                        <label for="confirm_password">确认密码</label>
 | 
			
		||||
                        <div class="input-with-icon">
 | 
			
		||||
                            <span class="input-icon">🔒</span>
 | 
			
		||||
                            <input type="password" id="confirm_password" name="confirm_password" class="form-control" placeholder="请再次输入密码" required>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div class="validation-message" id="confirm-password-error"></div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {% if error %}
 | 
			
		||||
                    <div class="alert alert-danger">{{ error }}</div>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                    <button type="submit" class="btn-login" id="register-button">
 | 
			
		||||
                        <span>注册</span>
 | 
			
		||||
                        <span class="loading">⟳</span>
 | 
			
		||||
                    </button>
 | 
			
		||||
                </form>
 | 
			
		||||
 | 
			
		||||
                <div class="signup">
 | 
			
		||||
                    已有账号? <a href="{{ url_for('user.login') }}">返回登录</a>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <footer>
 | 
			
		||||
        <p>© 2025 图书管理系统 - 版权所有</p>
 | 
			
		||||
    </footer>
 | 
			
		||||
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										0
									
								
								app/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/utils/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/utils/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								app/utils/db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/utils/db.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										91
									
								
								app/utils/email.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								app/utils/email.py
									
									
									
									
									
										Normal file
									
								
							@ -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"""
 | 
			
		||||
        <html>
 | 
			
		||||
        <body>
 | 
			
		||||
            <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e1e1e1; border-radius: 5px;">
 | 
			
		||||
                <h2 style="color: #4a89dc;">图书管理系统 - 邮箱验证</h2>
 | 
			
		||||
                <p>您好,</p>
 | 
			
		||||
                <p>感谢您注册图书管理系统,您的验证码是:</p>
 | 
			
		||||
                <div style="background-color: #f5f5f5; padding: 10px; border-radius: 5px; text-align: center; font-size: 24px; letter-spacing: 5px; font-weight: bold; margin: 20px 0;">
 | 
			
		||||
                    {verification_code}
 | 
			
		||||
                </div>
 | 
			
		||||
                <p>该验证码将在10分钟内有效,请勿将验证码分享给他人。</p>
 | 
			
		||||
                <p>如果您没有请求此验证码,请忽略此邮件。</p>
 | 
			
		||||
                <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e1e1e1; font-size: 12px; color: #888;">
 | 
			
		||||
                    <p>此邮件为系统自动发送,请勿回复。</p>
 | 
			
		||||
                    <p>© 2025 图书管理系统</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </body>
 | 
			
		||||
        </html>
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        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))
 | 
			
		||||
							
								
								
									
										0
									
								
								app/utils/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								app/utils/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										2640
									
								
								code_collection.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2640
									
								
								code_collection.txt
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								config.py
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										16
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								main.py
									
									
									
									
									
										Normal file
									
								
							@ -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 帮助
 | 
			
		||||
							
								
								
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
			
		||||
							
								
								
									
										193
									
								
								sql/book_system.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								sql/book_system.sql
									
									
									
									
									
										Normal file
									
								
							@ -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;
 | 
			
		||||
							
								
								
									
										28
									
								
								sql/module1_user.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								sql/module1_user.sql
									
									
									
									
									
										Normal file
									
								
							@ -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', '普通用户');
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										47
									
								
								sql/module2_book_info.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								sql/module2_book_info.sql
									
									
									
									
									
										Normal file
									
								
							@ -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());
 | 
			
		||||
							
								
								
									
										17
									
								
								sql/module3_borrow_record.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								sql/module3_borrow_record.sql
									
									
									
									
									
										Normal file
									
								
							@ -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`)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										13
									
								
								sql/module4_inventory_logs.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								sql/module4_inventory_logs.sql
									
									
									
									
									
										Normal file
									
								
							@ -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`)
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										28
									
								
								sql/module5_system_announcement.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								sql/module5_system_announcement.sql
									
									
									
									
									
										Normal file
									
								
							@ -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`)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								sql/module7_system_log.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								sql/module7_system_log.sql
									
									
									
									
									
										Normal file
									
								
							@ -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`)
 | 
			
		||||
);
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user