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>© 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
|
|
|
|
}
|
|
|
|
|
|
|
|
|