Book_system/code_collection.txt

32225 lines
966 KiB
Plaintext
Raw Permalink Normal View History

2025-04-29 11:18:18 +08:00
================================================================================
File: ./config.py
================================================================================
import os
# 数据库配置
2025-05-14 15:08:06 +08:00
"""
2025-04-29 11:18:18 +08:00
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')
2025-05-14 15:08:06 +08:00
"""
2025-05-17 15:34:28 +08:00
DB_HOST = os.environ.get('DB_HOST', 'rm-bp1h5oqo8ld21viftro.mysql.rds.aliyuncs.com')
2025-05-14 15:08:06 +08:00
DB_PORT = os.environ.get('DB_PORT', '3306')
2025-05-17 15:34:28 +08:00
DB_USER = os.environ.get('DB_USER', 'shiqi')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'Shiqi1234!')
2025-05-14 15:08:06 +08:00
DB_NAME = os.environ.get('DB_NAME', 'book_system')
2025-04-29 11:18:18 +08:00
# 数据库连接字符串
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
================================================================================
2025-05-17 15:34:28 +08:00
from flask import Flask, render_template, session, g, Markup, redirect, url_for, request
2025-05-01 04:52:53 +08:00
from flask_login import LoginManager
2025-05-14 15:08:06 +08:00
from app.models.database import db
from app.models.user import User
2025-04-29 11:18:18 +08:00
from app.controllers.user import user_bp
2025-05-01 04:52:53 +08:00
from app.controllers.book import book_bp
from app.controllers.borrow import borrow_bp
2025-05-06 12:01:11 +08:00
from app.controllers.inventory import inventory_bp
from flask_login import LoginManager, current_user
2025-05-12 02:42:27 +08:00
from app.controllers.statistics import statistics_bp
2025-05-14 00:14:34 +08:00
from app.controllers.announcement import announcement_bp
from app.models.notification import Notification
2025-05-12 02:42:27 +08:00
from app.controllers.log import log_bp
2025-04-29 11:18:18 +08:00
import os
2025-05-14 00:14:34 +08:00
from datetime import datetime
2025-05-01 04:52:53 +08:00
login_manager = LoginManager()
2025-04-29 11:18:18 +08:00
2025-05-01 04:52:53 +08:00
def create_app(config=None):
2025-04-29 11:18:18 +08:00
app = Flask(__name__)
2025-05-14 15:08:06 +08:00
# 加载默认配置
app.config.from_object('config')
# 如果提供了配置对象,则加载它
if config:
if isinstance(config, dict):
app.config.update(config)
else:
app.config.from_object(config)
2025-04-29 11:18:18 +08:00
2025-05-14 15:08:06 +08:00
# 从环境变量指定的文件加载配置(如果有)
app.config.from_envvar('APP_CONFIG_FILE', silent=True)
2025-04-29 11:18:18 +08:00
# 初始化数据库
db.init_app(app)
2025-05-01 04:52:53 +08:00
# 初始化 Flask-Login
login_manager.init_app(app)
login_manager.login_view = 'user.login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
2025-05-17 15:34:28 +08:00
from app.utils.template_helpers import register_template_helpers
2025-04-29 11:18:18 +08:00
# 注册蓝图
2025-05-17 15:34:28 +08:00
register_template_helpers(app)
2025-04-29 11:18:18 +08:00
app.register_blueprint(user_bp, url_prefix='/user')
2025-05-01 04:52:53 +08:00
app.register_blueprint(book_bp, url_prefix='/book')
app.register_blueprint(borrow_bp, url_prefix='/borrow')
2025-05-12 02:42:27 +08:00
app.register_blueprint(statistics_bp)
2025-05-06 12:01:11 +08:00
app.register_blueprint(inventory_bp)
2025-05-12 02:42:27 +08:00
app.register_blueprint(log_bp)
2025-05-14 00:14:34 +08:00
app.register_blueprint(announcement_bp, url_prefix='/announcement')
2025-04-29 11:18:18 +08:00
# 创建数据库表
with app.app_context():
2025-04-30 16:23:05 +08:00
# 先导入基础模型
from app.models.user import User, Role
from app.models.book import Book, Category
# 创建表
2025-04-29 11:18:18 +08:00
db.create_all()
2025-05-01 04:52:53 +08:00
# 再导入依赖模型 - 但不在这里定义关系
2025-04-30 16:23:05 +08:00
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
2025-05-12 02:42:27 +08:00
from app.models.log import Log
2025-04-30 16:23:05 +08:00
2025-05-01 04:52:53 +08:00
# 移除这些重复的关系定义
# Book.borrow_records = db.relationship('BorrowRecord', backref='book', lazy='dynamic')
# Book.inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic')
# Category.books = db.relationship('Book', backref='category', lazy='dynamic')
2025-04-30 16:23:05 +08:00
2025-04-29 11:18:18 +08:00
# 创建默认角色
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)
2025-04-30 16:23:05 +08:00
# 创建基础分类
from app.models.book import Category
if not Category.query.first():
categories = [
Category(name='文学', sort=1),
Category(name='计算机', sort=2),
Category(name='历史', sort=3),
Category(name='科学', sort=4),
Category(name='艺术', sort=5),
Category(name='经济', sort=6),
Category(name='哲学', sort=7),
Category(name='教育', sort=8)
]
db.session.add_all(categories)
2025-04-29 11:18:18 +08:00
db.session.commit()
2025-05-17 15:34:28 +08:00
# 添加缓存控制中间件
@app.after_request
def add_cache_headers(response):
# 为HTML页面和主页添加禁止缓存的头
if request.path == '/' or 'text/html' in response.headers.get('Content-Type', ''):
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
response.headers['Vary'] = 'Cookie, Authorization'
return response
2025-05-01 04:52:53 +08:00
# 其余代码保持不变...
2025-04-29 11:18:18 +08:00
@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():
2025-05-14 00:14:34 +08:00
from app.models.book import Book
from app.models.user import User
from app.models.borrow import BorrowRecord
from app.models.announcement import Announcement
from app.models.notification import Notification
from sqlalchemy import func, desc
from flask_login import current_user
# 获取统计数据
stats = {
'total_books': Book.query.count(),
'total_users': User.query.count(),
'active_borrows': BorrowRecord.query.filter(BorrowRecord.return_date.is_(None)).count(),
'user_borrows': 0
}
# 如果用户已登录,获取其待还图书数量
if current_user.is_authenticated:
stats['user_borrows'] = BorrowRecord.query.filter(
BorrowRecord.user_id == current_user.id,
BorrowRecord.return_date.is_(None)
).count()
# 获取最新图书
latest_books = Book.query.filter_by(status=1).order_by(Book.created_at.desc()).limit(4).all()
# 获取热门图书(根据借阅次数)
try:
# 这里假设你的数据库中有表记录借阅次数
popular_books_query = db.session.query(
Book, func.count(BorrowRecord.id).label('borrow_count')
).join(
BorrowRecord, Book.id == BorrowRecord.book_id, isouter=True
).filter(
Book.status == 1
).group_by(
Book.id
).order_by(
desc('borrow_count')
).limit(5)
# 提取图书对象并添加借阅计数
popular_books = []
for book, count in popular_books_query:
book.borrow_count = count
popular_books.append(book)
except Exception as e:
# 如果查询有问题,使用最新的书作为备选
popular_books = latest_books.copy() if latest_books else []
print(f"获取热门图书失败: {str(e)}")
# 获取最新公告
announcements = Announcement.query.filter_by(status=1).order_by(
Announcement.is_top.desc(),
Announcement.created_at.desc()
).limit(3).all()
now = datetime.now()
# 获取用户的未读通知
user_notifications = []
if current_user.is_authenticated:
user_notifications = Notification.query.filter_by(
user_id=current_user.id,
status=0
).order_by(
Notification.created_at.desc()
).limit(5).all()
return render_template('index.html',
stats=stats,
latest_books=latest_books,
popular_books=popular_books,
announcements=announcements,
user_notifications=user_notifications,
now=now
)
2025-04-29 11:18:18 +08:00
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
2025-04-30 16:23:05 +08:00
@app.template_filter('nl2br')
def nl2br_filter(s):
2025-05-01 04:52:53 +08:00
if s:
return Markup(s.replace('\n', '<br>'))
return s
2025-04-30 16:23:05 +08:00
2025-05-14 00:14:34 +08:00
@app.context_processor
def utility_processor():
def get_unread_notifications_count(user_id):
if user_id:
return Notification.get_unread_count(user_id)
return 0
def get_recent_notifications(user_id, limit=5):
if user_id:
# 按时间倒序获取最近的几条通知
notifications = Notification.query.filter_by(user_id=user_id) \
.order_by(Notification.created_at.desc()) \
.limit(limit) \
.all()
return notifications
return []
return dict(
get_unread_notifications_count=get_unread_notifications_count,
get_recent_notifications=get_recent_notifications
)
2025-05-06 12:01:11 +08:00
@app.context_processor
def inject_now():
2025-05-17 15:34:28 +08:00
return {'now': datetime.now()}
2025-05-14 00:14:34 +08:00
2025-05-17 15:34:28 +08:00
return app
2025-05-14 00:14:34 +08:00
2025-05-14 15:08:06 +08:00
================================================================================
File: ./app/init_permissions.py
================================================================================
from app import create_app
from app.models.database import db
from app.models.user import Role
from app.models.permission import Permission
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def init_permissions():
"""初始化系统权限"""
logger.info("开始初始化系统权限...")
# 只定义管理类权限,对应现有的 @admin_required 装饰的路由
permissions = [
# 公告管理权限
{'code': 'manage_announcements', 'name': '公告管理', 'description': '允许管理系统公告(发布、编辑、删除、置顶等)'},
# 图书管理权限
{'code': 'manage_books', 'name': '图书管理', 'description': '允许管理图书(添加、编辑、删除图书)'},
{'code': 'manage_categories', 'name': '分类管理', 'description': '允许管理图书分类'},
{'code': 'import_export_books', 'name': '导入导出图书', 'description': '允许批量导入和导出图书数据'},
# 借阅管理权限
{'code': 'manage_borrows', 'name': '借阅管理', 'description': '允许管理全系统借阅记录和处理借还书操作'},
{'code': 'manage_overdue', 'name': '逾期管理', 'description': '允许查看和处理逾期借阅'},
# 库存管理权限
{'code': 'manage_inventory', 'name': '库存管理', 'description': '允许查看和调整图书库存'},
# 日志权限
{'code': 'view_logs', 'name': '查看日志', 'description': '允许查看系统操作日志'},
# 统计权限
{'code': 'view_statistics', 'name': '查看统计', 'description': '允许查看统计分析数据'},
# 用户管理权限
{'code': 'manage_users', 'name': '用户管理', 'description': '允许管理用户(添加、编辑、禁用、删除用户)'},
{'code': 'manage_roles', 'name': '角色管理', 'description': '允许管理角色和权限'},
]
# 添加权限记录
added_count = 0
updated_count = 0
for perm_data in permissions:
# 检查权限是否已存在
existing_perm = Permission.query.filter_by(code=perm_data['code']).first()
if existing_perm:
# 更新现有权限信息
existing_perm.name = perm_data['name']
existing_perm.description = perm_data['description']
updated_count += 1
else:
# 创建新权限
permission = Permission(**perm_data)
db.session.add(permission)
added_count += 1
# 提交所有权限
db.session.commit()
logger.info(f"权限初始化完成: 新增 {added_count} 个, 更新 {updated_count} 个")
# 处理角色权限分配
assign_role_permissions()
def assign_role_permissions():
"""为系统默认角色分配权限"""
logger.info("开始分配角色权限...")
# 获取所有权限
all_permissions = Permission.query.all()
# 获取系统内置角色
admin_role = Role.query.get(1) # 管理员角色
user_role = Role.query.get(2) # 普通用户角色
if admin_role and user_role:
# 管理员拥有所有权限
admin_role.permissions = all_permissions
# 普通用户无需特殊管理权限
user_role.permissions = []
db.session.commit()
logger.info(f"管理员角色分配了 {len(all_permissions)} 个权限")
logger.info(f"普通用户角色无管理权限")
else:
logger.error("无法找到内置角色,请确保角色表已正确初始化")
def main():
"""主函数"""
app = create_app()
with app.app_context():
init_permissions()
if __name__ == "__main__":
main()
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/utils/auth.py
================================================================================
2025-04-30 16:23:05 +08:00
from functools import wraps
2025-05-14 00:14:34 +08:00
from flask import redirect, url_for, flash, request
from flask_login import current_user
2025-04-30 16:23:05 +08:00
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
2025-05-14 00:14:34 +08:00
print(f"DEBUG: login_required 检查 - current_user.is_authenticated = {current_user.is_authenticated}")
if not current_user.is_authenticated:
2025-04-30 16:23:05 +08:00
flash('请先登录', 'warning')
return redirect(url_for('user.login', next=request.url))
return f(*args, **kwargs)
2025-05-14 00:14:34 +08:00
2025-04-30 16:23:05 +08:00
return decorated_function
2025-05-14 00:14:34 +08:00
2025-04-30 16:23:05 +08:00
def admin_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
2025-05-14 00:14:34 +08:00
print(f"DEBUG: admin_required 检查 - current_user.is_authenticated = {current_user.is_authenticated}")
if not current_user.is_authenticated:
2025-04-30 16:23:05 +08:00
flash('请先登录', 'warning')
return redirect(url_for('user.login', next=request.url))
2025-05-14 00:14:34 +08:00
print(f"DEBUG: admin_required 检查 - current_user.role_id = {getattr(current_user, 'role_id', None)}")
if getattr(current_user, 'role_id', None) != 1: # 安全地获取role_id属性
2025-04-30 16:23:05 +08:00
flash('权限不足', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
2025-05-14 00:14:34 +08:00
2025-04-30 16:23:05 +08:00
return decorated_function
2025-04-29 11:18:18 +08:00
2025-05-14 15:08:06 +08:00
def permission_required(permission_code):
"""
检查用户是否拥有特定权限的装饰器
:param permission_code: 权限代码,例如 'manage_books'
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
print(
f"DEBUG: permission_required({permission_code}) 检查 - current_user.is_authenticated = {current_user.is_authenticated}")
# 检查用户是否登录
if not current_user.is_authenticated:
flash('请先登录', 'warning')
return redirect(url_for('user.login', next=request.url))
# 管理员拥有所有权限
if getattr(current_user, 'role_id', None) == 1:
return f(*args, **kwargs)
# 获取用户角色并检查是否有指定权限
from app.models.user import Role
role = Role.query.get(current_user.role_id)
if not role:
flash('用户角色异常', 'danger')
return redirect(url_for('index'))
# 检查角色是否有指定权限
has_permission = False
for perm in role.permissions:
if perm.code == permission_code:
has_permission = True
break
if not has_permission:
print(f"DEBUG: 用户 {current_user.username} 缺少权限 {permission_code}")
flash('您没有执行此操作的权限', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
return decorator
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/utils/db.py
================================================================================
================================================================================
File: ./app/utils/__init__.py
================================================================================
2025-05-12 02:42:27 +08:00
================================================================================
File: ./app/utils/logger.py
================================================================================
from flask import request, current_app
from flask_login import current_user
from app.models.log import Log
def record_activity(action, target_type=None, target_id=None, description=None):
"""
记录用户活动
参数:
- action: 操作类型,如 'login', 'logout', 'create', 'update', 'delete', 'borrow', 'return' 等
- target_type: 操作对象类型,如 'book', 'user', 'borrow' 等
- target_id: 操作对象ID
- description: 操作详细描述
"""
try:
# 获取当前用户ID
user_id = current_user.id if current_user.is_authenticated else None
# 获取客户端IP地址
ip_address = request.remote_addr
if 'X-Forwarded-For' in request.headers:
ip_address = request.headers.getlist("X-Forwarded-For")[0].rpartition(' ')[-1]
# 记录日志
Log.add_log(
action=action,
user_id=user_id,
target_type=target_type,
target_id=target_id,
ip_address=ip_address,
description=description
)
return True
except Exception as e:
# 记录错误,但不影响主要功能
if current_app:
current_app.logger.error(f"Error recording activity log: {str(e)}")
return False
2025-05-17 15:34:28 +08:00
================================================================================
File: ./app/utils/template_helpers.py
================================================================================
from app.models.permission import Permission
from flask import current_app
def register_template_helpers(app):
@app.context_processor
def inject_permissions():
def has_permission(user, permission_code):
"""检查用户是否拥有指定权限"""
if not user or not user.is_authenticated:
return False
# 管理员拥有所有权限
if user.role_id == 1:
return True
# 检查用户角色权限
if user.role:
for perm in user.role.permissions:
if perm.code == permission_code:
return True
return False
return dict(has_permission=has_permission)
# 在 create_app 函数中调用
# register_template_helpers(app)
2025-04-29 11:18:18 +08:00
================================================================================
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
================================================================================
2025-05-14 15:08:06 +08:00
from app.models.database import db
2025-04-29 11:18:18 +08:00
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
2025-05-01 04:52:53 +08:00
from flask_login import UserMixin
2025-05-14 15:08:06 +08:00
from app.models.permission import RolePermission, Permission
2025-04-29 11:18:18 +08:00
2025-05-14 15:08:06 +08:00
#db = SQLAlchemy()
2025-04-29 11:18:18 +08:00
2025-05-01 04:52:53 +08:00
class User(db.Model, UserMixin):
2025-04-29 11:18:18 +08:00
__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)
2025-05-06 12:01:11 +08:00
def __init__(self, username, password, email=None, phone=None, nickname=None, role_id=2, status=1):
2025-04-29 11:18:18 +08:00
self.username = username
self.set_password(password)
self.email = email
self.phone = phone
self.nickname = nickname
self.role_id = role_id
2025-05-06 12:01:11 +08:00
self.status = status # 新增
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
@property
2025-05-01 04:52:53 +08:00
def is_active(self):
return self.status == 1
2025-04-29 11:18:18 +08:00
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))
2025-05-14 15:08:06 +08:00
permissions = db.relationship(
'Permission',
secondary='role_permissions',
backref=db.backref('roles', lazy='dynamic'),
lazy='dynamic'
)
2025-04-29 11:18:18 +08:00
users = db.relationship('User', backref='role')
2025-05-14 15:08:06 +08:00
================================================================================
File: ./app/models/permission.py
================================================================================
from app.models.database import db
from datetime import datetime
# 这是权限表 model
class Permission(db.Model):
__tablename__ = 'permissions'
id = db.Column(db.Integer, primary_key=True)
code = db.Column(db.String(64), unique=True, nullable=False, comment='权限代码,用于系统识别')
name = db.Column(db.String(64), nullable=False, comment='权限名称,用于界面显示')
description = db.Column(db.String(255), comment='权限描述,说明权限用途')
# 角色-权限 关联表辅助对象模式方便ORM关系管理
class RolePermission(db.Model):
__tablename__ = 'role_permissions'
role_id = db.Column(db.Integer, db.ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True, comment='角色ID关联roles表')
permission_id = db.Column(db.Integer, db.ForeignKey('permissions.id', ondelete='CASCADE'), primary_key=True, comment='权限ID关联permissions表')
created_at = db.Column(db.DateTime, default=datetime.now, comment='权限分配时间')
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/log.py
================================================================================
2025-05-12 02:42:27 +08:00
from datetime import datetime
from app.models.user import db, User # 从user模块导入db而不是从utils导入
class Log(db.Model):
__tablename__ = 'logs'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
action = db.Column(db.String(64), nullable=False)
target_type = db.Column(db.String(32), nullable=True)
target_id = db.Column(db.Integer, nullable=True)
ip_address = db.Column(db.String(45), nullable=True)
description = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# 关联用户
user = db.relationship('User', backref=db.backref('logs', lazy=True))
def __init__(self, action, user_id=None, target_type=None, target_id=None,
ip_address=None, description=None):
self.user_id = user_id
self.action = action
self.target_type = target_type
self.target_id = target_id
self.ip_address = ip_address
self.description = description
self.created_at = datetime.now()
@staticmethod
def add_log(action, user_id=None, target_type=None, target_id=None,
ip_address=None, description=None):
"""添加一条日志记录"""
try:
log = Log(
action=action,
user_id=user_id,
target_type=target_type,
target_id=target_id,
ip_address=ip_address,
description=description
)
db.session.add(log)
db.session.commit()
return True, "日志记录成功"
except Exception as e:
db.session.rollback()
return False, f"日志记录失败: {str(e)}"
@staticmethod
def get_logs(page=1, per_page=20, user_id=None, action=None,
target_type=None, start_date=None, end_date=None):
"""查询日志记录"""
query = Log.query.order_by(Log.created_at.desc())
if user_id:
query = query.filter(Log.user_id == user_id)
if action:
query = query.filter(Log.action == action)
if target_type:
query = query.filter(Log.target_type == target_type)
if start_date:
query = query.filter(Log.created_at >= start_date)
if end_date:
query = query.filter(Log.created_at <= end_date)
return query.paginate(page=page, per_page=per_page)
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/notification.py
================================================================================
2025-05-14 00:14:34 +08:00
from datetime import datetime
from app.models.user import db, User # 从user模块导入db而不是从app.models导入
class Notification(db.Model):
__tablename__ = 'notifications'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
title = db.Column(db.String(128), nullable=False)
content = db.Column(db.Text, nullable=False)
type = db.Column(db.String(32), nullable=False) # 通知类型system, borrow, return, overdue, etc.
status = db.Column(db.Integer, default=0) # 0-未读, 1-已读
sender_id = db.Column(db.Integer, db.ForeignKey('users.id'))
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
read_at = db.Column(db.DateTime)
# 关联关系
user = db.relationship('User', foreign_keys=[user_id], backref='notifications')
sender = db.relationship('User', foreign_keys=[sender_id], backref='sent_notifications')
def to_dict(self):
"""将通知转换为字典"""
return {
'id': self.id,
'user_id': self.user_id,
'title': self.title,
'content': self.content,
'type': self.type,
'status': self.status,
'sender_id': self.sender_id,
'sender_name': self.sender.username if self.sender else 'System',
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'read_at': self.read_at.strftime('%Y-%m-%d %H:%M:%S') if self.read_at else None
}
@staticmethod
def get_user_notifications(user_id, page=1, per_page=10, unread_only=False):
"""获取用户通知"""
query = Notification.query.filter_by(user_id=user_id)
if unread_only:
query = query.filter_by(status=0)
return query.order_by(Notification.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
@staticmethod
def get_unread_count(user_id):
"""获取用户未读通知数量"""
return Notification.query.filter_by(user_id=user_id, status=0).count()
@staticmethod
def mark_as_read(notification_id, user_id=None):
"""将通知标记为已读"""
notification = Notification.query.get(notification_id)
if not notification:
return False, "通知不存在"
# 验证用户权限
if user_id and notification.user_id != user_id:
return False, "无权操作此通知"
notification.status = 1
notification.read_at = datetime.now()
try:
db.session.commit()
return True, "已标记为已读"
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def create_notification(user_id, title, content, notification_type, sender_id=None):
"""创建新通知"""
notification = Notification(
user_id=user_id,
title=title,
content=content,
type=notification_type,
sender_id=sender_id
)
try:
db.session.add(notification)
db.session.commit()
return True, notification
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def create_system_notification(user_ids, title, content, notification_type, sender_id=None):
"""创建系统通知,发送给多个用户"""
success_count = 0
fail_count = 0
for user_id in user_ids:
success, _ = Notification.create_notification(
user_id=user_id,
title=title,
content=content,
notification_type=notification_type,
sender_id=sender_id
)
if success:
success_count += 1
else:
fail_count += 1
return success_count, fail_count
2025-04-29 11:18:18 +08:00
2025-05-14 15:08:06 +08:00
================================================================================
File: ./app/models/database.py
================================================================================
from flask_sqlalchemy import SQLAlchemy
# 创建共享的SQLAlchemy实例
db = SQLAlchemy()
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/__init__.py
================================================================================
2025-04-30 16:23:05 +08:00
def create_app():
app = Flask(__name__)
# ... 配置代码 ...
# 初始化数据库
db.init_app(app)
# 导入模型,确保所有模型在创建表之前被加载
from app.models.user import User, Role
from app.models.book import Book, Category
from app.models.borrow import BorrowRecord
from app.models.inventory import InventoryLog
# 创建数据库表
with app.app_context():
db.create_all()
# ... 其余代码 ...
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/book.py
================================================================================
2025-04-30 16:23:05 +08:00
from app.models.user import db
from datetime import datetime
class Category(db.Model):
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
sort = db.Column(db.Integer, default=0)
# 关系 - 只保留与自身的关系
parent = db.relationship('Category', remote_side=[id], backref='children')
def __repr__(self):
return f'<Category {self.name}>'
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=False)
author = db.Column(db.String(128), nullable=False)
publisher = db.Column(db.String(128), nullable=True)
category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=True)
tags = db.Column(db.String(255), nullable=True)
isbn = db.Column(db.String(32), unique=True, nullable=True)
publish_year = db.Column(db.String(16), nullable=True)
description = db.Column(db.Text, nullable=True)
cover_url = db.Column(db.String(255), nullable=True)
stock = db.Column(db.Integer, default=0)
price = db.Column(db.Numeric(10, 2), nullable=True)
status = db.Column(db.Integer, default=1) # 1:可用, 0:不可用
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
2025-05-06 12:01:11 +08:00
# 添加与 InventoryLog 的关系
inventory_logs = db.relationship('InventoryLog', backref='book', lazy='dynamic')
2025-04-30 16:23:05 +08:00
def __repr__(self):
return f'<Book {self.title}>'
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/borrow.py
================================================================================
2025-04-30 16:23:05 +08:00
from app.models.user import db
from datetime import datetime
class BorrowRecord(db.Model):
__tablename__ = 'borrow_records'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False)
borrow_date = db.Column(db.DateTime, nullable=False, default=datetime.now)
due_date = db.Column(db.DateTime, nullable=False)
return_date = db.Column(db.DateTime, nullable=True)
renew_count = db.Column(db.Integer, default=0)
status = db.Column(db.Integer, default=1) # 1: 借出, 0: 已归还
remark = db.Column(db.String(255), nullable=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# 添加反向关系引用
user = db.relationship('User', backref=db.backref('borrow_records', lazy='dynamic'))
2025-05-01 04:52:53 +08:00
book = db.relationship('Book', backref=db.backref('borrow_records', lazy='dynamic'))
2025-04-30 16:23:05 +08:00
# book 关系会在后面步骤添加
def __repr__(self):
return f'<BorrowRecord {self.id}>'
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/announcement.py
================================================================================
2025-05-14 00:14:34 +08:00
from datetime import datetime
from app.models.user import db, User # 从user模块导入db而不是从app.models导入
class Announcement(db.Model):
__tablename__ = 'announcements'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128), nullable=False)
content = db.Column(db.Text, nullable=False)
publisher_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
is_top = db.Column(db.Boolean, default=False)
status = db.Column(db.Integer, default=1) # 1-正常, 0-已下架
created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
# 关联关系
publisher = db.relationship('User', backref='announcements')
def to_dict(self):
"""将公告转换为字典"""
return {
'id': self.id,
'title': self.title,
'content': self.content,
'publisher_id': self.publisher_id,
'publisher_name': self.publisher.username if self.publisher else '',
'is_top': self.is_top,
'status': self.status,
'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S')
}
@staticmethod
def get_active_announcements(limit=None):
"""获取活跃的公告"""
query = Announcement.query.filter_by(status=1).order_by(
Announcement.is_top.desc(),
Announcement.created_at.desc()
)
if limit:
query = query.limit(limit)
return query.all()
@staticmethod
def get_announcement_by_id(announcement_id):
"""根据ID获取公告"""
return Announcement.query.get(announcement_id)
@staticmethod
def create_announcement(title, content, publisher_id, is_top=False):
"""创建新公告"""
announcement = Announcement(
title=title,
content=content,
publisher_id=publisher_id,
is_top=is_top
)
try:
db.session.add(announcement)
db.session.commit()
return True, announcement
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def update_announcement(announcement_id, title, content, is_top=None):
"""更新公告内容"""
announcement = Announcement.query.get(announcement_id)
if not announcement:
return False, "公告不存在"
announcement.title = title
announcement.content = content
if is_top is not None:
announcement.is_top = is_top
try:
db.session.commit()
return True, announcement
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def change_status(announcement_id, status):
"""更改公告状态"""
announcement = Announcement.query.get(announcement_id)
if not announcement:
return False, "公告不存在"
announcement.status = status
try:
db.session.commit()
return True, "状态已更新"
except Exception as e:
db.session.rollback()
return False, str(e)
@staticmethod
def change_top_status(announcement_id, is_top):
"""更改置顶状态"""
announcement = Announcement.query.get(announcement_id)
if not announcement:
return False, "公告不存在"
announcement.is_top = is_top
try:
db.session.commit()
return True, "置顶状态已更新"
except Exception as e:
db.session.rollback()
return False, str(e)
2025-04-29 11:18:18 +08:00
================================================================================
File: ./app/models/inventory.py
================================================================================
2025-04-30 16:23:05 +08:00
from app.models.user import db
from datetime import datetime
class InventoryLog(db.Model):
__tablename__ = 'inventory_logs'
id = db.Column(db.Integer, primary_key=True)
book_id = db.Column(db.Integer, db.ForeignKey('books.id'), nullable=False)
change_type = db.Column(db.String(32), nullable=False) # 'in' 入库, 'out' 出库
change_amount = db.Column(db.Integer, nullable=False)
after_stock = db.Column(db.Integer, nullable=False)
operator_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
remark = db.Column(db.String(255), nullable=True)
changed_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# 添加反向关系引用
operator = db.relationship('User', backref=db.backref('inventory_logs', lazy='dynamic'))
# book 关系会在后面步骤添加
def __repr__(self):
return f'<InventoryLog {self.id}>'
2025-04-29 11:18:18 +08:00
2025-05-12 02:42:27 +08:00
================================================================================
File: ./app/static/css/log-detail.css
================================================================================
/* 日志详情样式 */
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.content-header h1 {
margin: 0;
font-size: 24px;
}
.log-info {
padding: 10px;
}
.info-item {
margin-bottom: 15px;
display: flex;
}
.info-item .label {
width: 100px;
font-weight: 600;
color: #495057;
}
.info-item .value {
flex: 1;
}
.description {
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
display: block;
}
.description .label {
display: block;
width: 100%;
margin-bottom: 10px;
}
.description .value {
display: block;
width: 100%;
white-space: pre-wrap;
word-break: break-word;
}
2025-04-30 23:28:51 +08:00
================================================================================
File: ./app/static/css/register.css
================================================================================
/* register.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;
}
.register-container {
width: 500px;
}
@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); }
}
.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;
}
.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;
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;
}
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;
}
@media (max-width: 576px) {
.login-container, .register-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;
}
}
2025-05-01 04:52:53 +08:00
================================================================================
2025-05-06 12:01:11 +08:00
File: ./app/static/css/inventory-book-logs.css
2025-05-01 04:52:53 +08:00
================================================================================
2025-05-06 12:01:11 +08:00
/* 冰雪奇缘主题库存日志页面样式 */
/* 基础背景与字体 */
body {
font-family: 'Arial Rounded MT Bold', 'Helvetica Neue', Arial, sans-serif;
background-color: #e6f2ff;
color: #2c3e50;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 冰雪背景 */
.frozen-background {
position: relative;
min-height: 100vh;
padding: 30px 0 50px;
background: linear-gradient(135deg, #e4f1fe, #d4e6fb, #c9e0ff);
overflow: hidden;
}
/* 雪花效果 */
.snowflakes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.snowflake {
position: absolute;
color: #fff;
font-size: 1.5em;
opacity: 0.8;
top: -20px;
animation: snowfall linear infinite;
}
.snowflake:nth-child(1) { left: 10%; animation-duration: 15s; animation-delay: 0s; }
.snowflake:nth-child(2) { left: 20%; animation-duration: 12s; animation-delay: 1s; }
.snowflake:nth-child(3) { left: 30%; animation-duration: 13s; animation-delay: 2s; }
.snowflake:nth-child(4) { left: 40%; animation-duration: 10s; animation-delay: 0s; }
.snowflake:nth-child(5) { left: 50%; animation-duration: 16s; animation-delay: 3s; }
.snowflake:nth-child(6) { left: 60%; animation-duration: 14s; animation-delay: 1s; }
.snowflake:nth-child(7) { left: 70%; animation-duration: 12s; animation-delay: 0s; }
.snowflake:nth-child(8) { left: 80%; animation-duration: 15s; animation-delay: 2s; }
.snowflake:nth-child(9) { left: 90%; animation-duration: 13s; animation-delay: 1s; }
.snowflake:nth-child(10) { left: 95%; animation-duration: 14s; animation-delay: 3s; }
@keyframes snowfall {
0% {
transform: translateY(0) rotate(0deg);
}
100% {
transform: translateY(100vh) rotate(360deg);
}
}
/* 冰雪主题卡片 */
.frozen-card {
position: relative;
background-color: rgba(255, 255, 255, 0.85);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(79, 149, 255, 0.2);
backdrop-filter: blur(10px);
border: 2px solid #e1f0ff;
margin-bottom: 40px;
overflow: hidden;
z-index: 2;
}
/* 城堡装饰 */
.castle-decoration {
position: absolute;
top: -40px;
right: 30px;
width: 120px;
height: 120px;
background-image: url('https://i.imgur.com/KkMfwWv.png');
background-size: contain;
background-repeat: no-repeat;
opacity: 0.6;
z-index: 1;
transform: rotate(10deg);
filter: hue-rotate(190deg);
}
/* 卡片标题栏 */
.card-header-frozen {
background: linear-gradient(45deg, #7AB6FF, #94C5FF);
color: #fff;
padding: 1.5rem;
border-radius: 18px 18px 0 0;
text-align: center;
position: relative;
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
2025-05-01 04:52:53 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
justify-content: center;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.card-header-frozen h4 {
font-weight: 700;
2025-05-01 04:52:53 +08:00
margin: 0;
2025-05-06 12:01:11 +08:00
font-size: 1.6rem;
z-index: 1;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.card-header-frozen i {
margin-right: 10px;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 冰晶装饰 */
.ice-crystal {
position: absolute;
width: 50px;
height: 50px;
background-image: url('https://i.imgur.com/8vZuwlG.png');
background-size: contain;
background-repeat: no-repeat;
filter: brightness(1.2) hue-rotate(190deg);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.ice-crystal.left {
left: 20px;
transform: rotate(-30deg) scale(0.8);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.ice-crystal.right {
right: 20px;
transform: rotate(30deg) scale(0.8);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 卡片内容区 */
.card-body-frozen {
padding: 2.5rem;
position: relative;
z-index: 2;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 书籍基本信息区域 */
.book-info-row {
background: linear-gradient(to right, rgba(232, 244, 255, 0.7), rgba(216, 234, 255, 0.4));
border-radius: 15px;
padding: 20px;
margin-bottom: 30px !important;
box-shadow: 0 5px 15px rgba(79, 149, 255, 0.1);
position: relative;
overflow: hidden;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 书籍封面 */
.book-cover-container {
2025-05-01 04:52:53 +08:00
display: flex;
2025-05-06 12:01:11 +08:00
justify-content: center;
align-items: center;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-frame {
position: relative;
padding: 10px;
background-color: white;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
transform: rotate(-3deg);
transition: transform 0.5s ease;
z-index: 1;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-frame:hover {
transform: rotate(0deg) scale(1.05);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-cover {
max-height: 250px;
width: auto;
object-fit: contain;
border-radius: 5px;
transform: rotate(3deg);
transition: transform 0.5s ease;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-frame:hover .book-cover {
transform: rotate(0deg);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-glow {
position: absolute;
top: 0;
left: 0;
2025-05-01 04:52:53 +08:00
width: 100%;
2025-05-06 12:01:11 +08:00
height: 100%;
background: radial-gradient(circle at 50% 50%, rgba(173, 216, 230, 0.4), rgba(173, 216, 230, 0) 70%);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-frame:hover .book-glow {
opacity: 1;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 书籍详情 */
.book-details {
display: flex;
flex-direction: column;
justify-content: center;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-title {
color: #4169e1;
font-weight: 700;
margin-bottom: 20px;
font-size: 1.8rem;
position: relative;
2025-05-01 04:52:53 +08:00
display: inline-block;
}
2025-05-06 12:01:11 +08:00
.book-title::after {
content: "";
position: absolute;
bottom: -10px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(to right, #7AB6FF, transparent);
border-radius: 3px;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.book-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.info-item {
margin: 0;
2025-05-01 04:52:53 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
font-size: 1.1rem;
color: #34495e;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.info-item i {
color: #7AB6FF;
margin-right: 10px;
font-size: 1.2rem;
width: 24px;
text-align: center;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 库存标签 */
.frozen-badge {
display: inline-block;
padding: 0.35em 0.9em;
border-radius: 50px;
font-weight: 600;
margin-left: 8px;
font-size: 0.95rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.high-stock {
background: linear-gradient(45deg, #e0f7fa, #b3e5fc);
color: #0277bd;
border: 1px solid #81d4fa;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.low-stock {
background: linear-gradient(45deg, #fff8e1, #ffecb3);
color: #ff8f00;
border: 1px solid #ffe082;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.out-stock {
background: linear-gradient(45deg, #ffebee, #ffcdd2);
color: #c62828;
border: 1px solid #ef9a9a;
}
/* 历史记录区域 */
.history-section {
2025-05-01 04:52:53 +08:00
position: relative;
2025-05-06 12:01:11 +08:00
margin-top: 40px;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.section-title {
color: #4169e1;
font-weight: 700;
font-size: 1.4rem;
margin-bottom: 25px;
position: relative;
display: inline-block;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.section-title i {
margin-right: 10px;
color: #7AB6FF;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.magic-underline {
position: absolute;
bottom: -8px;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(to right, #7AB6FF, transparent);
animation: sparkle 2s infinite;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
@keyframes sparkle {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
/* 自定义表格 */
.table-container {
position: relative;
margin-bottom: 30px;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 5px 15px rgba(79, 149, 255, 0.1);
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.table-frozen {
width: 100%;
background-color: white;
border-collapse: collapse;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.table-header-row {
display: grid;
grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 2fr 1.5fr;
background: linear-gradient(45deg, #5e81ac, #81a1c1);
color: white;
font-weight: 600;
}
2025-05-01 04:52:53 +08:00
2025-05-06 12:01:11 +08:00
.th-frozen {
padding: 15px;
text-align: center;
position: relative;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.th-frozen:not(:last-child)::after {
content: "";
position: absolute;
right: 0;
top: 20%;
height: 60%;
width: 1px;
background-color: rgba(255, 255, 255, 0.3);
}
2025-05-01 04:52:53 +08:00
2025-05-06 12:01:11 +08:00
.table-body {
max-height: 500px;
overflow-y: auto;
2025-05-01 04:52:53 +08:00
}
2025-05-06 12:01:11 +08:00
.table-row {
display: grid;
grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 2fr 1.5fr;
border-bottom: 1px solid #ecf0f1;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
overflow: hidden;
}
2025-05-01 04:52:53 +08:00
2025-05-06 12:01:11 +08:00
.table-row:hover {
background-color: #f0f8ff;
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(79, 149, 255, 0.1);
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
.table-row::before {
content: "";
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 4px;
background: linear-gradient(to bottom, #7AB6FF, #5e81ac);
opacity: 0;
transition: opacity 0.3s ease;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.table-row:hover::before {
opacity: 1;
}
.td-frozen {
padding: 15px;
text-align: center;
2025-04-30 16:23:05 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
justify-content: center;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.remark-cell {
text-align: left;
justify-content: flex-start;
font-style: italic;
color: #7f8c8d;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* 表格中的徽章 */
.operation-badge {
display: inline-flex;
align-items: center;
padding: 5px 12px;
border-radius: 50px;
font-weight: 600;
font-size: 0.9rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.operation-badge i {
margin-left: 5px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.in-badge {
background: linear-gradient(45deg, #e0f7fa, #b3e5fc);
color: #0277bd;
border: 1px solid #81d4fa;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.out-badge {
background: linear-gradient(45deg, #fff8e1, #ffecb3);
color: #ff8f00;
border: 1px solid #ffe082;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 奥拉夫空状态 */
.empty-log {
grid-template-columns: 1fr !important;
height: 250px;
}
.empty-message {
grid-column: span 7;
2025-04-29 11:18:18 +08:00
display: flex;
2025-04-30 16:23:05 +08:00
align-items: center;
2025-05-06 12:01:11 +08:00
justify-content: center;
height: 100%;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.olaf-empty {
text-align: center;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.olaf-image {
width: 120px;
height: 150px;
background-image: url('https://i.imgur.com/lM0cLxb.png');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin: 0 auto 15px;
animation: olaf-wave 3s infinite;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
@keyframes olaf-wave {
0%, 100% { transform: rotate(-5deg); }
50% { transform: rotate(5deg); }
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.olaf-empty p {
font-size: 1.2rem;
color: #7f8c8d;
margin: 0;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 特殊的行样式 */
.log-entry[data-type="in"] {
background-color: rgba(224, 247, 250, 0.2);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.log-entry[data-type="out"] {
background-color: rgba(255, 248, 225, 0.2);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* 分页容器 */
.pagination-container {
margin-top: 30px;
margin-bottom: 10px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-pagination {
display: flex;
padding-left: 0;
list-style: none;
justify-content: center;
gap: 5px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-pagination .page-item {
margin: 0 2px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-pagination .page-link {
2025-04-29 11:18:18 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
justify-content: center;
padding: 8px 16px;
color: #4169e1;
background-color: white;
border: 1px solid #e1f0ff;
border-radius: 50px;
text-decoration: none;
transition: all 0.3s ease;
min-width: 40px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-pagination .page-link:hover {
background-color: #e1f0ff;
color: #2c3e50;
transform: translateY(-2px);
box-shadow: 0 5px 10px rgba(79, 149, 255, 0.1);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-pagination .page-item.active .page-link {
background: linear-gradient(45deg, #7AB6FF, #5e81ac);
color: white;
border-color: #5e81ac;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-pagination .page-item.disabled .page-link {
color: #95a5a6;
background-color: #f8f9fa;
cursor: not-allowed;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* 页脚 */
.card-footer-frozen {
background: linear-gradient(45deg, #ecf5ff, #d8e6ff);
padding: 1.5rem;
border-radius: 0 0 18px 18px;
position: relative;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.footer-actions {
display: flex;
justify-content: space-between;
position: relative;
z-index: 2;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.footer-decoration {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 15px;
background-image: url('https://i.imgur.com/KkMfwWv.png');
background-size: 50px;
background-repeat: repeat-x;
opacity: 0.2;
filter: hue-rotate(190deg);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* 冰雪风格按钮 */
.frozen-btn {
padding: 10px 20px;
border-radius: 50px;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border: none;
color: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-btn i {
margin-right: 8px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.return-btn {
background: linear-gradient(45deg, #81a1c1, #5e81ac);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.return-btn:hover {
background: linear-gradient(45deg, #5e81ac, #4c6f94);
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(94, 129, 172, 0.3);
color: white;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.adjust-btn {
background: linear-gradient(45deg, #7AB6FF, #5d91e5);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.adjust-btn:hover {
background: linear-gradient(45deg, #5d91e5, #4169e1);
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(65, 105, 225, 0.3);
color: white;
}
.frozen-btn::after {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: rgba(255, 255, 255, 0.1);
transform: rotate(45deg);
transition: all 0.3s ease;
opacity: 0;
}
.frozen-btn:hover::after {
opacity: 1;
transform: rotate(45deg) translateY(-50%);
}
/* 动画类 */
.fade-in {
animation: fadeIn 0.5s ease forwards;
opacity: 0;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.selected-row {
background-color: #e3f2fd !important;
position: relative;
z-index: 1;
}
.selected-row::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to right, rgba(122, 182, 255, 0.1), transparent);
pointer-events: none;
2025-04-29 11:18:18 +08:00
}
2025-04-30 16:23:05 +08:00
/* 响应式调整 */
2025-05-06 12:01:11 +08:00
@media (max-width: 992px) {
.table-header-row,
.table-row {
grid-template-columns: 0.5fr 1fr 0.8fr 0.8fr 1fr 1.2fr 1.2fr;
}
.book-info {
grid-template-columns: 1fr;
}
}
2025-04-30 16:23:05 +08:00
@media (max-width: 768px) {
2025-05-06 12:01:11 +08:00
.book-cover-container {
margin-bottom: 30px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-frame {
transform: rotate(0);
max-width: 180px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-cover {
transform: rotate(0);
max-height: 200px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-title {
text-align: center;
font-size: 1.5rem;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.table-header-row,
.table-row {
display: flex;
flex-direction: column;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.th-frozen:after {
display: none;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.th-frozen {
text-align: left;
padding: 10px 15px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.td-frozen {
justify-content: flex-start;
padding: 10px 15px;
border-bottom: 1px solid #ecf0f1;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.td-frozen:before {
content: attr(data-label);
font-weight: 600;
margin-right: 10px;
color: #7f8c8d;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.footer-actions {
flex-direction: column;
gap: 15px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.frozen-btn {
width: 100%;
2025-04-30 16:23:05 +08:00
}
}
2025-05-06 12:01:11 +08:00
================================================================================
File: ./app/static/css/user-list.css
================================================================================
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
/* 用户列表页面样式 */
.user-list-container {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
/* 页面标题和操作按钮 */
2025-04-30 16:23:05 +08:00
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
2025-05-06 12:01:11 +08:00
border-bottom: 1px solid #f0f0f0;
2025-04-30 16:23:05 +08:00
}
.page-header h1 {
2025-05-06 12:01:11 +08:00
font-size: 1.8rem;
color: #333;
2025-04-30 16:23:05 +08:00
margin: 0;
}
2025-05-06 12:01:11 +08:00
.page-header .actions {
2025-04-30 16:23:05 +08:00
display: flex;
2025-05-06 12:01:11 +08:00
gap: 10px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 搜索和筛选区域 */
.search-filter-container {
margin-bottom: 20px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 6px;
}
.search-filter-form .form-row {
2025-04-30 16:23:05 +08:00
display: flex;
2025-05-06 12:01:11 +08:00
flex-wrap: wrap;
justify-content: space-between;
gap: 15px;
}
.search-box {
2025-04-30 16:23:05 +08:00
position: relative;
2025-05-06 12:01:11 +08:00
flex: 1;
min-width: 250px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.search-box input {
padding-right: 40px;
border-radius: 4px;
border: 1px solid #ddd;
}
.btn-search {
2025-04-30 16:23:05 +08:00
position: absolute;
2025-05-06 12:01:11 +08:00
right: 5px;
top: 5px;
background: none;
border: none;
color: #666;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.filter-box {
display: flex;
gap: 10px;
flex-wrap: wrap;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.filter-box select {
min-width: 120px;
border-radius: 4px;
border: 1px solid #ddd;
padding: 5px 10px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-filter, .btn-reset {
padding: 6px 15px;
border-radius: 4px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-filter {
background-color: #4c84ff;
2025-04-30 16:23:05 +08:00
color: white;
2025-05-06 12:01:11 +08:00
border: none;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-reset {
background-color: #f8f9fa;
color: #333;
border: 1px solid #ddd;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 表格样式 */
.table {
width: 100%;
margin-bottom: 0;
color: #333;
border-collapse: collapse;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.table th {
background-color: #f8f9fa;
padding: 12px 15px;
font-weight: 600;
text-align: left;
border-top: 1px solid #dee2e6;
border-bottom: 1px solid #dee2e6;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.table td {
padding: 12px 15px;
vertical-align: middle;
border-bottom: 1px solid #f0f0f0;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.table tr:hover {
background-color: #f8f9fa;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 状态标签 */
.status-badge {
display: inline-block;
padding: 5px 10px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.status-badge.active {
background-color: #e8f5e9;
color: #43a047;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.status-badge.inactive {
background-color: #ffebee;
color: #e53935;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 操作按钮 */
.actions {
display: flex;
gap: 5px;
align-items: center;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.actions .btn {
padding: 5px 8px;
line-height: 1;
}
/* 分页控件 */
.pagination-container {
margin-top: 20px;
2025-04-30 16:23:05 +08:00
display: flex;
justify-content: center;
}
2025-05-06 12:01:11 +08:00
.pagination {
display: flex;
padding-left: 0;
list-style: none;
border-radius: 0.25rem;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.page-item {
margin: 0 2px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.page-link {
position: relative;
display: block;
padding: 0.5rem 0.75rem;
margin-left: -1px;
color: #4c84ff;
background-color: #fff;
border: 1px solid #dee2e6;
text-decoration: none;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.page-item.active .page-link {
z-index: 3;
color: #fff;
background-color: #4c84ff;
border-color: #4c84ff;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.page-item.disabled .page-link {
color: #aaa;
pointer-events: none;
background-color: #f8f9fa;
border-color: #dee2e6;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 通知样式 */
.alert-box {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.alert-box .alert {
margin-bottom: 10px;
padding: 10px 15px;
border-radius: 4px;
opacity: 0;
transition: opacity 0.3s ease-in-out;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.alert-box .fade-in {
opacity: 1;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.alert-box .fade-out {
opacity: 0;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 响应式调整 */
@media (max-width: 992px) {
.search-filter-form .form-row {
flex-direction: column;
}
.search-box, .filter-box {
width: 100%;
}
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
@media (max-width: 768px) {
.table {
display: block;
overflow-x: auto;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
================================================================================
2025-05-12 02:42:27 +08:00
File: ./app/static/css/book_ranking.css
2025-05-06 12:01:11 +08:00
================================================================================
2025-05-12 02:42:27 +08:00
/* app/static/css/book_ranking.css */
.table-container {
margin-top: 30px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.table-container h3 {
text-align: center;
2025-05-06 12:01:11 +08:00
margin-bottom: 20px;
2025-05-12 02:42:27 +08:00
color: var(--accent-color);
font-family: 'Ma Shan Zheng', cursive, Arial, sans-serif;
font-size: 1.6em;
position: relative;
display: inline-block;
left: 50%;
transform: translateX(-50%);
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.table-container h3:before,
.table-container h3:after {
content: '';
position: absolute;
height: 2px;
background: linear-gradient(to right, transparent, var(--primary-color), transparent);
width: 120px;
top: 50%;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.table-container h3:before {
right: 100%;
margin-right: 15px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.table-container h3:after {
left: 100%;
margin-left: 15px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table img {
width: 55px;
height: 80px;
2025-05-06 12:01:11 +08:00
object-fit: cover;
2025-05-12 02:42:27 +08:00
border-radius: 8px;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border: 2px solid white;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table tr:hover img {
transform: scale(1.08);
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
border-color: var(--primary-color);
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table .rank {
font-weight: 700;
text-align: center;
position: relative;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
/* 前三名特殊样式 */
.data-table tr:nth-child(1) .rank:before {
content: '👑';
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table tr:nth-child(2) .rank:before {
content: '✨';
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table tr:nth-child(3) .rank:before {
content: '🌟';
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
font-size: 16px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table .book-title {
font-weight: 500;
color: var(--accent-color);
transition: color 0.3s;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table tr:hover .book-title {
color: #d06b9c;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table .author {
font-style: italic;
color: var(--light-text);
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table .borrow-count {
font-weight: 600;
color: var(--accent-color);
position: relative;
2025-05-14 00:14:34 +08:00
display: block; /* 修改为block以占据整个单元格 */
text-align: center; /* 确保文本居中 */
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table .borrow-count:after {
content: '❤️';
font-size: 12px;
margin-left: 5px;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
transform: translateY(5px);
2025-05-06 12:01:11 +08:00
display: inline-block;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.data-table tr:hover .borrow-count:after {
opacity: 1;
transform: translateY(0);
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.no-data {
text-align: center;
padding: 40px;
color: var(--light-text);
background-color: var(--secondary-color);
border-radius: 12px;
font-style: italic;
border: 1px dashed var(--border-color);
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
/* 书籍行动画 */
#ranking-table-body tr {
transition: transform 0.3s ease, opacity 0.3s ease;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
#ranking-table-body tr:hover {
transform: translateX(5px);
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
/* 加载动画美化 */
.loading-row td {
background-color: var(--secondary-color);
color: var(--accent-color);
font-size: 16px;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
/* 书名悬停效果 */
.book-title {
position: relative;
text-decoration: none;
display: inline-block;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.book-title:after {
content: '';
position: absolute;
width: 100%;
height: 2px;
bottom: -2px;
left: 0;
background-color: var(--accent-color);
transform: scaleX(0);
transform-origin: bottom right;
transition: transform 0.3s ease-out;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
tr:hover .book-title:after {
transform: scaleX(1);
transform-origin: bottom left;
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
/* 特殊效果:波浪下划线 */
@keyframes wave {
0%, 100% { background-position-x: 0%; }
50% { background-position-x: 100%; }
2025-04-30 16:23:05 +08:00
}
2025-05-12 02:42:27 +08:00
.page-title:after {
content: '';
display: block;
width: 100px;
height: 5px;
margin: 10px auto 0;
background: linear-gradient(90deg, var(--primary-color), var(--accent-color), var(--primary-color));
background-size: 200% 100%;
border-radius: 5px;
animation: wave 3s infinite linear;
}
.book-list-title {
text-align: center;
margin-bottom: 25px;
color: var(--accent-color);
font-family: 'Ma Shan Zheng', cursive, Arial, sans-serif;
font-size: 1.6em;
position: relative;
display: inline-block;
left: 50%;
transform: translateX(-50%);
padding: 0 15px;
}
.book-icon {
font-size: 0.9em;
margin: 0 8px;
opacity: 0.85;
}
.column-icon {
font-size: 0.9em;
margin-right: 5px;
opacity: 0.8;
}
.book-list-title:before,
.book-list-title:after {
content: '';
position: absolute;
height: 2px;
background: linear-gradient(to right, transparent, var(--primary-color), transparent);
width: 80px;
top: 50%;
}
.book-list-title:before {
right: 100%;
margin-right: 15px;
}
.book-list-title:after {
left: 100%;
margin-left: 15px;
}
/* 表格中的图标样式 */
.data-table .borrow-count:after {
content: '📚';
font-size: 12px;
margin-left: 5px;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
transform: translateY(5px);
display: inline-block;
}
2025-05-14 00:14:34 +08:00
/* 前三名特殊样式 - 替换这部分代码 */
.data-table tr:nth-child(1) .rank:before,
.data-table tr:nth-child(2) .rank:before,
.data-table tr:nth-child(3) .rank:before {
position: absolute;
left: 10px; /* 调整到数字左侧 */
top: 50%; /* 垂直居中 */
transform: translateY(-50%); /* 保持垂直居中 */
opacity: 0.9;
}
/* 分别设置每个奖牌的内容 */
2025-05-12 02:42:27 +08:00
.data-table tr:nth-child(1) .rank:before {
content: '🏆';
font-size: 18px;
}
.data-table tr:nth-child(2) .rank:before {
content: '🥈';
font-size: 16px;
}
.data-table tr:nth-child(3) .rank:before {
content: '🥉';
font-size: 16px;
}
2025-05-14 00:14:34 +08:00
/* 调整排名单元格的内边距,为图标留出空间 */
.data-table .rank {
padding-left: 35px; /* 增加左内边距为图标腾出空间 */
text-align: left; /* 使数字左对齐 */
}
2025-05-12 02:42:27 +08:00
/* 加载动画美化 */
.loading-animation {
display: flex;
align-items: center;
justify-content: center;
}
.loading-animation:before {
content: '📖';
margin-right: 10px;
animation: bookFlip 2s infinite;
display: inline-block;
}
@keyframes bookFlip {
0% { transform: rotateY(0deg); }
50% { transform: rotateY(180deg); }
100% { transform: rotateY(360deg); }
}
================================================================================
File: ./app/static/css/book-detail.css
================================================================================
/* 图书详情页样式 */
.book-detail-container {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.actions {
display: flex;
gap: 10px;
}
.book-content {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
overflow: hidden;
}
.book-header {
display: flex;
padding: 25px;
border-bottom: 1px solid #f0f0f0;
background-color: #f9f9f9;
}
.book-cover-large {
flex: 0 0 200px;
height: 300px;
background-color: #f0f0f0;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
margin-right: 30px;
}
.book-cover-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
.no-cover-large {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: #aaa;
}
.no-cover-large i {
font-size: 48px;
margin-bottom: 10px;
}
.book-main-info {
flex: 1;
}
.book-title {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 15px;
color: #333;
}
.book-author {
font-size: 1.1rem;
color: #555;
margin-bottom: 20px;
}
.book-meta-info {
margin-bottom: 25px;
}
.meta-item {
display: flex;
align-items: center;
margin-bottom: 12px;
color: #666;
}
.meta-item i {
width: 20px;
margin-right: 10px;
text-align: center;
color: #555;
}
.meta-value {
font-weight: 500;
color: #444;
}
.tag {
display: inline-block;
background-color: #e9ecef;
color: #495057;
padding: 2px 8px;
border-radius: 3px;
margin-right: 5px;
margin-bottom: 5px;
font-size: 0.85rem;
}
.book-status-info {
display: flex;
align-items: center;
gap: 20px;
margin-top: 20px;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
font-weight: 600;
font-size: 0.9rem;
}
.status-badge.available {
background-color: #d4edda;
color: #155724;
}
.status-badge.unavailable {
background-color: #f8d7da;
color: #721c24;
}
.stock-info {
font-size: 0.95rem;
color: #555;
}
.book-details-section {
padding: 25px;
}
.book-details-section h3 {
font-size: 1.3rem;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
color: #444;
}
.book-description {
color: #555;
line-height: 1.6;
}
.no-description {
color: #888;
font-style: italic;
}
.book-borrow-history {
padding: 0 25px 25px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-borrow-history h3 {
font-size: 1.3rem;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
color: #444;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.borrow-table {
border: 1px solid #eee;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.no-records {
color: #888;
font-style: italic;
text-align: center;
padding: 20px;
2025-04-30 16:23:05 +08:00
background-color: #f9f9f9;
2025-05-06 12:01:11 +08:00
border-radius: 4px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 响应式调整 */
@media (max-width: 768px) {
.book-header {
flex-direction: column;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.book-cover-large {
margin-right: 0;
margin-bottom: 20px;
max-width: 200px;
align-self: center;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.actions {
width: 100%;
}
}
2025-05-14 00:14:34 +08:00
================================================================================
File: ./app/static/css/announcement-form.css
================================================================================
.announcement-form-container {
padding: 20px;
max-width: 900px;
margin: 0 auto;
}
.page-header {
margin-bottom: 25px;
border-bottom: 1px solid #e3e3e3;
padding-bottom: 10px;
}
.card {
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
margin-bottom: 30px;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
font-weight: 500;
margin-bottom: 0.5rem;
display: block;
}
.ql-container {
min-height: 200px;
font-size: 16px;
}
.form-check {
margin-top: 20px;
margin-bottom: 20px;
}
.form-buttons {
display: flex;
justify-content: flex-end;
gap: 15px;
margin-top: 30px;
}
.form-buttons .btn {
min-width: 100px;
}
/* Quill编辑器样式重写 */
.ql-toolbar.ql-snow {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.ql-container.ql-snow {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
}
2025-05-06 12:01:11 +08:00
================================================================================
File: ./app/static/css/book.css
================================================================================
/* 图书列表页面样式 - 女性友好版 */
/* 背景和泡泡动画 */
.book-list-container {
padding: 24px;
background-color: #ffeef2; /* 淡粉色背景 */
min-height: calc(100vh - 60px);
position: relative;
2025-04-30 16:23:05 +08:00
overflow: hidden;
}
2025-05-06 12:01:11 +08:00
/* 泡泡动画 */
.book-list-container::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 0;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
@keyframes bubble {
0% {
transform: translateY(100%) scale(0);
opacity: 0;
}
50% {
opacity: 0.6;
}
100% {
transform: translateY(-100vh) scale(1);
opacity: 0;
}
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.bubble {
position: absolute;
bottom: -50px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
z-index: 1;
animation: bubble 15s infinite ease-in;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 为页面添加15个泡泡 */
.bubble:nth-child(1) { left: 5%; width: 30px; height: 30px; animation-duration: 20s; animation-delay: 0s; }
.bubble:nth-child(2) { left: 15%; width: 20px; height: 20px; animation-duration: 18s; animation-delay: 1s; }
.bubble:nth-child(3) { left: 25%; width: 25px; height: 25px; animation-duration: 16s; animation-delay: 2s; }
.bubble:nth-child(4) { left: 35%; width: 15px; height: 15px; animation-duration: 15s; animation-delay: 0.5s; }
.bubble:nth-child(5) { left: 45%; width: 30px; height: 30px; animation-duration: 14s; animation-delay: 3s; }
.bubble:nth-child(6) { left: 55%; width: 20px; height: 20px; animation-duration: 13s; animation-delay: 2.5s; }
.bubble:nth-child(7) { left: 65%; width: 25px; height: 25px; animation-duration: 12s; animation-delay: 1.5s; }
.bubble:nth-child(8) { left: 75%; width: 15px; height: 15px; animation-duration: 11s; animation-delay: 4s; }
.bubble:nth-child(9) { left: 85%; width: 30px; height: 30px; animation-duration: 10s; animation-delay: 3.5s; }
.bubble:nth-child(10) { left: 10%; width: 18px; height: 18px; animation-duration: 19s; animation-delay: 0.5s; }
.bubble:nth-child(11) { left: 20%; width: 22px; height: 22px; animation-duration: 17s; animation-delay: 2.5s; }
.bubble:nth-child(12) { left: 30%; width: 28px; height: 28px; animation-duration: 16s; animation-delay: 1.2s; }
.bubble:nth-child(13) { left: 40%; width: 17px; height: 17px; animation-duration: 15s; animation-delay: 3.7s; }
.bubble:nth-child(14) { left: 60%; width: 23px; height: 23px; animation-duration: 13s; animation-delay: 2.1s; }
.bubble:nth-child(15) { left: 80%; width: 19px; height: 19px; animation-duration: 12s; animation-delay: 1.7s; }
/* 页面标题部分 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(233, 152, 174, 0.3);
position: relative;
z-index: 2;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.page-header h1 {
color: #d23f6e;
font-size: 1.9rem;
font-weight: 600;
margin: 0;
text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 更漂亮的顶部按钮 */
.action-buttons {
display: flex;
gap: 12px;
position: relative;
z-index: 2;
}
.action-buttons .btn {
2025-04-30 16:23:05 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
justify-content: center;
2025-04-30 16:23:05 +08:00
gap: 8px;
2025-05-06 12:01:11 +08:00
border-radius: 50px;
font-weight: 500;
padding: 9px 18px;
transition: all 0.3s ease;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.06);
border: none;
font-size: 0.95rem;
position: relative;
overflow: hidden;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.action-buttons .btn::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.2), transparent);
pointer-events: none;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.action-buttons .btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.08);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.action-buttons .btn:active {
transform: translateY(1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 按钮颜色 */
.btn-primary {
background: linear-gradient(135deg, #5c88da, #4a73c7);
color: white;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-success {
background: linear-gradient(135deg, #56c596, #41b384);
2025-04-30 16:23:05 +08:00
color: white;
}
2025-05-06 12:01:11 +08:00
.btn-info {
background: linear-gradient(135deg, #5bc0de, #46b8da);
2025-04-30 16:23:05 +08:00
color: white;
}
2025-05-06 12:01:11 +08:00
.btn-secondary {
background: linear-gradient(135deg, #f0ad4e, #ec971f);
color: white;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-danger {
background: linear-gradient(135deg, #ff7676, #ff5252);
color: white;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 过滤和搜索部分 */
.filter-section {
margin-bottom: 25px;
padding: 18px;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 16px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 2;
backdrop-filter: blur(5px);
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.search-form {
display: flex;
flex-direction: column;
gap: 16px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.search-row {
margin-bottom: 5px;
width: 100%;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.search-group {
display: flex;
width: 100%;
max-width: 800px;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.search-group .form-control {
border: 1px solid #f9c0d0;
border-right: none;
border-radius: 25px 0 0 25px;
padding: 10px 20px;
height: 42px;
font-size: 0.95rem;
background-color: rgba(255, 255, 255, 0.9);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.3s;
flex: 1;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.search-group .form-control:focus {
outline: none;
border-color: #e67e9f;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2);
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.search-group .btn {
border-radius: 50%;
width: 42px;
height: 42px;
min-width: 42px;
padding: 0;
background: linear-gradient(135deg, #e67e9f 60%, #ffd3e1 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
margin-left: -1px; /* 防止和输入框间有缝隙 */
font-size: 1.1rem;
box-shadow: 0 2px 6px rgba(230, 126, 159, 0.10);
transition: background 0.2s, box-shadow 0.2s;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.search-group .btn:hover {
background: linear-gradient(135deg, #d23f6e 80%, #efb6c6 100%);
color: #fff;
box-shadow: 0 4px 12px rgba(230, 126, 159, 0.14);
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.filter-row {
display: flex;
flex-wrap: wrap;
gap: 15px;
width: 100%;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.filter-group {
flex: 1;
min-width: 130px;
}
2025-04-30 16:23:05 +08:00
2025-05-06 12:01:11 +08:00
.filter-section .form-control {
border: 1px solid #f9c0d0;
border-radius: 25px;
height: 42px;
padding: 10px 20px;
background-color: rgba(255, 255, 255, 0.9);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23e67e9f' d='M6 8.825L1.175 4 2.238 2.938 6 6.7 9.763 2.937 10.825 4z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 15px center;
background-size: 12px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05);
width: 100%;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.filter-section .form-control:focus {
outline: none;
border-color: #e67e9f;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 0 0 3px rgba(230, 126, 159, 0.2);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 图书网格布局 */
.books-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
margin-bottom: 30px;
position: relative;
z-index: 2;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
/* 图书卡片样式 */
.book-card {
2025-04-30 16:23:05 +08:00
display: flex;
2025-04-30 23:28:51 +08:00
flex-direction: column;
2025-05-06 12:01:11 +08:00
border-radius: 16px;
overflow: hidden;
background-color: white;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.06);
2025-04-30 23:28:51 +08:00
transition: all 0.3s ease;
2025-05-06 12:01:11 +08:00
height: 100%;
position: relative;
border: 1px solid rgba(233, 152, 174, 0.2);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-card:hover {
transform: translateY(-8px);
box-shadow: 0 12px 25px rgba(0, 0, 0, 0.1);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-cover {
width: 100%;
height: 180px;
background-color: #faf3f5;
overflow: hidden;
position: relative;
}
.book-cover::after {
content: '';
position: absolute;
2025-04-30 23:28:51 +08:00
top: 0;
left: 0;
2025-05-06 12:01:11 +08:00
width: 100%;
height: 100%;
background: linear-gradient(to bottom, transparent 60%, rgba(249, 219, 227, 0.4));
pointer-events: none;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-cover img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.book-card:hover .book-cover img {
transform: scale(1.05);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.no-cover {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #ffeef2 0%, #ffd9e2 100%);
color: #e67e9f;
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 1;
pointer-events: none;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.no-cover i {
font-size: 36px;
margin-bottom: 10px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-info {
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-title {
font-size: 1.1rem;
2025-04-30 23:28:51 +08:00
font-weight: 600;
2025-05-06 12:01:11 +08:00
margin: 0 0 10px;
color: #d23f6e;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-author {
font-size: 0.95rem;
color: #888;
margin-bottom: 15px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-meta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-category {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
background-color: #ffebf0;
color: #e67e9f;
2025-04-30 23:28:51 +08:00
font-weight: 500;
}
2025-05-06 12:01:11 +08:00
.book-status {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-status.available {
background-color: #dffff6;
color: #26a69a;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-status.unavailable {
background-color: #ffeeee;
color: #e57373;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
font-size: 0.9rem;
color: #777;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-details p {
margin: 0;
display: flex;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-details strong {
min-width: 65px;
color: #999;
font-weight: 600;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
/* 按钮组样式 */
.book-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: auto;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-actions .btn {
padding: 8px 0;
font-size: 0.9rem;
text-align: center;
border-radius: 25px;
transition: all 0.3s;
2025-04-30 23:28:51 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
justify-content: center;
gap: 6px;
border: none;
font-weight: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-actions .btn:hover {
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.12);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-actions .btn i {
font-size: 0.85rem;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
/* 具体按钮颜色 */
.book-actions .btn-primary {
background: linear-gradient(135deg, #5c88da, #4a73c7);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-actions .btn-info {
background: linear-gradient(135deg, #5bc0de, #46b8da);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-actions .btn-success {
background: linear-gradient(135deg, #56c596, #41b384);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.book-actions .btn-danger {
background: linear-gradient(135deg, #ff7676, #ff5252);
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
/* 无图书状态 */
.no-books {
grid-column: 1 / -1;
padding: 50px 30px;
text-align: center;
background-color: white;
border-radius: 16px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
position: relative;
z-index: 2;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.no-books i {
font-size: 60px;
color: #f9c0d0;
margin-bottom: 20px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.no-books p {
font-size: 1.1rem;
color: #e67e9f;
font-weight: 500;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
/* 分页容器 */
.pagination-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 30px;
position: relative;
z-index: 2;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.pagination {
display: flex;
list-style: none;
padding: 0;
margin: 0 0 15px 0;
background-color: white;
border-radius: 30px;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08);
2025-04-30 23:28:51 +08:00
overflow: hidden;
}
2025-05-06 12:01:11 +08:00
.pagination .page-item {
margin: 0;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.pagination .page-link {
display: flex;
align-items: center;
justify-content: center;
min-width: 40px;
height: 40px;
padding: 0 15px;
border: none;
color: #777;
font-weight: 500;
transition: all 0.2s;
position: relative;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.pagination .page-link:hover {
color: #e67e9f;
background-color: #fff9fb;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.pagination .page-item.active .page-link {
background-color: #e67e9f;
color: white;
box-shadow: none;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.pagination .page-item.disabled .page-link {
color: #bbb;
background-color: #f9f9f9;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.pagination-info {
color: #999;
font-size: 0.9rem;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
/* 优化模态框样式 */
.modal-content {
border-radius: 20px;
border: none;
box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07);
overflow: hidden;
}
.modal-header {
padding: 20px 25px;
background-color: #ffeef2;
border-bottom: 1px solid #ffe0e9;
}
.modal-title {
color: #d23f6e;
font-size: 1.2rem;
2025-04-30 23:28:51 +08:00
font-weight: 600;
}
2025-05-06 12:01:11 +08:00
.modal-body {
padding: 25px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal-footer {
padding: 15px 25px;
border-top: 1px solid #ffe0e9;
background-color: #ffeef2;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal-body p {
color: #666;
font-size: 1rem;
line-height: 1.6;
}
.modal-body p.text-danger {
color: #ff5252 !important;
font-weight: 500;
2025-04-30 23:28:51 +08:00
display: flex;
align-items: center;
2025-05-06 12:01:11 +08:00
gap: 8px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal-body p.text-danger::before {
content: "\f06a";
font-family: "Font Awesome 5 Free";
font-weight: 900;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal .close {
font-size: 1.5rem;
color: #e67e9f;
opacity: 0.8;
text-shadow: none;
transition: all 0.2s;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal .close:hover {
opacity: 1;
color: #d23f6e;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal .btn {
border-radius: 25px;
padding: 8px 20px;
font-weight: 500;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.1);
border: none;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.modal .btn-secondary {
background: linear-gradient(135deg, #a0a0a0, #808080);
color: white;
}
.modal .btn-danger {
background: linear-gradient(135deg, #ff7676, #ff5252);
color: white;
}
/* 封面标题栏 */
.cover-title-bar {
position: absolute;
left: 0; right: 0; bottom: 0;
background: linear-gradient(0deg, rgba(233,152,174,0.92) 0%, rgba(255,255,255,0.08) 90%);
color: #fff;
font-size: 1rem;
font-weight: bold;
padding: 10px 14px 7px 14px;
text-shadow: 0 2px 6px rgba(180,0,80,0.14);
line-height: 1.3;
width: 100%;
box-sizing: border-box;
display: flex;
align-items: flex-end;
min-height: 38px;
z-index: 2;
}
.book-card:hover .cover-title-bar {
background: linear-gradient(0deg, #d23f6e 0%, rgba(255,255,255,0.1) 100%);
font-size: 1.07rem;
letter-spacing: .5px;
}
/* 响应式调整 */
@media (max-width: 992px) {
.filter-row {
flex-wrap: wrap;
}
.filter-group {
flex: 1 0 180px;
}
}
@media (max-width: 768px) {
.book-list-container {
padding: 16px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.action-buttons {
2025-04-30 23:28:51 +08:00
width: 100%;
2025-05-06 12:01:11 +08:00
overflow-x: auto;
padding-bottom: 8px;
flex-wrap: nowrap;
justify-content: flex-start;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.filter-section {
padding: 15px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.search-form {
flex-direction: column;
gap: 12px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.search-group {
max-width: 100%;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.filter-row {
gap: 12px;
}
.books-grid {
grid-template-columns: 1fr;
}
.book-actions {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 600px) {
.cover-title-bar {
font-size: 0.95rem;
min-height: 27px;
padding: 8px 8px 5px 10px;
}
.book-actions {
grid-template-columns: 1fr;
2025-04-30 23:28:51 +08:00
}
}
================================================================================
2025-05-06 12:01:11 +08:00
File: ./app/static/css/login.css
2025-04-30 23:28:51 +08:00
================================================================================
2025-05-06 12:01:11 +08:00
/* login.css - 登录页面专用样式 */
2025-04-30 23:28:51 +08:00
* {
margin: 0;
padding: 0;
box-sizing: border-box;
2025-05-06 12:01:11 +08:00
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
: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;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
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;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
body {
background-color: var(--bg-color);
background-image: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
background-size: cover;
background-position: center;
2025-04-30 23:28:51 +08:00
display: flex;
2025-05-06 12:01:11 +08:00
flex-direction: column;
2025-04-30 23:28:51 +08:00
min-height: 100vh;
2025-05-06 12:01:11 +08:00
color: var(--text-color);
transition: all 0.3s ease;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.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);
2025-04-30 23:28:51 +08:00
position: fixed;
2025-05-06 12:01:11 +08:00
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -1;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.main-container {
2025-04-30 23:28:51 +08:00
display: flex;
justify-content: center;
2025-05-06 12:01:11 +08:00
align-items: center;
flex: 1;
padding: 20px;
2025-04-30 23:28:51 +08:00
}
2025-05-06 12:01:11 +08:00
.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;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.logo {
text-align: center;
margin-bottom: 25px;
position: relative;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.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;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
h1 {
2025-04-30 16:23:05 +08:00
text-align: center;
2025-05-06 12:01:11 +08:00
color: var(--text-color);
margin-bottom: 10px;
font-weight: 600;
font-size: 28px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.subtitle {
text-align: center;
color: var(--light-text);
margin-bottom: 30px;
font-size: 14px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.form-group {
margin-bottom: 22px;
position: relative;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.form-group label {
display: block;
margin-bottom: 8px;
color: var(--text-color);
font-weight: 500;
font-size: 14px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.input-with-icon {
position: relative;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.input-icon {
position: absolute;
left: 15px;
top: 50%;
transform: translateY(-50%);
color: var(--light-text);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.form-control {
2025-04-30 16:23:05 +08:00
width: 100%;
2025-05-06 12:01:11 +08:00
height: 48px;
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 0 15px 0 45px;
font-size: 15px;
2025-04-30 16:23:05 +08:00
transition: all 0.3s ease;
2025-05-06 12:01:11 +08:00
background-color: var(--card-bg);
color: var(--text-color);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 137, 220, 0.2);
2025-04-30 16:23:05 +08:00
outline: none;
}
2025-05-06 12:01:11 +08:00
.password-toggle {
2025-04-30 16:23:05 +08:00
position: absolute;
2025-05-06 12:01:11 +08:00
right: 15px;
2025-04-30 16:23:05 +08:00
top: 50%;
transform: translateY(-50%);
2025-05-06 12:01:11 +08:00
cursor: pointer;
color: var(--light-text);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.validation-message {
margin-top: 6px;
font-size: 12px;
color: var(--error-color);
display: none;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.validation-message.show {
display: block;
animation: shake 0.5s ease;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.remember-forgot {
2025-04-30 16:23:05 +08:00
display: flex;
2025-05-06 12:01:11 +08:00
justify-content: space-between;
2025-04-30 16:23:05 +08:00
align-items: center;
2025-05-06 12:01:11 +08:00
margin-bottom: 25px;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.custom-checkbox {
2025-04-30 16:23:05 +08:00
position: relative;
2025-05-06 12:01:11 +08:00
padding-left: 30px;
2025-04-30 16:23:05 +08:00
cursor: pointer;
2025-05-06 12:01:11 +08:00
font-size: 14px;
user-select: none;
color: var(--light-text);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.custom-checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.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;
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.custom-checkbox:hover input ~ .checkmark {
border-color: var(--primary-color);
2025-04-30 16:23:05 +08:00
}
2025-05-06 12:01:11 +08:00
.custom-checkbox input:checked ~ .checkmark {
background-color: var(--primary-color);
border-color: var(--primary-color);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.checkmark:after {
content: "";
2025-04-29 11:18:18 +08:00
position: absolute;
display: none;
}
2025-05-06 12:01:11 +08:00
.custom-checkbox input:checked ~ .checkmark:after {
2025-04-29 11:18:18 +08:00
display: block;
}
2025-05-06 12:01:11 +08:00
.custom-checkbox .checkmark:after {
left: 6px;
top: 2px;
width: 4px;
height: 9px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.forgot-password a {
color: var(--primary-color);
text-decoration: none;
font-size: 14px;
transition: color 0.3s ease;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.forgot-password a:hover {
color: var(--primary-hover);
text-decoration: underline;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-login {
width: 100%;
height: 48px;
background-color: var(--primary-color);
2025-04-29 11:18:18 +08:00
color: white;
2025-05-06 12:01:11 +08:00
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-login:hover {
background-color: var(--primary-hover);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-login:active {
transform: scale(0.98);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-login .loading {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-login.loading-state {
color: transparent;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-login.loading-state .loading {
display: block;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.signup {
2025-04-29 11:18:18 +08:00
text-align: center;
2025-05-06 12:01:11 +08:00
margin-top: 25px;
font-size: 14px;
color: var(--light-text);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.signup a {
color: var(--primary-color);
text-decoration: none;
2025-04-29 11:18:18 +08:00
font-weight: 600;
2025-05-06 12:01:11 +08:00
transition: color 0.3s ease;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.signup a:hover {
color: var(--primary-hover);
text-decoration: underline;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.features {
display: flex;
justify-content: center;
margin-top: 25px;
gap: 30px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.feature-item {
text-align: center;
font-size: 12px;
color: var(--light-text);
2025-04-29 11:18:18 +08:00
display: flex;
2025-05-06 12:01:11 +08:00
flex-direction: column;
2025-04-29 11:18:18 +08:00
align-items: center;
}
2025-05-06 12:01:11 +08:00
.feature-icon {
margin-bottom: 5px;
font-size: 18px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
footer {
text-align: center;
padding: 20px;
color: rgba(255, 255, 255, 0.7);
font-size: 12px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
footer a {
color: rgba(255, 255, 255, 0.9);
text-decoration: none;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.alert {
padding: 10px;
margin-bottom: 15px;
border-radius: 4px;
color: #721c24;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
@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;
}
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
================================================================================
File: ./app/static/css/book-edit.css
================================================================================
/* ========== 优雅粉色主题 - 图书编辑系统 ========== */
:root {
--primary-pink: #FF85A2;
--primary-pink-hover: #FF6D8E;
--secondary-pink: #FFC0D3;
--accent-pink: #FF4778;
--background-pink: #FFF5F7;
--border-pink: #FFD6E0;
--soft-lavender: #E2D1F9;
--mint-green: #D0F0C0;
--dark-text: #5D4E60;
--medium-text: #8A7B8F;
--light-text: #BFB5C6;
--white: #FFFFFF;
--shadow-sm: 0 4px 6px rgba(255, 133, 162, 0.1);
--shadow-md: 0 6px 12px rgba(255, 133, 162, 0.15);
--shadow-lg: 0 15px 25px rgba(255, 133, 162, 0.2);
--border-radius-sm: 8px;
--border-radius-md: 12px;
--border-radius-lg: 16px;
--transition-fast: 0.2s ease;
--transition-base: 0.3s ease;
--font-primary: 'Poppins', 'Helvetica Neue', sans-serif;
--font-secondary: 'Playfair Display', serif;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* ========== 全局样式 ========== */
body {
background-color: var(--background-pink);
color: var(--dark-text);
font-family: var(--font-primary);
line-height: 1.6;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-secondary);
color: var(--dark-text);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
a {
color: var(--accent-pink);
transition: color var(--transition-fast);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
a:hover {
color: var(--primary-pink-hover);
text-decoration: none;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn {
border-radius: var(--border-radius-sm);
font-weight: 500;
transition: all var(--transition-base);
box-shadow: var(--shadow-sm);
padding: 0.5rem 1.25rem;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-primary {
background-color: var(--primary-pink);
border-color: var(--primary-pink);
}
.btn-primary:hover, .btn-primary:focus {
background-color: var(--primary-pink-hover);
border-color: var(--primary-pink-hover);
}
.btn-info {
background-color: var(--soft-lavender);
border-color: var(--soft-lavender);
color: var(--dark-text);
}
.btn-info:hover, .btn-info:focus {
background-color: #D4BFF0;
border-color: #D4BFF0;
color: var(--dark-text);
}
.btn-secondary {
background-color: var(--white);
border-color: var(--border-pink);
color: var(--medium-text);
}
.btn-secondary:hover, .btn-secondary:focus {
background-color: var(--border-pink);
border-color: var(--border-pink);
color: var(--dark-text);
}
.btn i {
margin-right: 8px;
}
/* ========== 表单容器 ========== */
.book-form-container {
max-width: 1400px;
margin: 2rem auto;
padding: 2rem;
background-color: var(--white);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-md);
position: relative;
2025-04-29 11:18:18 +08:00
overflow: hidden;
}
2025-05-06 12:01:11 +08:00
.book-form-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 8px;
background: linear-gradient(to right, var(--primary-pink), var(--accent-pink), var(--soft-lavender));
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* ========== 页面标题区域 ========== */
.page-header {
2025-04-29 11:18:18 +08:00
display: flex;
justify-content: space-between;
2025-05-06 12:01:11 +08:00
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid var(--secondary-pink);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.page-header h1 {
font-size: 2.2rem;
font-weight: 700;
color: var(--primary-pink);
margin: 0;
position: relative;
font-family: var(--font-secondary);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.flower-icon {
color: var(--accent-pink);
margin-right: 8px;
}
.actions {
display: flex;
gap: 1rem;
}
/* ========== 表单元素 ========== */
.form-row {
margin-bottom: 1.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
color: var(--dark-text);
2025-04-29 11:18:18 +08:00
font-weight: 500;
2025-05-06 12:01:11 +08:00
font-size: 0.95rem;
margin-bottom: 0.5rem;
display: block;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.form-control {
border: 2px solid var(--border-pink);
border-radius: var(--border-radius-sm);
padding: 0.75rem 1rem;
color: var(--dark-text);
transition: all var(--transition-fast);
font-size: 0.95rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.form-control:focus {
border-color: var(--primary-pink);
box-shadow: 0 0 0 0.2rem rgba(255, 133, 162, 0.25);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.form-control::placeholder {
color: var(--light-text);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.required {
color: var(--accent-pink);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
select.form-control {
height: 42px; / 确保高度一致,内容不截断 */
line-height: 1.5;
padding: 8px 12px;
font-size: 0.95rem;
color: var(--dark-text);
background-color: var(--white);
border: 1px solid var(--border-pink);
border-radius: var(--border-radius-sm);
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%235D4E60' viewBox='0 0 24 24'%3E%3Cpath d='M7 10l5 5 5-5z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
select.form-control:focus {
border-color: var(--primary-pink);
outline: none;
box-shadow: 0 0 0 0.2rem rgba(255, 133, 162, 0.2);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* 状态选项 / 分类样式专属修复(可选项) */
#status, #category_id {
padding-top: 8px;
padding-bottom: 8px;
font-family: inherit;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* iOS & Edge 下拉兼容优化 */
select.form-control::-ms-expand {
display: none;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* 浏览器优雅过渡体验 */
select.form-control:hover {
border-color: var(--accent-pink);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
select.form-control:disabled {
background-color: var(--background-pink);
color: var(--light-text);
cursor: not-allowed;
opacity: 0.7;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
textarea.form-control {
min-height: 150px;
resize: vertical;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* ========== 卡片样式 ========== */
.card {
border: none;
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: all var(--transition-base);
margin-bottom: 1.5rem;
background-color: var(--white);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.card:hover {
box-shadow: var(--shadow-md);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.card-header {
background-color: var(--secondary-pink);
border-bottom: none;
padding: 1rem 1.5rem;
font-family: var(--font-secondary);
font-weight: 600;
color: var(--dark-text);
font-size: 1.1rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.card-body {
padding: 1.5rem;
background-color: var(--white);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* ========== 封面图片区域 ========== */
.cover-preview-container {
padding: 1rem;
text-align: center;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.cover-preview {
min-height: 300px;
background-color: var(--background-pink);
border: 2px dashed var(--secondary-pink);
border-radius: var(--border-radius-sm);
overflow: hidden;
2025-04-29 11:18:18 +08:00
display: flex;
align-items: center;
justify-content: center;
2025-05-06 12:01:11 +08:00
margin-bottom: 1rem;
position: relative;
transition: all var(--transition-fast);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.cover-preview:hover {
border-color: var(--primary-pink);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.cover-image {
max-width: 100%;
max-height: 300px;
border-radius: var(--border-radius-sm);
box-shadow: var(--shadow-sm);
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.no-cover-placeholder {
2025-04-29 11:18:18 +08:00
display: flex;
flex-direction: column;
2025-05-06 12:01:11 +08:00
align-items: center;
justify-content: center;
color: var(--light-text);
padding: 2rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.no-cover-placeholder i {
font-size: 3rem;
margin-bottom: 1rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.upload-container {
margin-top: 1rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.btn-outline-primary {
color: var(--primary-pink);
border-color: var(--primary-pink);
background-color: transparent;
transition: all var(--transition-base);
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
.btn-outline-primary:hover, .btn-outline-primary:focus {
background-color: var(--primary-pink);
color: white;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
/* ========== 提交按钮区域 ========== */
.form-submit-container {
margin-top: 2rem;
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
.btn-lg {
padding: 1rem 1.5rem;
font-size: 1.1rem;
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
.btn-block {
width: 100%;
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
/* 输入组样式 */
.input-group-prepend .input-group-text {
background-color: var(--secondary-pink);
border-color: var(--border-pink);
color: var(--dark-text);
border-radius: var(--border-radius-sm) 0 0 var(--border-radius-sm);
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
/* 聚焦效果 */
.is-focused label {
color: var(--primary-pink);
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
/* ========== 动画效果 ========== */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
.book-form-container {
animation: fadeIn 0.5s ease;
}
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
/* ========== 响应式样式 ========== */
@media (max-width: 992px) {
.book-form-container {
padding: 1.5rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.actions {
margin-top: 1rem;
2025-04-29 11:18:18 +08:00
}
}
2025-05-06 12:01:11 +08:00
@media (max-width: 768px) {
.book-form-container {
padding: 1rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.card-header, .card-body {
padding: 1rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.cover-preview {
min-height: 250px;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.col-md-8, .col-md-4 {
padding: 0 0.5rem;
2025-04-29 11:18:18 +08:00
}
}
2025-05-06 12:01:11 +08:00
.is-invalid {
border-color: #dc3545;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.is-valid {
border-color: #28a745;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.invalid-feedback {
display: none;
color: #dc3545;
font-size: 0.875rem;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.is-invalid ~ .invalid-feedback {
display: block;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
================================================================================
File: ./app/static/css/borrow_management.css
================================================================================
2025-04-29 11:18:18 +08:00
2025-05-06 12:01:11 +08:00
/* borrow_management.css - Optimized for literary female audience */
/* Main typography and colors */
body {
font-family: 'Georgia', serif;
color: #4a3728;
background-color: #fcf8f3;
2025-04-29 11:18:18 +08:00
}
2025-05-06 12:01:11 +08:00
.page-title {
margin-bottom: 1.5rem;
color: #5d3511;
border-bottom: 2px solid #d9c7b8;
padding-bottom: 15px;
font-family: 'Playfair Display', Georgia, serif;
letter-spacing: 0.5px;
position: relative;
2025-04-29 11:18:18 +08:00
}