diff --git a/app/__init__.py b/app/__init__.py index 3b71b3e..b90eb0b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -18,25 +18,18 @@ login_manager = LoginManager() def create_app(config=None): app = Flask(__name__) - # 配置应用 - app.config.from_mapping( - SECRET_KEY=os.environ.get('SECRET_KEY', 'dev_key_replace_in_production'), - SQLALCHEMY_DATABASE_URI='mysql+pymysql://book20250428:booksystem@27.124.22.104/book_system', - SQLALCHEMY_TRACK_MODIFICATIONS=False, - PERMANENT_SESSION_LIFETIME=86400 * 7, # 7天 + # 加载默认配置 + app.config.from_object('config') - # 邮件配置 - EMAIL_HOST='smtp.qq.com', - EMAIL_PORT=587, - EMAIL_ENCRYPTION='starttls', - EMAIL_USERNAME='3399560459@qq.com', - EMAIL_PASSWORD='fzwhyirhbqdzcjgf', - EMAIL_FROM='3399560459@qq.com', - EMAIL_FROM_NAME='BOOKSYSTEM_OFFICIAL' - ) + # 如果提供了配置对象,则加载它 + if config: + if isinstance(config, dict): + app.config.update(config) + else: + app.config.from_object(config) - # 实例配置,如果存在 - app.config.from_pyfile('config.py', silent=True) + # 从环境变量指定的文件加载配置(如果有) + app.config.from_envvar('APP_CONFIG_FILE', silent=True) # 初始化数据库 db.init_app(app) diff --git a/app/static/covers/0c78e7aa-df33-49c6-b921-5a82d723964f.jpg b/app/static/covers/0c78e7aa-df33-49c6-b921-5a82d723964f.jpg index 72cbaec..56f8bd3 100644 Binary files a/app/static/covers/0c78e7aa-df33-49c6-b921-5a82d723964f.jpg and b/app/static/covers/0c78e7aa-df33-49c6-b921-5a82d723964f.jpg differ diff --git a/app/static/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg b/app/static/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg index 72cbaec..56f8bd3 100644 Binary files a/app/static/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg and b/app/static/covers/69a1f7af-2b9c-4354-9af9-f3bb909acad4_.jpg differ diff --git a/app/static/covers/bainiangudu.jpg b/app/static/covers/bainiangudu.jpg index e6bcd0d..47c8169 100644 Binary files a/app/static/covers/bainiangudu.jpg and b/app/static/covers/bainiangudu.jpg differ diff --git a/app/static/covers/santi.jpg b/app/static/covers/santi.jpg index e21adaa..ada84d6 100644 Binary files a/app/static/covers/santi.jpg and b/app/static/covers/santi.jpg differ diff --git a/app/static/covers/zhongguotongshi.jpg b/app/static/covers/zhongguotongshi.jpg index 4b57d84..5d113be 100644 Binary files a/app/static/covers/zhongguotongshi.jpg and b/app/static/covers/zhongguotongshi.jpg differ diff --git a/code_collection.txt b/code_collection.txt index 73d753f..f918364 100644 --- a/code_collection.txt +++ b/code_collection.txt @@ -143,9 +143,11 @@ from app.controllers.borrow import borrow_bp from app.controllers.inventory import inventory_bp from flask_login import LoginManager, current_user from app.controllers.statistics import statistics_bp +from app.controllers.announcement import announcement_bp +from app.models.notification import Notification from app.controllers.log import log_bp import os - +from datetime import datetime login_manager = LoginManager() @@ -190,6 +192,7 @@ def create_app(config=None): app.register_blueprint(statistics_bp) app.register_blueprint(inventory_bp) app.register_blueprint(log_bp) + app.register_blueprint(announcement_bp, url_prefix='/announcement') # 创建数据库表 with app.app_context(): @@ -260,9 +263,83 @@ def create_app(config=None): @app.route('/') def index(): - if not current_user.is_authenticated: - return redirect(url_for('user.login')) - return render_template('index.html') # 无需传递current_user,Flask-Login自动提供 + 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 + ) @app.errorhandler(404) def page_not_found(e): @@ -274,38 +351,71 @@ def create_app(config=None): return Markup(s.replace('\n', '
')) return s + @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 + ) + return app @app.context_processor def inject_now(): return {'now': datetime.datetime.now()} + + ================================================================================ File: ./app/utils/auth.py ================================================================================ from functools import wraps -from flask import g, redirect, url_for, flash, request +from flask import redirect, url_for, flash, request +from flask_login import current_user + def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): - if g.user is None: + print(f"DEBUG: login_required 检查 - 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)) return f(*args, **kwargs) + return decorated_function + def admin_required(f): @wraps(f) def decorated_function(*args, **kwargs): - if g.user is None: + print(f"DEBUG: admin_required 检查 - 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 g.user.role_id != 1: # 假设role_id=1是管理员 + + 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属性 flash('权限不足', 'danger') return redirect(url_for('index')) return f(*args, **kwargs) + return decorated_function ================================================================================ @@ -625,6 +735,121 @@ class Log(db.Model): File: ./app/models/notification.py ================================================================================ +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 ================================================================================ File: ./app/models/__init__.py @@ -734,6 +959,129 @@ class BorrowRecord(db.Model): File: ./app/models/announcement.py ================================================================================ +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) ================================================================================ File: ./app/models/inventory.py @@ -2246,7 +2594,8 @@ File: ./app/static/css/book_ranking.css font-weight: 600; color: var(--accent-color); position: relative; - display: inline-block; + display: block; /* 修改为block以占据整个单元格 */ + text-align: center; /* 确保文本居中 */ } .data-table .borrow-count:after { @@ -2389,34 +2738,40 @@ tr:hover .book-title:after { display: inline-block; } -/* 前三名特殊样式 */ +/* 前三名特殊样式 - 替换这部分代码 */ +.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; +} + +/* 分别设置每个奖牌的内容 */ .data-table tr:nth-child(1) .rank:before { content: '🏆'; - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); font-size: 18px; } .data-table tr:nth-child(2) .rank:before { content: '🥈'; - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); font-size: 16px; } .data-table tr:nth-child(3) .rank:before { content: '🥉'; - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); font-size: 16px; } +/* 调整排名单元格的内边距,为图标留出空间 */ +.data-table .rank { + padding-left: 35px; /* 增加左内边距为图标腾出空间 */ + text-align: left; /* 使数字左对齐 */ +} + + /* 加载动画美化 */ .loading-animation { display: flex; @@ -2657,6 +3012,69 @@ File: ./app/static/css/book-detail.css } } +================================================================================ +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; +} + ================================================================================ File: ./app/static/css/book.css ================================================================================ @@ -5277,25 +5695,35 @@ ul { display: flex; background-color: #f8fafc; border-radius: 8px; - padding: 15px; + padding: 20px 15px 15px; /* 增加顶部内边距,为角标留出空间 */ min-width: 280px; position: relative; + margin-top: 10px; /* 在顶部添加一些外边距 */ + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + transition: transform 0.2s; +} + +.popular-book-item:hover { + transform: translateY(-3px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); } .rank-badge { position: absolute; - top: -10px; + top: -8px; /* 略微调高一点 */ left: 10px; background-color: #4a89dc; color: white; - width: 24px; - height: 24px; + width: 28px; /* 增加尺寸 */ + height: 28px; /* 增加尺寸 */ display: flex; align-items: center; justify-content: center; border-radius: 50%; - font-size: 0.8rem; + font-size: 0.85rem; font-weight: bold; + z-index: 10; /* 确保它位于其他元素之上 */ + box-shadow: 0 2px 4px rgba(0,0,0,0.2); /* 添加阴影使其更突出 */ } .book-cover.small { @@ -5303,6 +5731,14 @@ ul { height: 90px; min-width: 60px; margin-right: 15px; + border-radius: 4px; + overflow: hidden; /* 确保图片不会溢出容器 */ +} + +.book-cover.small img { + width: 100%; + height: 100%; + object-fit: cover; /* 确保图片正确填充容器 */ } .book-details { @@ -9947,6 +10383,94 @@ body { } } +================================================================================ +File: ./app/static/css/announcement-manage.css +================================================================================ + +.announcement-manage-container { + padding: 20px; +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + border-bottom: 1px solid #e3e3e3; + padding-bottom: 15px; +} + +.filter-container { + margin-bottom: 25px; +} + +.filter-form { + display: flex; + gap: 15px; + align-items: center; + flex-wrap: wrap; +} + +.filter-form .form-group { + margin-bottom: 0; + min-width: 200px; +} + +.announcement-table { + background-color: #fff; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + border-radius: 8px; +} + +.announcement-table th { + background-color: #f8f9fa; + white-space: nowrap; +} + +.announcement-title { + font-weight: 500; + color: #333; + text-decoration: none; + display: block; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.announcement-title:hover { + color: #007bff; + text-decoration: underline; +} + +.btn-group { + display: flex; + gap: 5px; +} + +.pagination-container { + margin-top: 30px; + display: flex; + justify-content: center; +} + +.no-records { + text-align: center; + padding: 50px 20px; + background-color: #f8f9fa; + border-radius: 8px; + color: #6c757d; +} + +.no-records i { + font-size: 3rem; + margin-bottom: 15px; +} + +.no-records p { + font-size: 1.2rem; +} + ================================================================================ File: ./app/static/css/user_activity.css ================================================================================ @@ -11189,85 +11713,343 @@ File: ./app/static/css/statistics.css /* app/static/css/statistics.css */ :root { - --primary-color: #f8c4d4; - --secondary-color: #fde9f1; - --accent-color: #e684ae; - --text-color: #7a4b56; - --light-text: #a67b84; - --border-color: #f3d1dc; - --shadow-color: rgba(244, 188, 204, 0.25); - --hover-color: #f4bccc; + /* Soft & Elegant Palette */ + --color-primary-light-pink: #FCE4EC; /* 淡粉色 */ + --color-primary-milk-white: #FFF8F0; /* 奶白色 */ + --color-primary-apricot: #FFDAB9; /* 浅杏色 */ + + --color-aux-rose-gold: #B76E79; /* 玫瑰金 */ + --color-aux-light-purple: #E6E6FA; /* 淡紫色 */ + --color-aux-soft-gray: #D3D3D3; /* 柔和的灰色 */ + + --color-accent-berry-red: #8C2D5A; /* 深一点的浆果红 */ + + --font-serif-elegant: 'Playfair Display', serif; + --font-serif-lora: 'Lora', serif; + --font-sans-clean: 'Open Sans', sans-serif; + --font-script-delicate: 'Sacramento', cursive; + --font-serif-garamond: 'EB Garamond', serif; + + /* Derived/General Usage */ + --background-main: var(--color-primary-milk-white); + --background-container: #FFFFFF; + --text-main: #5D5053; /* A darker, softer, slightly desaturated rose-brown */ + --text-soft: #8A797C; + --text-heading: var(--color-aux-rose-gold); + --text-accent: var(--color-accent-berry-red); + --border-soft: var(--color-aux-soft-gray); + --border-decorative: var(--color-primary-light-pink); + --shadow-soft: rgba(183, 110, 121, 0.1); /* Soft shadow based on rose gold */ + --shadow-subtle: rgba(0, 0, 0, 0.05); + + /* Fallback for old variables - some might still be used by unchanged CSS */ + --primary-color: var(--color-primary-light-pink); + --secondary-color: var(--color-primary-apricot); /* Or #FFF8F0 for a lighter secondary */ + --accent-color: var(--color-aux-rose-gold); + --text-color: var(--text-main); + --light-text: var(--text-soft); + --border-color: var(--border-soft); + --shadow-color: var(--shadow-soft); + --hover-color: #F8E0E6; /* Lighter pink for hover */ } body { - background-color: #fff9fb; - color: var(--text-color); - font-family: 'Arial', sans-serif; + background-color: var(--background-main); + color: var(--text-main); + font-family: var(--font-sans-clean); + font-weight: 300; /* Lighter default font weight */ + line-height: 1.7; /* Increased line height */ } .statistics-container { - padding: 25px; - max-width: 1200px; - margin: 0 auto; - background-color: white; - border-radius: 15px; - box-shadow: 0 5px 20px var(--shadow-color); + padding: 40px 30px; /* Increased padding */ + max-width: 1100px; /* Slightly adjusted max-width */ + margin: 40px auto; /* More margin for breathing room */ + background-color: var(--background-container); + border-radius: 16px; /* Softer, larger border-radius */ + box-shadow: 0 8px 25px var(--shadow-soft); /* Softer shadow */ position: relative; overflow: hidden; } .page-title { - color: var(--accent-color); - margin-bottom: 30px; + color: var(--text-heading); + margin-bottom: 35px; padding-bottom: 15px; - border-bottom: 2px dotted var(--border-color); + border-bottom: 1px solid var(--border-decorative); /* Thinner, delicate line */ text-align: center; - font-family: 'Ma Shan Zheng', cursive, Arial, sans-serif; - font-size: 2.2em; - letter-spacing: 1px; + font-family: var(--font-serif-elegant); + font-size: 2.8em; /* Larger, more prominent */ + font-weight: 700; + letter-spacing: 0.5px; } -/* 波浪下划线动画 */ +/* Simplified page title decoration */ .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; + width: 80px; /* Shorter line */ + height: 2px; /* Thinner line */ + margin: 12px auto 0; + background: var(--color-aux-rose-gold); /* Solid accent color */ + border-radius: 2px; + /* animation: wave 3s infinite linear; Removed wave animation for elegance */ } -@keyframes wave { +/* @keyframes wave { 0%, 100% { background-position-x: 0%; } 50% { background-position-x: 100%; } +} */ + +/* Quote Banner - Styled for elegance */ +.quote-banner { + background-color: var(--color-primary-light-pink); /* Soft pink background */ + border-radius: 12px; /* Softer radius */ + padding: 25px 35px; /* Ample padding */ + margin: 0 auto 40px; /* Increased bottom margin */ + max-width: 75%; + text-align: center; + box-shadow: 0 4px 15px rgba(183, 110, 121, 0.08); /* Very subtle shadow */ + border-left: 3px solid var(--color-aux-rose-gold); + border-right: 3px solid var(--color-aux-rose-gold); + position: relative; } +.quote-banner p { + font-family: var(--font-serif-garamond), serif; /* Elegant serif for quote */ + font-style: italic; + color: var(--color-accent-berry-red); /* Berry red for emphasis */ + font-size: 1.1em; /* Slightly larger */ + margin: 0; + letter-spacing: 0.2px; + line-height: 1.6; +} + +.quote-banner:before, +.quote-banner:after { + content: """; /* Using """ for opening */ + font-family: var(--font-serif-elegant), serif; /* Consistent elegant font */ + font-size: 50px; /* Adjusted size */ + color: var(--color-aux-rose-gold); /* Rose gold for quotes */ + opacity: 0.4; /* Softer opacity */ + position: absolute; + top: 0px; +} + +.quote-banner:before { + left: 15px; +} + +.quote-banner:after { + content: """; /* Using """ for closing */ + right: 15px; + top: auto; /* Adjust position for closing quote mark */ + bottom: -20px; +} + +/* Stats Grid - main navigation cards container */ +.stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + grid-gap: 30px; /* Increased gap for more whitespace */ + margin: 40px auto; /* Adjusted margin */ + max-width: 900px; /* Adjusted max-width */ +} + +.stats-grid .stats-card { + position: relative; + background-color: var(--background-container); + border-radius: 12px; /* Softer radius */ + overflow: hidden; + box-shadow: 0 6px 18px var(--shadow-subtle); /* More subtle shadow */ + transition: transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 0.35s cubic-bezier(0.25, 0.8, 0.25, 1); + text-decoration: none; + color: var(--text-main); + border: 1px solid #F0E8E9; /* Very light, almost invisible border */ + min-height: 260px; /* Ensure cards have enough height */ + padding: 0; +} + +.card-inner { /* This class is directly inside stats-card links */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 25px; /* Ample padding */ + height: 100%; + position: relative; + z-index: 2; + background: transparent; /* Make it transparent to show card background */ + transition: background-color 0.3s ease; +} + +.stats-grid .stats-card:hover { + transform: translateY(-6px); /* Slightly less aggressive transform */ + box-shadow: 0 10px 25px var(--shadow-soft); /* Enhanced shadow on hover */ + border-color: var(--color-primary-light-pink); +} +.stats-grid .stats-card:hover .card-inner { + /* background: rgba(255, 248, 240, 0.5); */ /* Optional: very subtle hover background on inner part */ +} + + +.stats-grid .card-icon { + font-size: 36px; /* Slightly smaller icon */ + margin-bottom: 18px; + color: var(--color-aux-rose-gold); + background-color: var(--color-primary-milk-white); /* Milk white for icon background */ + width: 70px; /* Adjusted size */ + height: 70px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + box-shadow: 0 3px 8px rgba(183, 110, 121, 0.15); /* Subtle shadow for icon */ + transition: transform 0.3s ease, color 0.3s ease; +} + +.stats-grid .stats-card:hover .card-icon { + transform: scale(1.08) rotate(3deg); + color: var(--color-accent-berry-red); /* Icon color change on hover */ +} + +.stats-grid .card-title { + font-family: var(--font-serif-lora); + font-size: 1.45em; /* Adjusted size */ + font-weight: 600; + margin-bottom: 12px; + color: var(--text-heading); + position: relative; + display: inline-block; +} + +.stats-grid .card-title:after { /* Decorative line under card title */ + content: ''; + position: absolute; + bottom: -6px; /* Positioned slightly below */ + left: 50%; + transform: translateX(-50%) scaleX(0); /* Start scaled to 0 */ + width: 60%; /* Line width relative to title */ + height: 1.5px; + background-color: var(--color-primary-light-pink); /* Light pink line */ + transition: transform 0.35s ease-out; + transform-origin: center; +} + +.stats-grid .stats-card:hover .card-title:after { + transform: translateX(-50%) scaleX(1); /* Scale to full on hover */ +} + +.stats-grid .card-description { + font-family: var(--font-sans-clean); + font-size: 0.9em; + color: var(--text-soft); + line-height: 1.5; + max-width: 90%; /* Prevent text from touching edges */ +} + + +/* Card Decoration - Subtle background elements */ +.card-decoration { + position: absolute; + bottom: -40px; /* Adjusted position */ + right: -40px; + width: 120px; /* Smaller decoration */ + height: 120px; + border-radius: 50%; + background-color: var(--color-primary-light-pink); /* Light pink base */ + opacity: 0.15; /* More subtle opacity */ + transition: all 0.5s ease; + z-index: 1; +} + +.stats-card:hover .card-decoration { /* Use stats-card hover for decoration */ + transform: scale(1.4); + opacity: 0.25; +} + +/* Specific card decorations with more subtle emoji styling */ +.card-decoration:before { /* General style for emoji if used */ + position: absolute; + font-size: 24px; /* Smaller emoji */ + top: 50%; /* Centered better */ + left: 50%; + transform: translate(-50%, -50%); + opacity: 0.3; /* Very subtle */ + color: var(--color-aux-rose-gold); /* Themed color */ +} + +.book-decoration:before { content: '📚'; } +.trend-decoration:before { content: '📈'; } +.user-decoration:before { content: '👥'; } +.overdue-decoration:before { content: '⏰'; } + + +/* Page Decoration - Floating elements */ +.page-decoration { + position: absolute; + width: 180px; /* Slightly smaller */ + height: 180px; + border-radius: 50%; + background: linear-gradient(45deg, var(--color-primary-apricot), var(--color-aux-light-purple), var(--color-primary-light-pink)); /* New gradient */ + opacity: 0.15; /* More subtle */ + z-index: -1; /* Ensure it's behind content */ +} + +.page-decoration.left { + top: -80px; /* Adjusted position */ + left: -80px; + animation: floatLeft 18s ease-in-out infinite; +} + +.page-decoration.right { + bottom: -80px; + right: -80px; + animation: floatRight 20s ease-in-out infinite; +} + +@keyframes floatLeft { + 0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); } + 25% { transform: translate(15px, 20px) rotate(8deg) scale(1.05); } + 50% { transform: translate(5px, 35px) rotate(15deg) scale(1); } + 75% { transform: translate(25px, 10px) rotate(5deg) scale(1.05); } +} + +@keyframes floatRight { + 0%, 100% { transform: translate(0, 0) rotate(0deg) scale(1); } + 25% { transform: translate(-15px, -18px) rotate(-7deg) scale(1.05); } + 50% { transform: translate(-10px, -30px) rotate(-12deg) scale(1); } + 75% { transform: translate(-22px, -12px) rotate(-6deg) scale(1.05); } +} + + +/* --- Unchanged CSS from this point onwards as per request for elements not in index.html --- */ +/* --- (Or elements whose styling should largely be preserved unless overridden by above general styles) --- */ + .breadcrumb { margin-bottom: 20px; font-size: 14px; - color: var(--light-text); + color: var(--light-text); /* Will use new --light-text */ } .breadcrumb a { - color: var(--accent-color); + color: var(--accent-color); /* Will use new --accent-color */ text-decoration: none; transition: all 0.3s ease; } .breadcrumb a:hover { text-decoration: underline; - color: #d06b9c; + color: var(--color-accent-berry-red); /* More specific hover */ } .breadcrumb .current-page { - color: var(--text-color); + color: var(--text-color); /* Will use new --text-color */ font-weight: 500; } -/* 原始卡片菜单 */ +/* 原始卡片菜单 - Unchanged as it's not used in the provided HTML */ .stats-menu { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); @@ -11275,8 +12057,10 @@ body { margin-top: 30px; } -/* 原始卡片样式 */ -.stats-card { +/* 原始卡片样式 - This .stats-card is different from .stats-grid .stats-card. Keeping for other pages. */ +/* However, some properties might be inherited if not specific enough. */ +/* Adding a more specific selector to avoid conflict if this old style is needed elsewhere */ +.stats-menu > .stats-card { background-color: var(--secondary-color); border-radius: 12px; padding: 25px; @@ -11291,55 +12075,70 @@ body { border: 1px solid var(--border-color); } -.stats-card:hover { +.stats-menu > .stats-card:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 8px 20px var(--shadow-color); border-color: var(--primary-color); } -.card-icon { +/* Card icon/title/description for .stats-menu > .stats-card */ +.stats-menu > .stats-card .card-icon { font-size: 40px; margin-bottom: 15px; color: var(--accent-color); + /* Resetting some properties from .stats-grid .card-icon if they conflict */ + background-color: transparent; + width: auto; + height: auto; + box-shadow: none; } -.card-title { +.stats-menu > .stats-card .card-title { font-size: 18px; font-weight: 600; margin-bottom: 10px; + font-family: var(--font-sans-clean); /* Keep it simple for this version */ + color: var(--text-color); /* Default text color for these */ +} +.stats-menu > .stats-card .card-title:after { + display: none; /* No line for this version */ } -.card-description { + +.stats-menu > .stats-card .card-description { font-size: 14px; color: var(--light-text); + font-family: var(--font-sans-clean); } + .filter-section { margin-bottom: 25px; display: flex; align-items: center; - background-color: var(--secondary-color); + background-color: var(--color-primary-milk-white); /* Updated bg */ padding: 12px 18px; border-radius: 10px; - border: 1px dashed var(--border-color); + border: 1px dashed var(--border-decorative); /* Updated border */ } .filter-label { font-weight: 500; margin-right: 10px; - color: var(--text-color); + color: var(--text-main); /* Updated text */ } .filter-select { padding: 8px 15px; - border: 1px solid var(--border-color); - border-radius: 8px; + border: 1px solid var(--border-soft); /* Updated border */ + border-radius: 8px; /* Softer radius */ background-color: white; - color: var(--text-color); + color: var(--text-main); font-size: 0.95em; + font-family: var(--font-sans-clean); transition: border-color 0.3s, box-shadow 0.3s; appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23e684ae' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23B76E79' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); /* Updated arrow color */ background-repeat: no-repeat; background-position: right 10px center; padding-right: 30px; @@ -11347,8 +12146,8 @@ body { .filter-select:focus { outline: none; - border-color: var(--accent-color); - box-shadow: 0 0 0 3px rgba(230, 132, 174, 0.25); + border-color: var(--color-aux-rose-gold); /* Updated focus color */ + box-shadow: 0 0 0 3px rgba(183, 110, 121, 0.2); /* Updated focus shadow */ } .ml-20 { @@ -11357,13 +12156,13 @@ body { .chart-container { background-color: white; - border-radius: 12px; + border-radius: 12px; /* Softer radius */ padding: 25px; - box-shadow: 0 4px 15px var(--shadow-color); + box-shadow: 0 4px 15px var(--shadow-soft); /* Updated shadow */ margin-bottom: 35px; position: relative; - height: 400px; /* 添加固定高度 */ - border: 1px solid var(--border-color); + height: 400px; + border: 1px solid var(--border-decorative); /* Updated border */ overflow: hidden; } @@ -11373,14 +12172,13 @@ body { position: relative; } -/* 图表装饰元素 */ -.chart-decoration { +.chart-decoration { /* These are for charts, distinct from page/card decorations */ position: absolute; width: 60px; height: 60px; border-radius: 50%; - background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); - opacity: 0.6; + background: linear-gradient(45deg, var(--color-primary-light-pink), var(--color-primary-apricot)); /* Updated gradient */ + opacity: 0.4; /* Softer opacity */ z-index: 0; } @@ -11400,38 +12198,33 @@ body { @keyframes floating { 0% { transform: translate(0, 0) scale(1); } - 50% { transform: translate(10px, 10px) scale(1.1); } + 50% { transform: translate(8px, 8px) scale(1.05); } /* Softer float */ 100% { transform: translate(0, 0) scale(1); } } -/* 适配小图表 */ .chart-container.half { height: auto; min-height: 400px; - padding-bottom: 40px; /* 增加底部空间 */ + padding-bottom: 40px; } -/* 特别针对分类图表的调整 */ .chart-container.half .chart-wrapper { - height: 340px; /* 增加图表容器高度 */ - padding-bottom: 20px; /* 增加底部填充 */ + height: 340px; + padding-bottom: 20px; } -/* 确保图表完整显示 */ canvas#category-chart { max-height: 100%; margin-bottom: 20px; padding-bottom: 20px; position: relative; } -/* 移除图表装饰元素在分类饼图中的影响 */ .chart-container.half::before, .chart-container.half::after { width: 40px; height: 40px; - opacity: 0.3; + opacity: 0.2; /* Softer opacity */ } -/* 调整图例位置,确保其正确显示 */ .chart-container.half .chart-wrapper { position: relative; } @@ -11449,25 +12242,43 @@ canvas#category-chart { min-width: 300px; } +/* 表格容器样式 */ +.table-container { + margin-bottom: 30px; + position: relative; + overflow: hidden; + border-radius: 12px; + box-shadow: 0 4px 20px var(--shadow-subtle); +} .data-table { width: 100%; - border-collapse: separate; + border-collapse: collapse; /* 修改为collapse以解决边框问题 */ border-spacing: 0; - border-radius: 10px; + border-radius: 10px; /* 保持圆角 */ overflow: hidden; - box-shadow: 0 2px 10px var(--shadow-color); + box-shadow: 0 2px 10px var(--shadow-subtle); /* 保持阴影 */ + font-family: var(--font-sans-clean); /* 确保一致字体 */ } .data-table th, .data-table td { padding: 14px 18px; text-align: left; + border-bottom: 1px solid var(--border-decorative); /* 保持底部边框 */ + vertical-align: middle; /* 确保所有内容垂直居中 */ + box-sizing: border-box; /* 确保边框计算在单元格尺寸内 */ +} + +.data-table td { + font-size: 0.95em; } .data-table th { - background-color: var(--primary-color); - font-weight: 600; - color: var(--text-color); + background-color: var(--color-primary-light-pink); /* Lighter pink for header */ + font-weight: 600; /* Was 600, can be 400 for softer look */ + color: var(--text-heading); /* Rose gold text for header */ letter-spacing: 0.5px; + font-size: 1em; + border-bottom: 2px solid var(--color-aux-rose-gold); } .data-table tr { @@ -11475,21 +12286,41 @@ canvas#category-chart { } .data-table tr:nth-child(even) { - background-color: var(--secondary-color); + background-color: var(--color-primary-milk-white); /* Milk white for even rows */ } .data-table tr:nth-child(odd) { background-color: white; } +.data-table tr:last-child td { + border-bottom: none; +} .data-table tr:hover { - background-color: #fceef3; + background-color: #FEF6F8; /* Very light pink on hover */ +} + +/* 表格特定列的样式 */ +.data-table th:first-child, +.data-table td:first-child { + text-align: center; /* 排名居中 */ + position: relative; /* 确保相对定位 */ +} + +.data-table th:nth-child(2), +.data-table td:nth-child(2) { + text-align: center; /* 封面图片居中 */ +} + +.data-table th:last-child, +.data-table td:last-child { + text-align: center; /* 借阅次数居中显示 */ } .loading-row td { text-align: center; padding: 30px; - color: var(--light-text); + color: var(--text-soft); /* Updated text color */ } .loading-animation { @@ -11503,6 +12334,7 @@ canvas#category-chart { margin-right: 10px; animation: bookFlip 2s infinite; display: inline-block; + color: var(--color-aux-rose-gold); /* Themed color */ } @keyframes bookFlip { @@ -11522,51 +12354,76 @@ canvas#category-chart { 100% { opacity: 0.3; } } -.stats-cards { +.stats-cards { /* This is for the small summary cards, different from .stats-grid */ display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; } -.stats-card .card-value { - font-size: 28px; - font-weight: 700; - margin-bottom: 5px; - color: var(--accent-color); +/* Style for .stats-cards > .stats-card if they exist */ +.stats-cards > .stats-card { + background-color: var(--background-container); + border: 1px solid var(--border-decorative); + padding: 20px; + border-radius: 10px; + box-shadow: 0 3px 10px var(--shadow-subtle); + text-align: center; } -/* 引用容器 */ +.stats-cards .stats-card .card-value { /* Assuming .card-value is inside these cards */ + font-size: 2em; /* Adjusted size */ + font-weight: 700; + margin-bottom: 8px; + color: var(--color-accent-berry-red); /* Berry red for value */ + font-family: var(--font-serif-elegant); +} +.stats-cards .stats-card .card-title { /* Title within these small cards */ + font-family: var(--font-sans-clean); + font-size: 0.95em; + color: var(--text-soft); + font-weight: 400; +} +.stats-cards .stats-card .card-icon { /* Icon within these small cards */ + font-size: 1.8em; + color: var(--color-aux-rose-gold); + margin-bottom: 10px; +} + + +/* Quote Container - Appears distinct from .quote-banner, kept for other pages */ .quote-container { text-align: center; margin: 40px auto 20px; max-width: 600px; font-style: italic; - color: var(--text-color); + color: var(--text-main); /* Updated text */ padding: 20px; - background-color: var(--secondary-color); - border-radius: 12px; + background-color: var(--color-primary-apricot); /* Apricot background */ + border-radius: 12px; /* Softer radius */ position: relative; + font-family: var(--font-serif-garamond); + box-shadow: 0 3px 10px var(--shadow-subtle); } .quote-container:before, .quote-container:after { content: """; - font-size: 60px; - font-family: Georgia, serif; + font-size: 50px; + font-family: var(--font-serif-elegant); position: absolute; - color: var(--primary-color); - opacity: 0.5; + color: var(--color-aux-rose-gold); /* Rose gold quotes */ + opacity: 0.3; /* Softer */ } .quote-container:before { - top: -10px; + top: -5px; left: 10px; } .quote-container:after { content: """; - bottom: -30px; + bottom: -25px; right: 10px; } @@ -11574,50 +12431,54 @@ canvas#category-chart { position: relative; z-index: 1; margin-bottom: 10px; - font-size: 16px; + font-size: 1.05em; /* Adjusted */ + line-height: 1.6; } .quote-author { display: block; - font-size: 14px; + font-size: 0.9em; font-style: normal; text-align: right; - color: var(--light-text); + color: var(--text-soft); /* Updated text */ + font-family: var(--font-sans-clean); } -/* 书籍列表标题 */ +/* Book list title - for table pages */ .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; + color: var(--text-heading); /* Rose gold */ + font-family: var(--font-serif-lora); /* Lora for this title */ + font-size: 1.8em; /* Adjusted */ position: relative; display: inline-block; left: 50%; transform: translateX(-50%); - padding: 0 15px; + padding: 0 20px; } -.book-icon { +.book-icon { /* General book icon if used with this title */ font-size: 0.9em; margin: 0 8px; opacity: 0.85; + color: var(--color-aux-rose-gold); } .column-icon { font-size: 0.9em; margin-right: 5px; opacity: 0.8; + color: var(--color-aux-rose-gold); } .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; + height: 1.5px; /* Thinner line */ + background: linear-gradient(to right, transparent, var(--color-primary-light-pink), transparent); /* Softer gradient */ + width: 70px; top: 50%; } @@ -11632,6 +12493,15 @@ canvas#category-chart { } /* 表格中的图标样式 */ +.data-table .borrow-count { + font-weight: 600; + color: var(--text-heading); + position: relative; + display: block; /* 修改为block以占据整个单元格 */ + text-align: center; /* 确保文本居中 */ + font-size: 1em; +} + .data-table .borrow-count:after { content: '📚'; font-size: 12px; @@ -11640,56 +12510,76 @@ canvas#category-chart { transition: opacity 0.3s ease, transform 0.3s ease; transform: translateY(5px); display: inline-block; + color: var(--color-aux-rose-gold); } .data-table tr:hover .borrow-count:after { - opacity: 1; + opacity: 0.7; /* Softer opacity */ transform: translateY(0); } -/* 前三名特殊样式 */ +/* 排名列样式 */ +.data-table .rank { + font-weight: 700; + text-align: center; + position: relative; + font-size: 1.1em; + color: var(--text-heading); + font-family: var(--font-serif-lora); + padding: 5px 15px; /* 基本内边距 */ +} + +/* 前三名奖牌样式 */ +.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; + font-size: 1.2em; + left: 5px; /* 左侧位置 */ + top: 50%; + transform: translateY(-50%); + opacity: 0.85; +} + +/* 分别设置每个奖牌的内容 */ .data-table tr:nth-child(1) .rank:before { content: '🏆'; - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); - font-size: 18px; } .data-table tr:nth-child(2) .rank:before { content: '🥈'; - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); - font-size: 16px; } .data-table tr:nth-child(3) .rank:before { content: '🥉'; - position: absolute; - top: -15px; - left: 50%; - transform: translateX(-50%); - font-size: 16px; } -/* 书名悬停效果 */ -.book-title { +/* 确保所有排名单元格的对齐一致 */ +.data-table td:first-child { + text-align: center; +} + + +.book-title { /* In data tables */ position: relative; text-decoration: none; display: inline-block; + font-weight: 600; /* Bolder for emphasis */ + color: var(--text-accent); /* Berry red for book titles */ + transition: color 0.3s; +} +.data-table tr:hover .book-title { + color: var(--color-aux-rose-gold); /* Rose gold on hover */ } -.book-title:after { +.book-title:after { /* Underline effect for book titles in tables */ content: ''; position: absolute; width: 100%; - height: 2px; - bottom: -2px; + height: 1.5px; + bottom: -3px; left: 0; - background-color: var(--accent-color); + background-color: var(--color-primary-light-pink); /* Light pink underline */ transform: scaleX(0); transform-origin: bottom right; transition: transform 0.3s ease-out; @@ -11700,324 +12590,137 @@ tr:hover .book-title:after { transform-origin: bottom left; } -/* 数据表格相关样式 */ +/* Data table image styling */ .data-table img { - width: 55px; - height: 80px; + width: 50px; /* Slightly smaller */ + height: 75px; object-fit: cover; - border-radius: 8px; - box-shadow: 0 3px 10px rgba(0,0,0,0.1); + border-radius: 6px; /* Softer radius */ + box-shadow: 0 2px 6px rgba(0,0,0,0.08); /* Softer shadow */ transition: transform 0.3s ease, box-shadow 0.3s ease; border: 2px solid white; } .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); -} - -.data-table .rank { - font-weight: 700; - text-align: center; - position: relative; -} - -.data-table .book-title { - font-weight: 500; - color: var(--accent-color); - transition: color 0.3s; -} - -.data-table tr:hover .book-title { - color: #d06b9c; + transform: scale(1.1); /* Slightly more pop */ + box-shadow: 0 4px 10px rgba(0,0,0,0.12); + border-color: var(--color-primary-light-pink); /* Pink border on hover */ } .data-table .author { font-style: italic; - color: var(--light-text); -} - -.data-table .borrow-count { - font-weight: 600; - color: var(--accent-color); - position: relative; - display: inline-block; + color: var(--text-soft); /* Softer text for author */ + font-size: 0.9em; } .no-data { text-align: center; padding: 40px; - color: var(--light-text); - background-color: var(--secondary-color); + color: var(--text-soft); + background-color: var(--color-primary-milk-white); /* Milk white background */ border-radius: 12px; font-style: italic; - border: 1px dashed var(--border-color); + border: 1px dashed var(--border-decorative); /* Decorative dashed border */ + font-family: var(--font-serif-garamond); } /* 书籍行动画 */ #ranking-table-body tr { - transition: transform 0.3s ease, opacity 0.3s ease; + transition: transform 0.3s ease, opacity 0.3s ease, background-color 0.3s ease; /* Added background-color */ } #ranking-table-body tr:hover { - transform: translateX(5px); + transform: translateX(3px); /* Subtle shift */ } -/* 四宫格统计页面样式 */ -.quote-banner { - background-color: var(--secondary-color); - border-radius: 10px; - padding: 15px 25px; - margin: 0 auto 30px; - max-width: 80%; - text-align: center; - box-shadow: 0 3px 15px var(--shadow-color); - border-left: 4px solid var(--accent-color); - border-right: 4px solid var(--accent-color); - position: relative; -} -.quote-banner p { - font-style: italic; - color: var(--text-color); - font-size: 16px; - margin: 0; - letter-spacing: 0.5px; -} - -.quote-banner:before, -.quote-banner:after { - content: '"'; - font-family: Georgia, serif; - font-size: 50px; - color: var(--primary-color); - opacity: 0.5; - position: absolute; - top: -15px; -} - -.quote-banner:before { - left: 10px; -} - -.quote-banner:after { - right: 10px; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - grid-gap: 25px; - margin: 20px auto; - max-width: 1000px; -} - -/* 四宫格卡片样式 */ -.stats-grid .stats-card { - position: relative; - background-color: white; - border-radius: 15px; - overflow: hidden; - box-shadow: 0 5px 20px var(--shadow-color); - transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); - text-decoration: none; - color: var(--text-color); - border: 1px solid var(--border-color); - height: 250px; - padding: 0; /* 重置内边距 */ -} - -.card-inner { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: 30px; - height: 100%; - position: relative; - z-index: 2; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.stats-grid .stats-card:hover { - transform: translateY(-8px); -} - -.stats-grid .stats-card:hover .card-inner { - background: rgba(255, 255, 255, 0.95); -} - -.stats-grid .card-icon { - font-size: 40px; - margin-bottom: 20px; - color: var(--accent-color); - background-color: var(--secondary-color); - width: 80px; - height: 80px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - box-shadow: 0 4px 10px var(--shadow-color); - transition: transform 0.3s ease; -} - -.stats-grid .stats-card:hover .card-icon { - transform: scale(1.1) rotate(5deg); -} - -.stats-grid .card-title { - font-size: 20px; - font-weight: 600; - margin-bottom: 15px; - color: var(--accent-color); - position: relative; - display: inline-block; -} - -.stats-grid .card-title:after { - content: ''; - position: absolute; - bottom: -5px; - left: 0; - width: 100%; - height: 2px; - background-color: var(--primary-color); - transform: scaleX(0); - transform-origin: left; - transition: transform 0.3s ease; -} - -.stats-grid .stats-card:hover .card-title:after { - transform: scaleX(1); -} - -/* 卡片装饰 */ -.card-decoration { - position: absolute; - bottom: -30px; - right: -30px; - width: 150px; - height: 150px; - border-radius: 50%; - background-color: var(--primary-color); - opacity: 0.1; - transition: all 0.5s ease; - z-index: 1; -} - -.card-decoration.active { - transform: scale(1.5); - opacity: 0.2; -} - -/* 特定卡片的独特装饰 */ -.book-decoration:before { - content: '📚'; - position: absolute; - font-size: 30px; - top: 40px; - left: 40px; - opacity: 0.4; -} - -.trend-decoration:before { - content: '📈'; - position: absolute; - font-size: 30px; - top: 40px; - left: 40px; - opacity: 0.4; -} - -.user-decoration:before { - content: '👥'; - position: absolute; - font-size: 30px; - top: 40px; - left: 40px; - opacity: 0.4; -} - -.overdue-decoration:before { - content: '⏰'; - position: absolute; - font-size: 30px; - top: 40px; - left: 40px; - opacity: 0.4; -} - -/* 页面装饰 */ -.page-decoration { - position: absolute; - width: 200px; - height: 200px; - border-radius: 50%; - background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); - opacity: 0.3; - z-index: -1; -} - -.page-decoration.left { - top: -100px; - left: -100px; - animation: floatLeft 15s ease-in-out infinite; -} - -.page-decoration.right { - bottom: -100px; - right: -100px; - animation: floatRight 17s ease-in-out infinite; -} - -@keyframes floatLeft { - 0%, 100% { transform: translate(0, 0) rotate(0deg); } - 25% { transform: translate(20px, 20px) rotate(5deg); } - 50% { transform: translate(10px, 30px) rotate(10deg); } - 75% { transform: translate(30px, 10px) rotate(5deg); } -} - -@keyframes floatRight { - 0%, 100% { transform: translate(0, 0) rotate(0deg); } - 25% { transform: translate(-20px, -10px) rotate(-5deg); } - 50% { transform: translate(-15px, -25px) rotate(-10deg); } - 75% { transform: translate(-25px, -15px) rotate(-5deg); } -} - -/* 动画效果 */ -.fade-in { - animation: fadeIn 0.5s ease forwards; +/* Animation shared */ +.fade-in { /* This is a custom class, not from animate.css */ + animation: customFadeIn 0.6s ease forwards; /* Renamed to avoid conflict */ opacity: 0; - transform: translateY(10px); + transform: translateY(15px); /* Slightly more travel */ } -@keyframes fadeIn { +@keyframes customFadeIn { /* Renamed */ to { opacity: 1; transform: translateY(0); } } -/* 响应式调整 */ +/* Responsive adjustments */ +@media (max-width: 992px) { /* Adjusted breakpoint */ + .stats-grid { + max-width: 95%; + gap: 20px; /* Smaller gap on medium screens */ + } + .stats-grid .stats-card { + min-height: 240px; + } + .page-title { + font-size: 2.4em; + } + .quote-banner { + max-width: 85%; + } +} + + @media (max-width: 768px) { + .statistics-container { + padding: 30px 20px; + margin: 20px auto; + } + .page-title { + font-size: 2em; + } + .quote-banner { + max-width: 90%; + padding: 20px; + } + .quote-banner:before, + .quote-banner:after { + font-size: 35px; + } + .quote-banner:after { + bottom: -15px; + } + + .stats-grid { + grid-template-columns: 1fr; /* Single column for cards */ + gap: 25px; + max-width: 450px; /* Max width for single column */ + } + + .stats-grid .stats-card { + min-height: auto; /* Auto height for single column */ + height: auto; /* Ensure this is not fixed */ + padding-bottom: 20px; /* Ensure padding for content */ + } + .stats-grid .card-inner { + padding: 20px; + } + + .chart-row { flex-direction: column; } .half { width: 100%; + flex-basis: 100%; /* Ensure it takes full width */ } - .stats-cards { + .stats-cards { /* Small summary cards */ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } .filter-section { flex-wrap: wrap; + padding: 10px 15px; + } + .filter-select { + width: 100%; } .ml-20 { @@ -12025,23 +12728,63 @@ tr:hover .book-title:after { margin-top: 10px; } - .stats-grid { - grid-template-columns: 1fr; + .page-decoration { /* Make page decorations smaller or hide on mobile */ + width: 120px; + height: 120px; + opacity: 0.1; } - - .stats-grid .stats-card { - height: 200px; + .page-decoration.left { + top: -60px; + left: -60px; } - - .quote-banner { - max-width: 95%; - padding: 15px; + .page-decoration.right { + bottom: -60px; + right: -60px; } + .data-table th, .data-table td { + padding: 10px 12px; + font-size: 0.9em; + } + .data-table img { + width: 40px; + height: 60px; + } +} - .quote-banner:before, - .quote-banner:after { +@media (max-width: 480px) { + .page-title { + font-size: 1.8em; + } + .quote-banner p { + font-size: 1em; + } + .stats-grid .card-title { + font-size: 1.3em; + } + .stats-grid .card-description { + font-size: 0.85em; + } + .stats-grid .card-icon { + width: 60px; + height: 60px; font-size: 30px; } + .statistics-container { + margin: 15px auto; + padding: 20px 15px; + } + .page-decoration { + display: none; /* Hide complex decorations on very small screens */ + } + + /* 移动端表格调整 */ + .data-table .rank:before { + left: -5px; /* 小屏幕上减少偏移量 */ + font-size: 1.2em; + } + .data-table .rank { + padding: 5px 8px; /* 减少内边距 */ + } } ================================================================================ @@ -12945,6 +13688,729 @@ body { } } +================================================================================ +File: ./app/static/css/announcement-list.css +================================================================================ + +/* Fresh & Vibrant Style for Announcement List */ + +:root { + --mint-green: #A8E6CF; + --pale-yellow: #FFD3B6; + --coral-pink: #FFAAA5; + --sky-blue: #BDE4F4; + --clean-white: #FFFFFF; + --bright-orange: #FF8C69; /* Emphasis for buttons/key info */ + --lemon-yellow: #FFFACD; + + --text-dark: #424242; + --text-medium: #616161; /* Slightly darker medium for better contrast */ + --text-light: #888888; /* Adjusted light text */ + + --font-title: 'Poppins', sans-serif; + --font-body: 'Nunito Sans', sans-serif; + + --card-shadow: 0 5px 18px rgba(0, 0, 0, 0.07); + --card-hover-shadow: 0 8px 25px rgba(0, 0, 0, 0.12); + --border-radius-main: 16px; /* Slightly larger radius for a softer look */ + --border-radius-small: 10px; +} + +/* Apply base font and background to body (likely in base.css) */ +body { + font-family: var(--font-body); + background-color: #fcfdfe; /* Very light off-white, almost white */ + color: var(--text-dark); + line-height: 1.65; +} + +.announcement-container { + padding: 25px 30px; + max-width: 960px; + margin: 25px auto; + background-color: var(--clean-white); + border-radius: var(--border-radius-main); + /* Optional: Subtle gradient background for the container itself */ + /* background-image: linear-gradient(to bottom right, #f0f9ff, #ffffff); */ +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #eef2f5; /* Softer border */ +} + +.page-header h1 { + font-family: var(--font-title); + font-size: 2.2rem; + font-weight: 700; + color: var(--text-dark); + margin: 0; + display: flex; + align-items: center; +} +.page-icon { /* Icon for page title */ + color: var(--coral-pink); + margin-right: 12px; + font-size: 1.8rem; +} + +/* Optional: Style for a "Create New" button if you add one */ +.btn-fresh-create { + background-color: var(--mint-green); + color: #3a7c68; /* Darker mint for text */ + border: none; + padding: 10px 20px; + border-radius: 25px; + font-family: var(--font-body); + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + font-size: 0.9rem; + box-shadow: 0 2px 8px rgba(168, 230, 207, 0.4); +} + +.btn-fresh-create:hover { + background-color: #97e0c6; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(168, 230, 207, 0.5); +} +.btn-fresh-create i { + margin-right: 8px; +} + + +.announcement-list { + margin-top: 20px; + display: grid; + gap: 25px; /* Spacing between announcement items */ +} + +.announcement-item { + background-color: var(--clean-white); + border-radius: var(--border-radius-main); + box-shadow: var(--card-shadow); + padding: 25px 30px; + position: relative; /* For pin-badge */ + transition: transform 0.25s ease-out, box-shadow 0.25s ease-out; + overflow: hidden; /* If using pseudo-elements for borders */ +} + +.announcement-item:hover { + transform: translateY(-5px) scale(1.01); + box-shadow: var(--card-hover-shadow); +} + +.announcement-item.pinned { + /* Use a top border or a more distinct background */ + border-top: 4px solid var(--mint-green); + background-color: #f6fffb; /* Light mint */ +} + +.pin-badge { + position: absolute; + top: 0px; + right: 0px; + background: linear-gradient(135deg, var(--mint-green), #8fdcc3); + color: var(--clean-white); + padding: 6px 15px 6px 20px; + border-radius: 0 0 0 var(--border-radius-main); /* Creative corner */ + font-size: 0.8rem; + font-weight: 600; + font-family: var(--font-body); + box-shadow: -2px 2px 8px rgba(168, 230, 207, 0.3); +} +.pin-badge i { + margin-right: 6px; + font-size: 0.75rem; +} + +.announcement-header { + display: flex; + justify-content: space-between; + align-items: flex-start; /* Align date to top if title wraps */ + margin-bottom: 10px; +} + +.announcement-header h3 { + margin: 0; + font-size: 1.4rem; /* Slightly larger title */ + font-family: var(--font-title); + font-weight: 600; + line-height: 1.3; + margin-right: 15px; /* Space between title and date */ +} + +.announcement-header h3 a { + color: var(--text-dark); + text-decoration: none; + transition: color 0.2s ease; +} + +.announcement-header h3 a:hover { + color: var(--coral-pink); +} + +.date { + color: var(--text-light); + font-size: 0.85rem; + font-weight: 400; + white-space: nowrap; /* Prevent date from wrapping */ + padding-top: 3px; /* Align better with h3 */ +} + +.announcement-preview { + margin: 15px 0; + color: var(--text-medium); + line-height: 1.7; + font-size: 0.95rem; + letter-spacing: 0.1px; +} + +.announcement-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding-top: 15px; + border-top: 1px solid #f0f4f7; /* Lighter separator */ +} + +.publisher { + color: var(--text-light); + font-size: 0.85rem; + display: flex; + align-items: center; +} +.publisher i { + margin-right: 6px; + color: var(--sky-blue); +} + +.read-more { + color: var(--bright-orange); + text-decoration: none; + font-weight: 600; + font-size: 0.9rem; + font-family: var(--font-body); + display: inline-flex; /* Allows icon alignment and hover effects */ + align-items: center; + padding: 6px 12px; + border-radius: 20px; + background-color: transparent; + transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +.read-more:hover { + background-color: var(--bright-orange); + color: var(--clean-white); + transform: translateX(3px); +} + +.read-more i { + margin-left: 6px; + transition: transform 0.2s ease-in-out; +} +/* .read-more:hover i { + transform: translateX(4px); +} */ /* Handled by transform on .read-more now */ + +/* Pagination Styles (copied and adapted from previous for consistency) */ +.pagination-container { + margin-top: 40px; + display: flex; + justify-content: center; +} + +.pagination { + display: flex; + list-style: none; + padding-left: 0; +} + +.pagination .page-item .page-link { + color: var(--coral-pink); + background-color: var(--clean-white); + border: 1px solid var(--pale-yellow); + margin: 0 5px; /* Slightly more spacing */ + border-radius: 50%; + width: 40px; /* Slightly larger */ + height: 40px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.95rem; + font-family: var(--font-body); + transition: all 0.25s ease-in-out; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); +} + +.pagination .page-item .page-link:hover { + background-color: var(--pale-yellow); + color: var(--coral-pink); + border-color: var(--coral-pink); + text-decoration: none; + transform: translateY(-2px); + box-shadow: 0 3px 8px rgba(255, 211, 182, 0.5); +} + +.pagination .page-item.active .page-link { + background-color: var(--coral-pink); + border-color: var(--coral-pink); + color: var(--clean-white); + box-shadow: 0 4px 10px rgba(255, 170, 165, 0.6); +} + +.pagination .page-item.disabled .page-link { + color: #cccccc; + background-color: #f9f9f9; + border-color: #eeeeee; + pointer-events: none; + box-shadow: none; +} + +.no-records { + text-align: center; + padding: 60px 30px; + background-color: #fffaf8; /* Very light coral/yellow tint */ + border-radius: var(--border-radius-main); + color: var(--text-medium); + margin-top: 20px; + box-shadow: var(--card-shadow); +} + +.no-records-icon { + width: 60px; + height: 60px; + margin-bottom: 20px; + opacity: 0.9; +} +/* Fallback for FontAwesome if SVG doesn't load or is removed */ +.no-records .fas.fa-info-circle { + font-size: 3.5rem; + margin-bottom: 20px; + color: var(--coral-pink); + opacity: 0.8; +} + +.no-records p { + font-size: 1.15rem; + font-family: var(--font-body); + font-weight: 600; + color: var(--text-dark); + line-height: 1.6; +} + +================================================================================ +File: ./app/static/css/notifications.css +================================================================================ + +/* Fresh & Vibrant Style for Notifications */ + +:root { + --mint-green: #A8E6CF; + --pale-yellow: #FFD3B6; + --coral-pink: #FFAAA5; + --sky-blue: #BDE4F4; + --clean-white: #FFFFFF; + --bright-orange: #FF8C69; /* Emphasis for buttons/key info */ + --lemon-yellow: #FFFACD; /* Can be used for subtle highlights */ + + --text-dark: #424242; /* Slightly softer than pure black */ + --text-medium: #757575; + --text-light: #9E9E9E; + + --font-title: 'Poppins', sans-serif; + --font-body: 'Nunito Sans', sans-serif; + + --card-shadow: 0 4px 15px rgba(0, 0, 0, 0.06); + --card-hover-shadow: 0 6px 20px rgba(0, 0, 0, 0.1); + --border-radius-main: 12px; + --border-radius-small: 8px; +} + +/* Apply base font and background to body (likely in base.css, but good for context) */ +body { + font-family: var(--font-body); + background-color: var(--clean-white); /* Or a very light tint like #FDFCFA */ + color: var(--text-dark); + line-height: 1.6; + font-weight: 400; +} + +.notifications-container { + padding: 25px 30px; + max-width: 900px; + margin: 20px auto; + background-color: var(--clean-white); + /* Optional: add a subtle pattern or a large soft circular gradient */ + /* background-image: linear-gradient(135deg, var(--mint-green) -20%, var(--clean-white) 30%); */ + border-radius: var(--border-radius-main); + /* box-shadow: 0 10px 30px rgba(168, 230, 207, 0.2); */ /* Subtle shadow for container */ +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid #f0f0f0; /* Softer border */ +} + +.page-header h1 { + font-family: var(--font-title); + font-size: 2rem; /* Slightly larger */ + font-weight: 600; + color: var(--text-dark); + margin: 0; +} + +/* Fresh Action Button Style */ +.btn-fresh-action { + background-color: var(--bright-orange); + color: var(--clean-white); + border: none; + padding: 10px 20px; + border-radius: 25px; /* Pill shape */ + font-family: var(--font-body); + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + font-size: 0.9rem; + box-shadow: 0 2px 8px rgba(255, 140, 105, 0.3); +} + +.btn-fresh-action:hover { + background-color: #ff7b5a; /* Slightly darker orange */ + color: var(--clean-white); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(255, 140, 105, 0.4); +} + +.btn-fresh-action i { + margin-right: 8px; +} + +.filter-tabs { + display: flex; + margin-bottom: 25px; + gap: 10px; + /* border-bottom: 2px solid var(--pale-yellow); */ /* Optional subtle line */ +} + +.filter-tab { + padding: 10px 20px; + color: var(--text-medium); + text-decoration: none; + border-radius: var(--border-radius-small); /* Rounded tabs */ + font-weight: 600; + font-size: 0.95rem; + transition: all 0.3s ease; + border-bottom: 3px solid transparent; /* Underline effect for active */ +} + +.filter-tab:hover { + color: var(--coral-pink); + background-color: rgba(255, 170, 165, 0.1); /* Light coral tint on hover */ +} + +.filter-tab.active { + color: var(--coral-pink); + border-bottom-color: var(--coral-pink); + /* background-color: var(--coral-pink); */ + /* color: var(--clean-white); */ +} + +.notifications-list { + margin-top: 20px; + display: grid; + gap: 20px; +} + +.notification-card { + background-color: var(--clean-white); + border-radius: var(--border-radius-main); + box-shadow: var(--card-shadow); + padding: 20px 25px; + transition: transform 0.25s ease, box-shadow 0.25s ease; + display: flex; /* For icon alignment */ + align-items: flex-start; /* Align icon to top of content */ + gap: 15px; + border-left: 5px solid transparent; /* Placeholder for unread state */ +} +.notification-icon-area { + font-size: 1.5rem; + color: var(--sky-blue); + padding-top: 5px; /* Align with title */ +} +.notification-card.unread .notification-icon-area { + color: var(--mint-green); +} + + +.notification-card:hover { + transform: translateY(-4px); + box-shadow: var(--card-hover-shadow); +} + +.notification-card.unread { + border-left-color: var(--mint-green); + background-color: #f6fffb; /* Very light mint */ +} + +.notification-content { + flex-grow: 1; +} + +.notification-title { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 0; + margin-bottom: 8px; + font-size: 1.15rem; /* Adjusted size */ + font-family: var(--font-title); + font-weight: 600; +} + +.notification-title a { + color: var(--text-dark); + text-decoration: none; + transition: color 0.2s ease; +} + +.notification-title a:hover { + color: var(--coral-pink); +} + +.unread-badge { + background-color: var(--bright-orange); + color: white; + font-size: 0.7rem; + padding: 4px 10px; + border-radius: 15px; /* Pill shape */ + margin-left: 10px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.notification-text { + color: var(--text-medium); + margin-bottom: 15px; + line-height: 1.6; + font-size: 0.9rem; + letter-spacing: 0.2px; +} + +.notification-meta { + display: flex; + justify-content: space-between; + align-items: center; + color: var(--text-light); + font-size: 0.8rem; +} + +.notification-type { + background-color: var(--sky-blue); /* Sky Blue for type */ + color: #3E84A8; /* Darker text for contrast on sky blue */ + padding: 3px 10px; + border-radius: var(--border-radius-small); + font-weight: 600; +} + +.notification-time { + font-style: italic; +} + +/* Pagination */ +.pagination-container { + margin-top: 30px; + display: flex; + justify-content: center; +} + +.pagination { + display: flex; + list-style: none; + padding-left: 0; +} + +.pagination .page-item .page-link { + color: var(--coral-pink); + background-color: var(--clean-white); + border: 1px solid var(--pale-yellow); + margin: 0 4px; + border-radius: 50%; /* Circular pagination items */ + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.9rem; + transition: all 0.2s ease-in-out; +} + +.pagination .page-item .page-link:hover { + background-color: var(--pale-yellow); + color: var(--coral-pink); + border-color: var(--coral-pink); + text-decoration: none; +} + +.pagination .page-item.active .page-link { + background-color: var(--coral-pink); + border-color: var(--coral-pink); + color: var(--clean-white); + box-shadow: 0 2px 5px rgba(255, 170, 165, 0.5); +} + +.pagination .page-item.disabled .page-link { + color: #ccc; + background-color: #f8f8f8; + border-color: #eee; + pointer-events: none; +} + + +.no-records { + text-align: center; + padding: 50px 20px; + background-color: #fafffd; /* Very light mint/yellow mix */ + border-radius: var(--border-radius-main); + color: var(--text-medium); + margin-top: 20px; +} + +.no-records-icon { /* Style for the inline SVG */ + width: 60px; + height: 60px; + margin-bottom: 20px; + opacity: 0.8; +} +/* If using Font Awesome for no-records icon: */ +.no-records .fas.fa-bell-slash { + font-size: 3.5rem; + margin-bottom: 20px; + color: var(--mint-green); + opacity: 0.7; +} + + +.no-records p { + font-size: 1.1rem; + font-family: var(--font-body); + font-weight: 600; + color: var(--text-dark); +} + +/* Notification Dropdown Styles (assuming this is for a navbar dropdown or similar) */ +/* These are kept minimal as the main focus was the page content */ +.notification-dropdown { + width: 350px; /* Wider for more content */ + padding: 0; + max-height: 450px; + overflow-y: auto; + border-radius: var(--border-radius-small); + box-shadow: 0 5px 25px rgba(0,0,0,0.1); + background-color: var(--clean-white); +} + +.notification-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 15px; + background-color: var(--pale-yellow); /* Light yellow header */ + border-bottom: 1px solid #f0e0d0; +} +.notification-header h5 { + margin:0; + font-family: var(--font-title); + font-weight: 600; + color: var(--text-dark); +} + +.mark-all-read { /* Link in dropdown header */ + font-size: 0.8rem; + color: var(--coral-pink); + font-weight: 600; + text-decoration: none; +} +.mark-all-read:hover { + text-decoration: underline; +} + +.notification-items { + max-height: 300px; + overflow-y: auto; +} + +.notification-item { + padding: 12px 15px; + border-bottom: 1px solid #f5f5f5; + transition: background-color 0.2s ease; +} +.notification-item:last-child { + border-bottom: none; +} + +.notification-item:hover { + background-color: var(--mint-green-light, #e6f7f0); /* Very light mint on hover */ +} + +.notification-item.unread { + background-color: #fff8f0; /* Very light orange/yellow for unread in dropdown */ + border-left: 3px solid var(--bright-orange); + padding-left: 12px; +} + +.notification-item .notification-content h6 { /* Assuming title in dropdown is h6 */ + margin-bottom: 5px; + font-size: 0.9rem; + font-family: var(--font-title); + font-weight: 600; + color: var(--text-dark); +} + +.notification-item .notification-text { /* Text snippet in dropdown */ + font-size: 0.8rem; + color: var(--text-medium); + margin-bottom: 5px; + line-height: 1.4; +} + +.notification-item .notification-time { + font-size: 0.75rem; + color: var(--text-light); +} + +.view-all { /* Footer link in dropdown */ + text-align: center; + font-weight: 600; + padding: 12px 15px; + display: block; + text-decoration: none; + color: var(--bright-orange); + background-color: #fffaf5; + transition: background-color 0.2s ease; +} +.view-all:hover { + background-color: var(--pale-yellow); +} + +.no-notifications { /* Message in empty dropdown */ + padding: 25px; + text-align: center; + color: var(--text-medium); + font-size: 0.9rem; +} + ================================================================================ File: ./app/static/css/inventory-logs.css ================================================================================ @@ -14024,6 +15490,112 @@ File: ./app/static/css/overdue_analysis.css } } +================================================================================ +File: ./app/static/css/announcement-detail.css +================================================================================ + +.announcement-detail-container { + padding: 20px; + max-width: 900px; + margin: 0 auto; +} + +.page-header { + margin-bottom: 25px; + position: relative; +} + +.back-link { + display: inline-block; + margin-bottom: 15px; + color: #6c757d; + text-decoration: none; + transition: color 0.2s; +} + +.back-link:hover { + color: #007bff; +} + +.page-header h1 { + margin-top: 0; + margin-bottom: 20px; + font-size: 2rem; + line-height: 1.3; +} + +.announcement-meta { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 25px; + padding: 15px; + background-color: #f8f9fa; + border-radius: 8px; +} + +.meta-item { + display: flex; + align-items: center; + font-size: 0.95rem; + color: #6c757d; +} + +.meta-item i { + margin-right: 8px; +} + +.meta-item.pinned { + color: #dc3545; + font-weight: 500; +} + +.announcement-content { + background-color: #fff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.05); + padding: 25px; + line-height: 1.7; + color: #333; +} + +/* 内容中的富文本样式 */ +.announcement-content h1, +.announcement-content h2, +.announcement-content h3 { + margin-top: 1.5em; + margin-bottom: 0.8em; +} + +.announcement-content p { + margin-bottom: 1em; +} + +.announcement-content img { + max-width: 100%; + height: auto; + border-radius: 4px; + margin: 15px 0; +} + +.announcement-content ul, +.announcement-content ol { + margin-bottom: 1em; + padding-left: 2em; +} + +.announcement-content a { + color: #007bff; +} + +.announcement-content blockquote { + border-left: 4px solid #e3e3e3; + padding-left: 15px; + color: #6c757d; + margin-left: 0; + margin-right: 0; +} + ================================================================================ File: ./app/static/js/book-import.js ================================================================================ @@ -14576,6 +16148,48 @@ document.addEventListener('DOMContentLoaded', function() { } }); +================================================================================ +File: ./app/static/js/announcement-form.js +================================================================================ + +// 公告编辑表单的Javascript +document.addEventListener('DOMContentLoaded', function() { + // 表单提交前验证 + document.getElementById('announcementForm').addEventListener('submit', function(e) { + // 由于富文本内容在各页面单独处理,这里仅做一些通用表单验证 + const title = document.getElementById('title').value.trim(); + + if (!title) { + e.preventDefault(); + alert('请输入公告标题'); + return false; + } + }); + + // 返回按钮处理 + const cancelButton = document.querySelector('button[type="button"]'); + if (cancelButton) { + cancelButton.addEventListener('click', function() { + // 如果有未保存内容,给出提示 + if (formHasChanges()) { + if (!confirm('表单有未保存的内容,确定要离开吗?')) { + return; + } + } + history.back(); + }); + } + + // 检测表单是否有变化 + function formHasChanges() { + // 这里可以添加逻辑来检测表单内容是否有变化 + // 简单实现:检查标题是否不为空 + const title = document.getElementById('title').value.trim(); + return title !== ''; + } +}); + + ================================================================================ File: ./app/static/js/user-edit.js ================================================================================ @@ -14696,7 +16310,7 @@ document.addEventListener('DOMContentLoaded', function() { // 显示加载状态 document.getElementById('ranking-table-body').innerHTML = ` - 加载中... +
正在打开书页...
`; @@ -14734,15 +16348,16 @@ document.addEventListener('DOMContentLoaded', function() { let tableHtml = ''; data.forEach((book, index) => { + // 给每个单元格添加适当的类名以匹配CSS tableHtml += ` ${index + 1} - + ${book.title} - ${book.title} - ${book.author} - ${book.borrow_count} + ${book.title} + ${book.author} + ${book.borrow_count} `; }); @@ -14773,8 +16388,8 @@ document.addEventListener('DOMContentLoaded', function() { datasets: [{ label: '借阅次数', data: borrowCounts, - backgroundColor: 'rgba(54, 162, 235, 0.6)', - borderColor: 'rgba(54, 162, 235, 1)', + backgroundColor: 'rgba(183, 110, 121, 0.6)', // 玫瑰金色调 + borderColor: 'rgba(140, 45, 90, 1)', // 浆果红 borderWidth: 1 }] }, @@ -14786,13 +16401,41 @@ document.addEventListener('DOMContentLoaded', function() { beginAtZero: true, title: { display: true, - text: '借阅次数' + text: '借阅次数', + font: { + family: "'Open Sans', sans-serif", + size: 13 + }, + color: '#5D5053' + }, + ticks: { + color: '#8A797C', + font: { + family: "'Open Sans', sans-serif" + } + }, + grid: { + color: 'rgba(211, 211, 211, 0.3)' } }, x: { title: { display: true, - text: '图书' + text: '图书', + font: { + family: "'Open Sans', sans-serif", + size: 13 + }, + color: '#5D5053' + }, + ticks: { + color: '#8A797C', + font: { + family: "'Open Sans', sans-serif" + } + }, + grid: { + display: false } } }, @@ -14802,7 +16445,13 @@ document.addEventListener('DOMContentLoaded', function() { }, title: { display: true, - text: '热门图书借阅排行' + text: '热门图书借阅排行', + font: { + family: "'Playfair Display', serif", + size: 16, + weight: 'bold' + }, + color: '#B76E79' } } } @@ -15722,6 +17371,80 @@ function showAlert(message, type) { }, 3000); } +================================================================================ +File: ./app/static/js/announcement-manage.js +================================================================================ + +// 公告管理页面的Javascript +document.addEventListener('DOMContentLoaded', function() { + // 提示框自动关闭 + setTimeout(function() { + const alerts = document.querySelectorAll('.alert-success, .alert-info'); + alerts.forEach(alert => { + const bsAlert = new bootstrap.Alert(alert); + bsAlert.close(); + }); + }, 5000); +}); + +// 更改公告状态(发布/撤销) +function changeStatus(announcementId, status) { + if (!confirm('确定要' + (status === 1 ? '发布' : '撤销') + '这条公告吗?')) { + return; + } + + fetch(`/announcement/status/${announcementId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ status: status }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert(data.message); + location.reload(); + } else { + alert('操作失败: ' + data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('发生错误,请重试'); + }); +} + +// 更改公告置顶状态 +function changeTopStatus(announcementId, isTop) { + if (!confirm('确定要' + (isTop ? '置顶' : '取消置顶') + '这条公告吗?')) { + return; + } + + fetch(`/announcement/top/${announcementId}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ is_top: isTop }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + alert(data.message); + location.reload(); + } else { + alert('操作失败: ' + data.message); + } + }) + .catch(error => { + console.error('Error:', error); + alert('发生错误,请重试'); + }); +} + ================================================================================ File: ./app/static/js/borrow_management.js ================================================================================ @@ -15992,7 +17715,12 @@ document.addEventListener('DOMContentLoaded', function() { // 调用API获取数据 fetch('/statistics/api/user-activity') - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) .then(data => { updateUserTable(data); updateUserChart(data); @@ -16004,13 +17732,22 @@ document.addEventListener('DOMContentLoaded', function() { 加载数据失败,请稍后重试 `; + // 也可以考虑清除或提示图表加载失败 + if (activityChart) { + activityChart.destroy(); + activityChart = null; + } + const canvas = document.getElementById('user-activity-chart'); + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除画布 + // 可以在画布上显示错误信息,但这比较复杂,通常表格的错误提示已足够 }); } function updateUserTable(data) { const tableBody = document.getElementById('user-table-body'); - if (data.length === 0) { + if (!data || data.length === 0) { // 增加了对 data 本身的检查 tableBody.innerHTML = ` 暂无数据 @@ -16025,8 +17762,8 @@ document.addEventListener('DOMContentLoaded', function() { tableHtml += ` ${index + 1} - ${user.username} - ${user.nickname} + ${user.username || 'N/A'} + ${user.nickname || 'N/A'} ${user.borrow_count} `; @@ -16039,18 +17776,27 @@ document.addEventListener('DOMContentLoaded', function() { // 销毁旧图表 if (activityChart) { activityChart.destroy(); + activityChart = null; // 确保旧实例被完全清除 } - if (data.length === 0) { + const canvas = document.getElementById('user-activity-chart'); + const ctx = canvas.getContext('2d'); + + if (!data || data.length === 0) { + // 如果没有数据,清除画布,避免显示旧图表或空白图表框架 + ctx.clearRect(0, 0, canvas.width, canvas.height); + // 也可以在这里显示 "暂无数据" 的文本到 canvas 上,如果需要 + // 例如: + // ctx.textAlign = 'center'; + // ctx.fillText('暂无图表数据', canvas.width / 2, canvas.height / 2); return; } // 准备图表数据 - const labels = data.map(user => user.nickname || user.username); + const labels = data.map(user => user.nickname || user.username || '未知用户'); const borrowCounts = data.map(user => user.borrow_count); // 创建图表 - const ctx = document.getElementById('user-activity-chart').getContext('2d'); activityChart = new Chart(ctx, { type: 'bar', data: { @@ -16077,17 +17823,22 @@ document.addEventListener('DOMContentLoaded', function() { x: { title: { display: true, - text: '用户' + text: '用户' // 这个标题现在应该有空间显示了 } } }, plugins: { legend: { - display: false + display: false // 保持不显示图例 }, title: { - display: true, - text: '最活跃用户排行' + display: true, // 如果HTML中已有h3标题,这里可以设为 false + text: '最活跃用户排行' // 这个是图表内部的标题 + } + }, + layout: { // <--- 这是添加的部分 + padding: { + bottom: 30 // 为X轴标题和标签留出足够的底部空间,可以根据实际显示效果调整此数值 } } } @@ -17097,58 +18848,98 @@ document.addEventListener('DOMContentLoaded', function() { let overdueRangeChart = null; let overdueStatusChart = null; - // 初始加载 + // Initial load loadOverdueStatistics(); function loadOverdueStatistics() { - // 调用API获取数据 + // Call API to get data fetch('/statistics/api/overdue-statistics') - .then(response => response.json()) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + return response.json(); + }) .then(data => { + if (!data) { + console.error('加载逾期统计数据失败: API返回空数据'); + // Optionally update UI to show error for cards + return; + } updateOverdueCards(data); updateOverdueRangeChart(data.overdue_ranges); updateOverdueStatusChart(data); }) .catch(error => { console.error('加载逾期统计数据失败:', error); + // Optionally update UI to show error for cards and charts + // For charts, you might want to clear them or show an error message + clearChart('overdue-range-chart'); + clearChart('overdue-status-chart'); }); } function updateOverdueCards(data) { - document.getElementById('total-borrows').querySelector('.card-value').textContent = data.total_borrows; - document.getElementById('current-overdue').querySelector('.card-value').textContent = data.current_overdue; - document.getElementById('returned-overdue').querySelector('.card-value').textContent = data.returned_overdue; - document.getElementById('overdue-rate').querySelector('.card-value').textContent = data.overdue_rate + '%'; + document.getElementById('total-borrows').querySelector('.card-value').textContent = data.total_borrows || 0; + document.getElementById('current-overdue').querySelector('.card-value').textContent = data.current_overdue || 0; + document.getElementById('returned-overdue').querySelector('.card-value').textContent = data.returned_overdue || 0; + const overdueRate = data.overdue_rate !== undefined ? data.overdue_rate : 0; + document.getElementById('overdue-rate').querySelector('.card-value').textContent = overdueRate + '%'; + } + + function clearChart(canvasId) { + const canvas = document.getElementById(canvasId); + if (canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + // Optionally display a message like "数据加载失败" or "暂无数据" + // ctx.textAlign = 'center'; + // ctx.fillText('数据加载失败', canvas.width / 2, canvas.height / 2); + } } function updateOverdueRangeChart(rangeData) { - // 销毁旧图表 + // Destroy old chart if (overdueRangeChart) { overdueRangeChart.destroy(); + overdueRangeChart = null; } + const canvas = document.getElementById('overdue-range-chart'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!rangeData || rangeData.length === 0) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + // Optionally display "暂无数据" + // ctx.textAlign = 'center'; + // ctx.fillText('暂无逾期时长数据', canvas.width / 2, canvas.height / 2); return; } - // 准备图表数据 + // Prepare chart data const labels = rangeData.map(item => item.range); const counts = rangeData.map(item => item.count); - // 创建图表 - const ctx = document.getElementById('overdue-range-chart').getContext('2d'); + // Create chart overdueRangeChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ - label: '逾期数量', + label: '逾期数量', // This will appear in the legend data: counts, backgroundColor: [ - 'rgba(255, 206, 86, 0.7)', - 'rgba(255, 159, 64, 0.7)', - 'rgba(255, 99, 132, 0.7)', - 'rgba(255, 0, 0, 0.7)' + 'rgba(255, 206, 86, 0.7)', // 1-7天 + 'rgba(255, 159, 64, 0.7)', // 8-14天 + 'rgba(255, 99, 132, 0.7)', // 15-30天 + 'rgba(230, 0, 0, 0.7)' // 30天以上 (made it darker red) + ], + borderColor: [ // Optional: Add border colors if you want them distinct + 'rgba(255, 206, 86, 1)', + 'rgba(255, 159, 64, 1)', + 'rgba(255, 99, 132, 1)', + 'rgba(230, 0, 0, 1)' ], borderWidth: 1 }] @@ -17163,12 +18954,27 @@ document.addEventListener('DOMContentLoaded', function() { display: true, text: '数量' } + }, + x: { + // No title for X-axis needed here based on current config } }, plugins: { - title: { - display: true, - text: '逾期时长分布' + title: { // Chart.js internal title + display: false, // Set to false to use HTML

title + // text: '逾期时长分布' // This would be the Chart.js title if display: true + }, + legend: { + display: true, // Show legend for '逾期数量' + position: 'top', + } + }, + layout: { + padding: { + left: 25, // Increased left padding for Y-axis title "数量" + bottom: 10, // Padding for X-axis labels + top: 10, // Padding for legend/top elements + right: 10 } } } @@ -17176,21 +18982,39 @@ document.addEventListener('DOMContentLoaded', function() { } function updateOverdueStatusChart(data) { - // 销毁旧图表 + // Destroy old chart if (overdueStatusChart) { overdueStatusChart.destroy(); + overdueStatusChart = null; } - // 准备图表数据 - const statusLabels = ['当前逾期', '历史逾期', '正常']; + const canvas = document.getElementById('overdue-status-chart'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + + const totalBorrows = data.total_borrows || 0; + const currentOverdue = data.current_overdue || 0; + const returnedOverdue = data.returned_overdue || 0; + + if (totalBorrows === 0 && currentOverdue === 0 && returnedOverdue === 0) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + // Optionally display "暂无数据" + // ctx.textAlign = 'center'; + // ctx.fillText('暂无借阅状态数据', canvas.width / 2, canvas.height / 2); + return; + } + + + // Prepare chart data + const statusLabels = ['当前逾期', '历史逾期 (已归还)', '正常在借/已还']; // Clarified labels + const normalCount = totalBorrows - currentOverdue - returnedOverdue; const statusData = [ - data.current_overdue, - data.returned_overdue, - data.total_borrows - data.current_overdue - data.returned_overdue + currentOverdue, + returnedOverdue, + normalCount < 0 ? 0 : normalCount // Ensure not negative ]; - // 创建图表 - const ctx = document.getElementById('overdue-status-chart').getContext('2d'); + // Create chart overdueStatusChart = new Chart(ctx, { type: 'pie', data: { @@ -17198,9 +19022,14 @@ document.addEventListener('DOMContentLoaded', function() { datasets: [{ data: statusData, backgroundColor: [ - 'rgba(255, 99, 132, 0.7)', - 'rgba(255, 206, 86, 0.7)', - 'rgba(75, 192, 192, 0.7)' + 'rgba(255, 99, 132, 0.7)', // 当前逾期 + 'rgba(255, 206, 86, 0.7)', // 历史逾期 + 'rgba(75, 192, 192, 0.7)' // 正常 + ], + borderColor: [ // Optional: Add border colors + 'rgba(255, 99, 132, 1)', + 'rgba(255, 206, 86, 1)', + 'rgba(75, 192, 192, 1)' ], borderWidth: 1 }] @@ -17209,16 +19038,48 @@ document.addEventListener('DOMContentLoaded', function() { responsive: true, maintainAspectRatio: false, plugins: { - title: { + title: { // Chart.js internal title + display: false, // Set to false to use HTML

title + // text: '借阅状态分布' // This would be the Chart.js title if display: true + }, + legend: { display: true, - text: '借阅状态分布' + position: 'top', // Pie chart legends are often at top or side } + }, + layout: { + padding: 20 // General padding for pie chart (can be object: {top: val, ...}) + // e.g., { top: 20, bottom: 20, left: 10, right: 10 } } } }); } }); +================================================================================ +File: ./app/static/js/announcement-list.js +================================================================================ + +// 通知公告列表页面的Javascript +document.addEventListener('DOMContentLoaded', function() { + // 平滑滚动到页面锚点 + const scrollToAnchor = (anchorId) => { + const element = document.getElementById(anchorId); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }; + + // 如果URL中有锚点,执行滚动 + if (window.location.hash) { + const anchorId = window.location.hash.substring(1); + setTimeout(() => scrollToAnchor(anchorId), 300); + } +}); + ================================================================================ File: ./app/static/js/statistics.js ================================================================================ @@ -18458,6 +20319,77 @@ document.addEventListener('DOMContentLoaded', function() { document.head.appendChild(style); }); +================================================================================ +File: ./app/static/js/notifications.js +================================================================================ + +document.addEventListener('DOMContentLoaded', function() { + // 标记通知为已读 + const markAsRead = (notificationId) => { + fetch(`/announcement/notification/${notificationId}/mark-read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // 更新UI,如移除未读标记 + const notificationCard = document.querySelector(`.notification-card[data-id="${notificationId}"]`); + if (notificationCard) { + notificationCard.classList.remove('unread'); + const unreadBadge = notificationCard.querySelector('.unread-badge'); + if (unreadBadge) { + unreadBadge.remove(); + } + } + + // 更新通知计数 + updateNotificationCount(); + } + }) + .catch(error => console.error('Error:', error)); + }; + + // 自动标记为已读 + const notificationCards = document.querySelectorAll('.notification-card.unread'); + notificationCards.forEach(card => { + const notificationId = card.dataset.id; + if (notificationId) { + // 当用户查看通知列表时自动标记为已读 + // 这可以是可选的功能,也可能需要用户点击后才标记 + // markAsRead(notificationId); + } + }); + + // 更新通知计数 + function updateNotificationCount() { + // 获取当前未读通知数 + fetch('/announcement/notifications/count') + .then(response => response.json()) + .then(data => { + const badge = document.querySelector('.notifications .badge'); + if (data.count > 0) { + if (badge) { + badge.textContent = data.count; + } else { + const newBadge = document.createElement('span'); + newBadge.className = 'badge'; + newBadge.textContent = data.count; + document.querySelector('.notifications').appendChild(newBadge); + } + } else { + if (badge) { + badge.remove(); + } + } + }) + .catch(error => console.error('Error:', error)); + } +}); + ================================================================================ File: ./app/static/js/borrow_statistics.js ================================================================================ @@ -18984,28 +20916,28 @@ File: ./app/templates/index.html

馆藏总量

-

8,567

+

{{ stats.total_books }}

注册用户

-

1,245

+

{{ stats.total_users }}

当前借阅

-

352

+

{{ stats.active_borrows }}

待还图书

-

{{ 5 }}

+

{{ stats.user_borrows if current_user.is_authenticated else 0 }}

@@ -19016,25 +20948,52 @@ File: ./app/templates/index.html

最新图书

- 查看全部 + 查看全部
- {% for i in range(4) %} -
-
- Book Cover -
-
-

示例图书标题

-

作者名

-
- 计算机 - 可借阅 + {% if latest_books %} + {% for book in latest_books %} +
+
+ {% if book.cover_url %} + {{ book.title }} 封面 + {% else %} + 无封面 + {% endif %} +
+
+

{{ book.title }}

+

{{ book.author }}

+
+ {{ book.category.name if book.category else '未分类' }} + + {{ '可借阅' if book.stock > 0 else '已借完' }} + +
+ + {{ '借阅' if book.stock > 0 else '详情' }} +
-
-
- {% endfor %} + {% endfor %} + {% else %} + {% for i in range(4) %} +
+
+ Book Cover +
+
+

示例图书标题

+

作者名

+
+ 计算机 + 可借阅 +
+ +
+
+ {% endfor %} + {% endif %}
@@ -19042,30 +21001,55 @@ File: ./app/templates/index.html

通知公告

- 查看全部 + 查看全部
-
-
-
-

关于五一假期图书馆开放时间调整的通知

-

五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。

-
- 2023-04-28 + {% if announcements %} + {% for announcement in announcements %} +
+
+
+

{{ announcement.title }}

+

{{ announcement.content|striptags|truncate(100) }}

+
+ {{ announcement.created_at.strftime('%Y-%m-%d') }} +
-
-
-
-
-

您有2本图书即将到期

-

《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。

-
- 2023-04-27 - + {% endfor %} + {% else %} +
+
+
+

暂无公告

+

目前没有任何通知公告

+
+ {{ now.strftime('%Y-%m-%d') }} +
-
+ {% endif %} + + {% if current_user.is_authenticated and user_notifications %} +
+
+
+

您有{{ user_notifications|length }}条未读通知

+

+ {% for notification in user_notifications %} + {% if loop.index <= 2 %} + {{ notification.title }}{% if not loop.last and loop.index < 2 %}、{% endif %} + {% endif %} + {% endfor %} + {% if user_notifications|length > 2 %}等{% endif %} +

+
+ {{ now.strftime('%Y-%m-%d') }} + 查看详情 +
+
+
+ {% endif %}
@@ -19074,25 +21058,48 @@ File: ./app/templates/index.html