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 = `
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 }}
五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。
- -《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。
- + {% endif %} + + {% if current_user.is_authenticated and user_notifications %} ++ {% 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 %} +
+ +
+ {% endif %}
+