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