Book_system/code_collection.txt
2025-04-29 11:18:18 +08:00

2641 lines
78 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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