index__new_feature
This commit is contained in:
parent
c75521becd
commit
5933289d6e
109
app/__init__.py
109
app/__init__.py
@ -7,9 +7,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()
|
||||
|
||||
|
||||
@ -54,6 +56,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():
|
||||
@ -124,9 +127,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):
|
||||
@ -138,8 +215,32 @@ def create_app(config=None):
|
||||
return Markup(s.replace('\n', '<br>'))
|
||||
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()}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,324 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from app.models.announcement import Announcement
|
||||
from app.models.log import Log
|
||||
from app.utils.auth import admin_required
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime
|
||||
from app.models.notification import Notification
|
||||
|
||||
# 创建蓝图
|
||||
announcement_bp = Blueprint('announcement', __name__)
|
||||
|
||||
|
||||
@announcement_bp.route('/list', methods=['GET'])
|
||||
def announcement_list():
|
||||
"""公告列表页面 - 所有用户可见"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 10
|
||||
|
||||
# 查询活跃的公告
|
||||
query = Announcement.query.filter_by(status=1).order_by(
|
||||
Announcement.is_top.desc(),
|
||||
Announcement.created_at.desc()
|
||||
)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
return render_template('announcement/list.html', pagination=pagination)
|
||||
|
||||
|
||||
@announcement_bp.route('/detail/<int:announcement_id>', methods=['GET'])
|
||||
def announcement_detail(announcement_id):
|
||||
"""公告详情页面"""
|
||||
announcement = Announcement.get_announcement_by_id(announcement_id)
|
||||
|
||||
if not announcement or announcement.status == 0:
|
||||
flash('公告不存在或已被删除', 'error')
|
||||
return redirect(url_for('announcement.announcement_list'))
|
||||
|
||||
return render_template('announcement/detail.html', announcement=announcement)
|
||||
|
||||
|
||||
@announcement_bp.route('/manage', methods=['GET'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def manage_announcements():
|
||||
"""管理员公告管理页面"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 10
|
||||
search = request.args.get('search', '')
|
||||
status = request.args.get('status', type=int)
|
||||
|
||||
# 构建查询
|
||||
query = Announcement.query
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
query = query.filter(Announcement.title.like(f'%{search}%'))
|
||||
|
||||
# 状态过滤
|
||||
if status is not None:
|
||||
query = query.filter(Announcement.status == status)
|
||||
|
||||
# 排序
|
||||
query = query.order_by(
|
||||
Announcement.is_top.desc(),
|
||||
Announcement.created_at.desc()
|
||||
)
|
||||
|
||||
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
|
||||
# 记录访问日志
|
||||
Log.add_log(
|
||||
action="访问公告管理",
|
||||
user_id=current_user.id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f"管理员 {current_user.username} 访问公告管理页面"
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'announcement/manage.html',
|
||||
pagination=pagination,
|
||||
search=search,
|
||||
status=status
|
||||
)
|
||||
|
||||
|
||||
@announcement_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def add_announcement():
|
||||
"""添加公告"""
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
content = request.form.get('content')
|
||||
is_top = request.form.get('is_top') == 'on'
|
||||
|
||||
if not title or not content:
|
||||
flash('标题和内容不能为空', 'error')
|
||||
return render_template('announcement/add.html')
|
||||
|
||||
success, result = Announcement.create_announcement(
|
||||
title=title,
|
||||
content=content,
|
||||
publisher_id=current_user.id,
|
||||
is_top=is_top
|
||||
)
|
||||
|
||||
if success:
|
||||
# 记录操作日志
|
||||
Log.add_log(
|
||||
action="添加公告",
|
||||
user_id=current_user.id,
|
||||
target_type="公告",
|
||||
target_id=result.id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f"管理员 {current_user.username} 添加了新公告: {title}"
|
||||
)
|
||||
|
||||
flash('公告发布成功', 'success')
|
||||
return redirect(url_for('announcement.manage_announcements'))
|
||||
else:
|
||||
flash(f'公告发布失败: {result}', 'error')
|
||||
return render_template('announcement/add.html')
|
||||
|
||||
return render_template('announcement/add.html')
|
||||
|
||||
|
||||
@announcement_bp.route('/edit/<int:announcement_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def edit_announcement(announcement_id):
|
||||
"""编辑公告"""
|
||||
announcement = Announcement.get_announcement_by_id(announcement_id)
|
||||
|
||||
if not announcement:
|
||||
flash('公告不存在', 'error')
|
||||
return redirect(url_for('announcement.manage_announcements'))
|
||||
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
content = request.form.get('content')
|
||||
is_top = request.form.get('is_top') == 'on'
|
||||
|
||||
if not title or not content:
|
||||
flash('标题和内容不能为空', 'error')
|
||||
return render_template('announcement/edit.html', announcement=announcement)
|
||||
|
||||
success, result = Announcement.update_announcement(
|
||||
announcement_id=announcement_id,
|
||||
title=title,
|
||||
content=content,
|
||||
is_top=is_top
|
||||
)
|
||||
|
||||
if success:
|
||||
# 记录操作日志
|
||||
Log.add_log(
|
||||
action="编辑公告",
|
||||
user_id=current_user.id,
|
||||
target_type="公告",
|
||||
target_id=announcement_id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f"管理员 {current_user.username} 编辑了公告: {title}"
|
||||
)
|
||||
|
||||
flash('公告更新成功', 'success')
|
||||
return redirect(url_for('announcement.manage_announcements'))
|
||||
else:
|
||||
flash(f'公告更新失败: {result}', 'error')
|
||||
return render_template('announcement/edit.html', announcement=announcement)
|
||||
|
||||
return render_template('announcement/edit.html', announcement=announcement)
|
||||
|
||||
|
||||
@announcement_bp.route('/status/<int:announcement_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def change_status(announcement_id):
|
||||
"""更改公告状态"""
|
||||
data = request.get_json()
|
||||
status = data.get('status')
|
||||
|
||||
if status is None or status not in [0, 1]:
|
||||
return jsonify({'success': False, 'message': '无效的状态值'})
|
||||
|
||||
# 查询公告获取标题(用于日志)
|
||||
announcement = Announcement.get_announcement_by_id(announcement_id)
|
||||
if not announcement:
|
||||
return jsonify({'success': False, 'message': '公告不存在'})
|
||||
|
||||
success, message = Announcement.change_status(announcement_id, status)
|
||||
|
||||
if success:
|
||||
# 记录状态变更日志
|
||||
status_text = "发布" if status == 1 else "撤销"
|
||||
Log.add_log(
|
||||
action=f"公告{status_text}",
|
||||
user_id=current_user.id,
|
||||
target_type="公告",
|
||||
target_id=announcement_id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f"管理员 {current_user.username} {status_text}公告: {announcement.title}"
|
||||
)
|
||||
|
||||
return jsonify({'success': True, 'message': f'公告已{status_text}'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': message})
|
||||
|
||||
|
||||
@announcement_bp.route('/top/<int:announcement_id>', methods=['POST'])
|
||||
@login_required
|
||||
@admin_required
|
||||
def change_top_status(announcement_id):
|
||||
"""更改公告置顶状态"""
|
||||
data = request.get_json()
|
||||
is_top = data.get('is_top')
|
||||
|
||||
if is_top is None:
|
||||
return jsonify({'success': False, 'message': '无效的置顶状态'})
|
||||
|
||||
# 查询公告获取标题(用于日志)
|
||||
announcement = Announcement.get_announcement_by_id(announcement_id)
|
||||
if not announcement:
|
||||
return jsonify({'success': False, 'message': '公告不存在'})
|
||||
|
||||
success, message = Announcement.change_top_status(announcement_id, is_top)
|
||||
|
||||
if success:
|
||||
# 记录置顶状态变更日志
|
||||
action_text = "置顶" if is_top else "取消置顶"
|
||||
Log.add_log(
|
||||
action=f"公告{action_text}",
|
||||
user_id=current_user.id,
|
||||
target_type="公告",
|
||||
target_id=announcement_id,
|
||||
ip_address=request.remote_addr,
|
||||
description=f"管理员 {current_user.username} {action_text}公告: {announcement.title}"
|
||||
)
|
||||
|
||||
return jsonify({'success': True, 'message': f'公告已{action_text}'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': message})
|
||||
|
||||
|
||||
@announcement_bp.route('/latest', methods=['GET'])
|
||||
def get_latest_announcements():
|
||||
"""获取最新公告列表,用于首页和API"""
|
||||
limit = request.args.get('limit', 5, type=int)
|
||||
announcements = Announcement.get_active_announcements(limit=limit)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'announcements': [announcement.to_dict() for announcement in announcements]
|
||||
})
|
||||
|
||||
|
||||
@announcement_bp.route('/notifications')
|
||||
@login_required
|
||||
def user_notifications():
|
||||
"""用户个人通知列表页面"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 10
|
||||
unread_only = request.args.get('unread_only') == '1'
|
||||
|
||||
pagination = Notification.get_user_notifications(
|
||||
user_id=current_user.id,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
unread_only=unread_only
|
||||
)
|
||||
|
||||
return render_template(
|
||||
'announcement/notifications.html',
|
||||
pagination=pagination,
|
||||
unread_only=unread_only
|
||||
)
|
||||
|
||||
|
||||
@announcement_bp.route('/notification/<int:notification_id>')
|
||||
@login_required
|
||||
def view_notification(notification_id):
|
||||
"""查看单条通知"""
|
||||
notification = Notification.query.get_or_404(notification_id)
|
||||
|
||||
# 检查权限 - 只能查看自己的通知
|
||||
if notification.user_id != current_user.id:
|
||||
flash('您无权查看此通知', 'error')
|
||||
return redirect(url_for('announcement.user_notifications'))
|
||||
|
||||
# 标记为已读
|
||||
if notification.status == 0:
|
||||
Notification.mark_as_read(notification_id, current_user.id)
|
||||
|
||||
# 如果是借阅类型的通知,可能需要跳转到相关页面
|
||||
if notification.type == 'borrow' and 'borrow_id' in notification.content:
|
||||
# 这里可以解析content获取borrow_id然后重定向
|
||||
pass
|
||||
|
||||
return render_template('announcement/notification_detail.html', notification=notification)
|
||||
|
||||
|
||||
@announcement_bp.route('/notifications/mark-all-read')
|
||||
@login_required
|
||||
def mark_all_as_read():
|
||||
"""标记所有通知为已读"""
|
||||
try:
|
||||
# 获取所有未读通知
|
||||
unread_notifications = Notification.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
status=0
|
||||
).all()
|
||||
|
||||
# 标记为已读
|
||||
for notification in unread_notifications:
|
||||
notification.status = 1
|
||||
notification.read_at = datetime.now()
|
||||
|
||||
db.session.commit()
|
||||
flash('所有通知已标记为已读', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'操作失败: {str(e)}', 'error')
|
||||
|
||||
return redirect(url_for('announcement.user_notifications'))
|
||||
@ -1,8 +1,8 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, g, jsonify
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app, jsonify
|
||||
from app.models.book import Book, Category
|
||||
from app.models.user import db
|
||||
from app.utils.auth import login_required, admin_required
|
||||
from flask_login import current_user, login_required
|
||||
from flask_login import current_user # 移除重复的 login_required 导入
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
import datetime
|
||||
@ -62,7 +62,7 @@ def admin_book_list():
|
||||
category_id=category_id,
|
||||
sort=sort,
|
||||
order=order,
|
||||
current_user=g.user,
|
||||
current_user=current_user, # 使用current_user替代g.user
|
||||
is_admin_view=True) # 指明这是管理视图
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ def book_list():
|
||||
category_id=category_id,
|
||||
sort=sort,
|
||||
order=order,
|
||||
current_user=g.user)
|
||||
current_user=current_user) # 使用current_user替代g.user
|
||||
|
||||
|
||||
# 图书详情页面
|
||||
@ -136,8 +136,8 @@ def book_detail(book_id):
|
||||
|
||||
# 如果用户是管理员,预先查询并排序借阅记录
|
||||
borrow_records = []
|
||||
# 防御性编程:确保 g.user 存在且有 role_id 属性
|
||||
if hasattr(g, 'user') and g.user is not None and hasattr(g.user, 'role_id') and g.user.role_id == 1:
|
||||
# 使用current_user代替g.user
|
||||
if current_user.is_authenticated and current_user.role_id == 1:
|
||||
from app.models.borrow import BorrowRecord
|
||||
borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit(
|
||||
10).all()
|
||||
@ -155,7 +155,7 @@ def book_detail(book_id):
|
||||
return render_template(
|
||||
'book/detail.html',
|
||||
book=book,
|
||||
current_user=current_user, # 使用 flask_login 的 current_user 而不是 g.user
|
||||
current_user=current_user,
|
||||
borrow_records=borrow_records,
|
||||
now=now
|
||||
)
|
||||
@ -209,7 +209,7 @@ def add_book():
|
||||
'price': price
|
||||
}
|
||||
return render_template('book/add.html', categories=categories,
|
||||
current_user=g.user, book=book_data)
|
||||
current_user=current_user, book=book_data)
|
||||
|
||||
# 处理封面图片上传
|
||||
cover_url = None
|
||||
@ -269,7 +269,7 @@ def add_book():
|
||||
change_type='入库',
|
||||
change_amount=stock,
|
||||
after_stock=stock,
|
||||
operator_id=g.user.id,
|
||||
operator_id=current_user.id, # 使用current_user.id替代g.user.id
|
||||
remark='新书入库',
|
||||
changed_at=datetime.datetime.now()
|
||||
)
|
||||
@ -320,10 +320,10 @@ def add_book():
|
||||
'price': price
|
||||
}
|
||||
return render_template('book/add.html', categories=categories,
|
||||
current_user=g.user, book=book_data)
|
||||
current_user=current_user, book=book_data)
|
||||
|
||||
categories = Category.query.all()
|
||||
return render_template('book/add.html', categories=categories, current_user=g.user)
|
||||
return render_template('book/add.html', categories=categories, current_user=current_user)
|
||||
|
||||
|
||||
# 编辑图书
|
||||
@ -350,7 +350,7 @@ def edit_book(book_id):
|
||||
if not title or not author:
|
||||
flash('书名和作者不能为空', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# ISBN验证
|
||||
if isbn and isbn.strip(): # 确保ISBN不是空字符串
|
||||
@ -361,7 +361,7 @@ def edit_book(book_id):
|
||||
if len(clean_isbn) != 10 and len(clean_isbn) != 13:
|
||||
flash('ISBN必须是10位或13位', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# ISBN-10验证
|
||||
if len(clean_isbn) == 10:
|
||||
@ -369,13 +369,13 @@ def edit_book(book_id):
|
||||
if not clean_isbn[:9].isdigit():
|
||||
flash('ISBN-10的前9位必须是数字', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# 检查最后一位是否为数字或'X'
|
||||
if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'):
|
||||
flash('ISBN-10的最后一位必须是数字或X', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# 校验和验证
|
||||
sum = 0
|
||||
@ -388,7 +388,7 @@ def edit_book(book_id):
|
||||
if sum % 11 != 0:
|
||||
flash('ISBN-10校验和无效', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# ISBN-13验证
|
||||
if len(clean_isbn) == 13:
|
||||
@ -396,7 +396,7 @@ def edit_book(book_id):
|
||||
if not clean_isbn.isdigit():
|
||||
flash('ISBN-13必须全是数字', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# 校验和验证
|
||||
sum = 0
|
||||
@ -408,7 +408,7 @@ def edit_book(book_id):
|
||||
if check_digit != int(clean_isbn[12]):
|
||||
flash('ISBN-13校验和无效', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# 处理库存变更
|
||||
new_stock = request.form.get('stock', type=int) or 0 # 默认为0而非None
|
||||
@ -422,7 +422,7 @@ def edit_book(book_id):
|
||||
change_type=change_type,
|
||||
change_amount=abs(change_amount),
|
||||
after_stock=new_stock,
|
||||
operator_id=g.user.id,
|
||||
operator_id=current_user.id, # 使用current_user.id替代g.user.id
|
||||
remark=f'管理员编辑图书库存 - {book.title}',
|
||||
changed_at=datetime.datetime.now()
|
||||
)
|
||||
@ -490,11 +490,11 @@ def edit_book(book_id):
|
||||
|
||||
flash(f'保存失败: {str(e)}', 'danger')
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
# GET 请求
|
||||
categories = Category.query.all()
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=g.user)
|
||||
return render_template('book/edit.html', book=book, categories=categories, current_user=current_user)
|
||||
|
||||
|
||||
# 删除图书
|
||||
@ -553,7 +553,7 @@ def category_list():
|
||||
description="访问图书分类管理页面"
|
||||
)
|
||||
|
||||
return render_template('book/categories.html', categories=categories, current_user=g.user)
|
||||
return render_template('book/categories.html', categories=categories, current_user=current_user)
|
||||
|
||||
|
||||
# 添加分类
|
||||
@ -741,7 +741,7 @@ def import_books():
|
||||
change_type='入库',
|
||||
change_amount=book.stock,
|
||||
after_stock=book.stock,
|
||||
operator_id=g.user.id,
|
||||
operator_id=current_user.id, # 使用current_user.id
|
||||
remark='批量导入图书',
|
||||
changed_at=datetime.datetime.now()
|
||||
)
|
||||
@ -784,7 +784,7 @@ def import_books():
|
||||
flash('只支持Excel文件(.xlsx, .xls)', 'danger')
|
||||
return redirect(request.url)
|
||||
|
||||
return render_template('book/import.html', current_user=g.user)
|
||||
return render_template('book/import.html', current_user=current_user) # 使用current_user
|
||||
|
||||
|
||||
# 导出图书
|
||||
@ -927,7 +927,7 @@ def browse_books():
|
||||
categories=categories,
|
||||
category_id=category_id,
|
||||
sort=sort,
|
||||
order=order, )
|
||||
order=order) # current_user自动传递到模板
|
||||
|
||||
|
||||
@book_bp.route('/template/download')
|
||||
@ -1016,4 +1016,3 @@ def download_template():
|
||||
as_attachment=True,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
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)
|
||||
@ -0,0 +1,115 @@
|
||||
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
|
||||
101
app/static/css/announcement-detail.css
Normal file
101
app/static/css/announcement-detail.css
Normal file
@ -0,0 +1,101 @@
|
||||
.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;
|
||||
}
|
||||
58
app/static/css/announcement-form.css
Normal file
58
app/static/css/announcement-form.css
Normal file
@ -0,0 +1,58 @@
|
||||
.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;
|
||||
}
|
||||
130
app/static/css/announcement-list.css
Normal file
130
app/static/css/announcement-list.css
Normal file
@ -0,0 +1,130 @@
|
||||
.announcement-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 25px;
|
||||
border-bottom: 1px solid #e3e3e3;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.announcement-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.announcement-item {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.announcement-item:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.announcement-item.pinned {
|
||||
border-left: 4px solid #dc3545;
|
||||
background-color: #fff9f9;
|
||||
}
|
||||
|
||||
.pin-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 0 8px 0 8px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.announcement-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.announcement-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.announcement-header h3 a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.announcement-header h3 a:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.announcement-preview {
|
||||
margin: 15px 0;
|
||||
color: #495057;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.announcement-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.publisher {
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.read-more {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.read-more i {
|
||||
margin-left: 5px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.read-more:hover i {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
83
app/static/css/announcement-manage.css
Normal file
83
app/static/css/announcement-manage.css
Normal file
@ -0,0 +1,83 @@
|
||||
.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;
|
||||
}
|
||||
@ -104,7 +104,8 @@
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: block; /* 修改为block以占据整个单元格 */
|
||||
text-align: center; /* 确保文本居中 */
|
||||
}
|
||||
|
||||
.data-table .borrow-count:after {
|
||||
@ -247,34 +248,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;
|
||||
|
||||
@ -533,25 +533,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 {
|
||||
@ -559,6 +569,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 {
|
||||
|
||||
188
app/static/css/notifications.css
Normal file
188
app/static/css/notifications.css
Normal file
@ -0,0 +1,188 @@
|
||||
.notifications-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;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 10px 20px;
|
||||
color: #495057;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: #007bff;
|
||||
border-bottom-color: #007bff;
|
||||
}
|
||||
|
||||
.notifications-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.notification-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.notification-card.unread {
|
||||
border-left: 4px solid #007bff;
|
||||
background-color: #f8fbff;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.notification-title a {
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notification-title a:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
color: #495057;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.notification-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #6c757d;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.notification-type {
|
||||
background-color: #f1f1f1;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* 通知下拉菜单样式 */
|
||||
.notification-dropdown {
|
||||
width: 320px;
|
||||
padding: 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.mark-all-read {
|
||||
font-size: 0.8rem;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.notification-items {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
padding: 10px 15px;
|
||||
border-bottom: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.notification-item.unread {
|
||||
background-color: #f8fbff;
|
||||
}
|
||||
|
||||
.notification-content h6 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-notifications {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
37
app/static/js/announcement-form.js
Normal file
37
app/static/js/announcement-form.js
Normal file
@ -0,0 +1,37 @@
|
||||
// 公告编辑表单的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 !== '';
|
||||
}
|
||||
});
|
||||
|
||||
19
app/static/js/announcement-list.js
Normal file
19
app/static/js/announcement-list.js
Normal file
@ -0,0 +1,19 @@
|
||||
// 通知公告列表页面的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);
|
||||
}
|
||||
});
|
||||
69
app/static/js/announcement-manage.js
Normal file
69
app/static/js/announcement-manage.js
Normal file
@ -0,0 +1,69 @@
|
||||
// 公告管理页面的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('发生错误,请重试');
|
||||
});
|
||||
}
|
||||
@ -18,7 +18,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
// 显示加载状态
|
||||
document.getElementById('ranking-table-body').innerHTML = `
|
||||
<tr class="loading-row">
|
||||
<td colspan="5">加载中...</td>
|
||||
<td colspan="5"><div class="loading-animation"><span>正在打开书页</span><span class="dot-animation">...</span></div></td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
@ -56,15 +56,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let tableHtml = '';
|
||||
|
||||
data.forEach((book, index) => {
|
||||
// 给每个单元格添加适当的类名以匹配CSS
|
||||
tableHtml += `
|
||||
<tr>
|
||||
<td class="rank">${index + 1}</td>
|
||||
<td>
|
||||
<td class="book-cover">
|
||||
<img src="${book.cover_url || '/static/images/book-placeholder.jpg'}" alt="${book.title}">
|
||||
</td>
|
||||
<td>${book.title}</td>
|
||||
<td>${book.author}</td>
|
||||
<td class="borrow-count">${book.borrow_count}</td>
|
||||
<td class="book-title-cell"><span class="book-title">${book.title}</span></td>
|
||||
<td class="author">${book.author}</td>
|
||||
<td><span class="borrow-count">${book.borrow_count}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
@ -95,8 +96,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
|
||||
}]
|
||||
},
|
||||
@ -108,13 +109,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
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -124,7 +153,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '热门图书借阅排行'
|
||||
text: '热门图书借阅排行',
|
||||
font: {
|
||||
family: "'Playfair Display', serif",
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
},
|
||||
color: '#B76E79'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
66
app/static/js/notifications.js
Normal file
66
app/static/js/notifications.js
Normal file
@ -0,0 +1,66 @@
|
||||
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));
|
||||
}
|
||||
});
|
||||
@ -3,58 +3,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
|
||||
}]
|
||||
@ -69,12 +109,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 <h3> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,21 +137,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: {
|
||||
@ -104,9 +177,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
|
||||
}]
|
||||
@ -115,10 +193,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
title: {
|
||||
title: { // Chart.js internal title
|
||||
display: false, // Set to false to use HTML <h3> 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 }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -15,7 +15,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);
|
||||
@ -27,13 +32,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<td colspan="4">加载数据失败,请稍后重试</td>
|
||||
</tr>
|
||||
`;
|
||||
// 也可以考虑清除或提示图表加载失败
|
||||
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 = `
|
||||
<tr class="no-data-row">
|
||||
<td colspan="4">暂无数据</td>
|
||||
@ -48,8 +62,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
tableHtml += `
|
||||
<tr>
|
||||
<td class="rank">${index + 1}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.nickname}</td>
|
||||
<td>${user.username || 'N/A'}</td>
|
||||
<td>${user.nickname || 'N/A'}</td>
|
||||
<td class="borrow-count">${user.borrow_count}</td>
|
||||
</tr>
|
||||
`;
|
||||
@ -62,18 +76,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: {
|
||||
@ -100,17 +123,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轴标题和标签留出足够的底部空间,可以根据实际显示效果调整此数值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
app/templates/announcement/add.html
Normal file
91
app/templates/announcement/add.html
Normal file
@ -0,0 +1,91 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}添加公告 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-form.css') }}">
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="announcement-form-container">
|
||||
<div class="page-header">
|
||||
<h1>添加公告</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('announcement.add_announcement') }}" id="announcementForm">
|
||||
<div class="form-group">
|
||||
<label for="title">公告标题 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editor">公告内容 <span class="text-danger">*</span></label>
|
||||
<div id="editor" style="height: 250px;"></div>
|
||||
<input type="hidden" name="content" id="content">
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_top" name="is_top">
|
||||
<label class="form-check-label" for="is_top">置顶公告</label>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons">
|
||||
<button type="button" class="btn btn-secondary" onclick="history.back()">取消</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">发布公告</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/announcement-form.js') }}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化富文本编辑器
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
},
|
||||
placeholder: '请输入公告内容...'
|
||||
});
|
||||
|
||||
// 提交表单前获取富文本内容
|
||||
document.getElementById('announcementForm').addEventListener('submit', function(e) {
|
||||
const content = document.getElementById('content');
|
||||
content.value = quill.root.innerHTML;
|
||||
|
||||
// 简单验证
|
||||
if (!document.getElementById('title').value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('请输入公告标题');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (quill.getText().trim().length <= 1) {
|
||||
e.preventDefault();
|
||||
alert('请输入公告内容');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
36
app/templates/announcement/detail.html
Normal file
36
app/templates/announcement/detail.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}{{ announcement.title }} - 通知公告{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-detail.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="announcement-detail-container">
|
||||
<div class="page-header">
|
||||
<a href="{{ url_for('announcement.announcement_list') }}" class="back-link">
|
||||
<i class="fas fa-arrow-left"></i> 返回公告列表
|
||||
</a>
|
||||
<h1>{{ announcement.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="announcement-meta">
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-user"></i> 发布者: {{ announcement.publisher.username if announcement.publisher else '系统' }}
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<i class="fas fa-calendar-alt"></i> 发布时间: {{ announcement.created_at.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
</div>
|
||||
{% if announcement.is_top %}
|
||||
<div class="meta-item pinned">
|
||||
<i class="fas fa-thumbtack"></i> 置顶公告
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="announcement-content">
|
||||
{{ announcement.content|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
94
app/templates/announcement/edit.html
Normal file
94
app/templates/announcement/edit.html
Normal file
@ -0,0 +1,94 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}编辑公告 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-form.css') }}">
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="announcement-form-container">
|
||||
<div class="page-header">
|
||||
<h1>编辑公告</h1>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('announcement.edit_announcement', announcement_id=announcement.id) }}" id="announcementForm">
|
||||
<div class="form-group">
|
||||
<label for="title">公告标题 <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{ announcement.title }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="editor">公告内容 <span class="text-danger">*</span></label>
|
||||
<div id="editor" style="height: 250px;"></div>
|
||||
<input type="hidden" name="content" id="content">
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_top" name="is_top" {% if announcement.is_top %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_top">置顶公告</label>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons">
|
||||
<button type="button" class="btn btn-secondary" onclick="history.back()">取消</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">更新公告</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/announcement-form.js') }}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化富文本编辑器
|
||||
const quill = new Quill('#editor', {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'align': [] }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
},
|
||||
placeholder: '请输入公告内容...'
|
||||
});
|
||||
|
||||
// 加载现有内容
|
||||
quill.root.innerHTML = `{{ announcement.content|safe }}`;
|
||||
|
||||
// 提交表单前获取富文本内容
|
||||
document.getElementById('announcementForm').addEventListener('submit', function(e) {
|
||||
const content = document.getElementById('content');
|
||||
content.value = quill.root.innerHTML;
|
||||
|
||||
// 简单验证
|
||||
if (!document.getElementById('title').value.trim()) {
|
||||
e.preventDefault();
|
||||
alert('请输入公告标题');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (quill.getText().trim().length <= 1) {
|
||||
e.preventDefault();
|
||||
alert('请输入公告内容');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
104
app/templates/announcement/list.html
Normal file
104
app/templates/announcement/list.html
Normal file
@ -0,0 +1,104 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}通知公告 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-list.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="announcement-container">
|
||||
<div class="page-header">
|
||||
<h1>通知公告</h1>
|
||||
</div>
|
||||
|
||||
<div class="announcement-list">
|
||||
{% if pagination.items %}
|
||||
{% for announcement in pagination.items %}
|
||||
<div class="announcement-item {% if announcement.is_top %}pinned{% endif %}">
|
||||
{% if announcement.is_top %}
|
||||
<div class="pin-badge">
|
||||
<i class="fas fa-thumbtack"></i> 置顶
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="announcement-header">
|
||||
<h3><a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}">{{ announcement.title }}</a></h3>
|
||||
<span class="date">{{ announcement.created_at }}</span>
|
||||
</div>
|
||||
<div class="announcement-preview">
|
||||
{{ announcement.content|striptags|truncate(150) }}
|
||||
</div>
|
||||
<div class="announcement-footer">
|
||||
<span class="publisher">发布者: {{ announcement.publisher.username if announcement.publisher else '系统' }}</span>
|
||||
<a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}" class="read-more">阅读更多 <i class="fas fa-arrow-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
{% if pagination.pages > 1 %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.announcement_list', page=pagination.prev_num) }}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if page_num == pagination.page %}
|
||||
<li class="page-item active">
|
||||
<a class="page-link" href="{{ url_for('announcement.announcement_list', page=page_num) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.announcement_list', page=page_num) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#">...</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.announcement_list', page=pagination.next_num) }}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-records">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<p>目前没有通知公告</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/announcement-list.js') }}"></script>
|
||||
{% endblock %}
|
||||
163
app/templates/announcement/manage.html
Normal file
163
app/templates/announcement/manage.html
Normal file
@ -0,0 +1,163 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}公告管理 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/announcement-manage.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="announcement-manage-container">
|
||||
<div class="page-header">
|
||||
<h1>公告管理</h1>
|
||||
<a href="{{ url_for('announcement.add_announcement') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> 添加公告
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="filter-container">
|
||||
<form action="{{ url_for('announcement.manage_announcements') }}" method="get" class="filter-form">
|
||||
<div class="form-group">
|
||||
<input type="text" name="search" class="form-control" placeholder="搜索公告标题..." value="{{ search }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="status" class="form-control">
|
||||
<option value="">全部状态</option>
|
||||
<option value="1" {% if status == 1 %}selected{% endif %}>已发布</option>
|
||||
<option value="0" {% if status == 0 %}selected{% endif %}>已撤销</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-secondary">
|
||||
<i class="fas fa-search"></i> 搜索
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 公告列表 -->
|
||||
<div class="announcement-list">
|
||||
{% if pagination.items %}
|
||||
<table class="table announcement-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>标题</th>
|
||||
<th>发布者</th>
|
||||
<th>发布时间</th>
|
||||
<th>最后更新</th>
|
||||
<th>状态</th>
|
||||
<th>置顶</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for announcement in pagination.items %}
|
||||
<tr>
|
||||
<td>{{ announcement.id }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}" target="_blank" class="announcement-title">
|
||||
{{ announcement.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ announcement.publisher.username if announcement.publisher else '系统' }}</td>
|
||||
<td>{{ announcement.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>{{ announcement.updated_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ 'success' if announcement.status == 1 else 'secondary' }}">
|
||||
{{ '已发布' if announcement.status == 1 else '已撤销' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ 'primary' if announcement.is_top else 'light' }}">
|
||||
{{ '已置顶' if announcement.is_top else '未置顶' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{{ url_for('announcement.edit_announcement', announcement_id=announcement.id) }}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-edit"></i> 编辑
|
||||
</a>
|
||||
<button class="btn btn-sm btn-{{ 'warning' if announcement.status == 1 else 'success' }}"
|
||||
onclick="changeStatus({{ announcement.id }}, {{ 0 if announcement.status == 1 else 1 }})">
|
||||
<i class="fas fa-{{ 'times' if announcement.status == 1 else 'check' }}"></i>
|
||||
{{ '撤销' if announcement.status == 1 else '发布' }}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-{{ 'secondary' if announcement.is_top else 'primary' }}"
|
||||
onclick="changeTopStatus({{ announcement.id }}, {{ 'false' if announcement.is_top else 'true' }})">
|
||||
<i class="fas fa-thumbtack"></i>
|
||||
{{ '取消置顶' if announcement.is_top else '置顶' }}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
{% if pagination.pages > 1 %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.manage_announcements', page=pagination.prev_num, search=search, status=status) }}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if page_num == pagination.page %}
|
||||
<li class="page-item active">
|
||||
<a class="page-link" href="{{ url_for('announcement.manage_announcements', page=page_num, search=search, status=status) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.manage_announcements', page=page_num, search=search, status=status) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#">...</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.manage_announcements', page=pagination.next_num, search=search, status=status) }}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-records">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<p>没有找到符合条件的公告</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/announcement-manage.js') }}"></script>
|
||||
{% endblock %}
|
||||
113
app/templates/announcement/notifications.html
Normal file
113
app/templates/announcement/notifications.html
Normal file
@ -0,0 +1,113 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %}我的通知 - 图书管理系统{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/notifications.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="notifications-container">
|
||||
<div class="page-header">
|
||||
<h1>我的通知</h1>
|
||||
<div class="notification-actions">
|
||||
{% if pagination.items and not (unread_only and pagination.total == 0) %}
|
||||
<a href="{{ url_for('announcement.mark_all_as_read') }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-check-double"></i> 全部标为已读
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-tabs">
|
||||
<a href="{{ url_for('announcement.user_notifications') }}" class="filter-tab {% if not unread_only %}active{% endif %}">所有通知</a>
|
||||
<a href="{{ url_for('announcement.user_notifications', unread_only=1) }}" class="filter-tab {% if unread_only %}active{% endif %}">未读通知</a>
|
||||
</div>
|
||||
|
||||
<div class="notifications-list">
|
||||
{% if pagination.items %}
|
||||
{% for notification in pagination.items %}
|
||||
<div class="notification-card {% if notification.status == 0 %}unread{% endif %}">
|
||||
<div class="notification-content">
|
||||
<h3 class="notification-title">
|
||||
<a href="{{ url_for('announcement.view_notification', notification_id=notification.id) }}">{{ notification.title }}</a>
|
||||
{% if notification.status == 0 %}
|
||||
<span class="unread-badge">未读</span>
|
||||
{% endif %}
|
||||
</h3>
|
||||
<div class="notification-text">{{ notification.content|striptags|truncate(150) }}</div>
|
||||
<div class="notification-meta">
|
||||
<span class="notification-type">{{ notification.type }}</span>
|
||||
<span class="notification-time">{{ notification.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
{% if pagination.pages > 1 %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.user_notifications', page=pagination.prev_num, unread_only=1 if unread_only else 0) }}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
|
||||
{% if page_num %}
|
||||
{% if page_num == pagination.page %}
|
||||
<li class="page-item active">
|
||||
<a class="page-link" href="{{ url_for('announcement.user_notifications', page=page_num, unread_only=1 if unread_only else 0) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.user_notifications', page=page_num, unread_only=1 if unread_only else 0) }}">{{ page_num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#">...</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ url_for('announcement.user_notifications', page=pagination.next_num, unread_only=1 if unread_only else 0) }}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-records">
|
||||
<i class="fas fa-bell-slash"></i>
|
||||
<p>{{ '暂无未读通知' if unread_only else '暂无通知' }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
|
||||
{% endblock %}
|
||||
@ -30,7 +30,7 @@
|
||||
<a href="{{ url_for('borrow.my_borrows') }}"><i class="fas fa-bookmark"></i> 我的借阅</a>
|
||||
</li>
|
||||
<li class="{% if '/announcement' in request.path %}active{% endif %}">
|
||||
<a href="#"><i class="fas fa-bell"></i> 通知公告</a>
|
||||
<a href="{{ url_for('announcement.announcement_list') }}"><i class="fas fa-bell"></i> 通知公告</a>
|
||||
</li>
|
||||
{% if current_user.is_authenticated and current_user.role_id == 1 %}
|
||||
<li class="nav-category">管理功能</li>
|
||||
@ -57,6 +57,9 @@
|
||||
<li class="{% if '/log' in request.path %}active{% endif %}">
|
||||
<a href="{{ url_for('log.log_list') }}"><i class="fas fa-history"></i> 日志管理</a>
|
||||
</li>
|
||||
<li class="{% if '/announcement/manage' in request.path %}active{% endif %}">
|
||||
<a href="{{ url_for('announcement.manage_announcements') }}"><i class="fas fa-bullhorn"></i> 公告管理</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
@ -70,9 +73,52 @@
|
||||
<input type="text" placeholder="搜索图书..." class="search-input">
|
||||
</div>
|
||||
<div class="user-menu">
|
||||
<div class="notifications">
|
||||
<i class="fas fa-bell"></i>
|
||||
<span class="badge">3</span>
|
||||
<div class="notifications dropdown">
|
||||
<a href="#" class="notification-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-bell"></i>
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set unread_count = get_unread_notifications_count(current_user.id) %}
|
||||
{% if unread_count > 0 %}
|
||||
<span class="badge">{{ unread_count }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="dropdown-menu notification-dropdown">
|
||||
<div class="notification-header">
|
||||
<h6 class="dropdown-header">通知中心</h6>
|
||||
{% if unread_count > 0 %}
|
||||
<a href="{{ url_for('announcement.mark_all_as_read') }}" class="mark-all-read">全部标为已读</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="notification-items">
|
||||
{% set recent_notifications = get_recent_notifications(current_user.id, 5) %}
|
||||
{% if recent_notifications %}
|
||||
{% for notification in recent_notifications %}
|
||||
<a class="dropdown-item notification-item {% if notification.status == 0 %}unread{% endif %}"
|
||||
href="{{ url_for('announcement.view_notification', notification_id=notification.id) }}">
|
||||
<div class="notification-content">
|
||||
<h6 class="notification-title">{{ notification.title }}</h6>
|
||||
<p class="notification-text">{{ notification.content|striptags|truncate(50) }}</p>
|
||||
<span class="notification-time">{{ notification.created_at }}</span>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="no-notifications">
|
||||
<p>暂无通知</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item view-all" href="{{ url_for('announcement.user_notifications') }}">
|
||||
查看所有通知
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="user-info">
|
||||
|
||||
@ -20,28 +20,28 @@
|
||||
<i class="fas fa-book stat-icon"></i>
|
||||
<div class="stat-info">
|
||||
<h3>馆藏总量</h3>
|
||||
<p class="stat-number">8,567</p>
|
||||
<p class="stat-number">{{ stats.total_books }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<i class="fas fa-users stat-icon"></i>
|
||||
<div class="stat-info">
|
||||
<h3>注册用户</h3>
|
||||
<p class="stat-number">1,245</p>
|
||||
<p class="stat-number">{{ stats.total_users }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<i class="fas fa-exchange-alt stat-icon"></i>
|
||||
<div class="stat-info">
|
||||
<h3>当前借阅</h3>
|
||||
<p class="stat-number">352</p>
|
||||
<p class="stat-number">{{ stats.active_borrows }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<i class="fas fa-clock stat-icon"></i>
|
||||
<div class="stat-info">
|
||||
<h3>待还图书</h3>
|
||||
<p class="stat-number">{{ 5 }}</p>
|
||||
<p class="stat-number">{{ stats.user_borrows if current_user.is_authenticated else 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,25 +52,52 @@
|
||||
<div class="content-section book-section">
|
||||
<div class="section-header">
|
||||
<h2>最新图书</h2>
|
||||
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
|
||||
<a href="{{ url_for('book.browse_books', sort='newest') }}" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
|
||||
</div>
|
||||
<div class="book-grid">
|
||||
{% for i in range(4) %}
|
||||
<div class="book-card">
|
||||
<div class="book-cover">
|
||||
<img src="https://via.placeholder.com/150x210?text=No+Cover" alt="Book Cover">
|
||||
</div>
|
||||
<div class="book-info">
|
||||
<h3 class="book-title">示例图书标题</h3>
|
||||
<p class="book-author">作者名</p>
|
||||
<div class="book-meta">
|
||||
<span class="book-category">计算机</span>
|
||||
<span class="book-status available">可借阅</span>
|
||||
{% if latest_books %}
|
||||
{% for book in latest_books %}
|
||||
<div class="book-card">
|
||||
<div class="book-cover">
|
||||
{% if book.cover_url %}
|
||||
<img src="{{ book.cover_url }}" alt="{{ book.title }} 封面">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="无封面">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="book-info">
|
||||
<h3 class="book-title">{{ book.title }}</h3>
|
||||
<p class="book-author">{{ book.author }}</p>
|
||||
<div class="book-meta">
|
||||
<span class="book-category">{{ book.category.name if book.category else '未分类' }}</span>
|
||||
<span class="book-status {{ 'available' if book.stock > 0 else 'unavailable' }}">
|
||||
{{ '可借阅' if book.stock > 0 else '已借完' }}
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ url_for('book.book_detail', book_id=book.id) }}" class="borrow-btn">
|
||||
{{ '借阅' if book.stock > 0 else '详情' }}
|
||||
</a>
|
||||
</div>
|
||||
<button class="borrow-btn">借阅</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for i in range(4) %}
|
||||
<div class="book-card">
|
||||
<div class="book-cover">
|
||||
<img src="https://via.placeholder.com/150x210?text=No+Cover" alt="Book Cover">
|
||||
</div>
|
||||
<div class="book-info">
|
||||
<h3 class="book-title">示例图书标题</h3>
|
||||
<p class="book-author">作者名</p>
|
||||
<div class="book-meta">
|
||||
<span class="book-category">计算机</span>
|
||||
<span class="book-status available">可借阅</span>
|
||||
</div>
|
||||
<button class="borrow-btn">借阅</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -78,30 +105,55 @@
|
||||
<div class="content-section notice-section">
|
||||
<div class="section-header">
|
||||
<h2>通知公告</h2>
|
||||
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
|
||||
<a href="{{ url_for('announcement.announcement_list') }}" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
|
||||
</div>
|
||||
<div class="notice-list">
|
||||
<div class="notice-item">
|
||||
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
|
||||
<div class="notice-content">
|
||||
<h3>关于五一假期图书馆开放时间调整的通知</h3>
|
||||
<p>五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。</p>
|
||||
<div class="notice-meta">
|
||||
<span class="notice-time">2023-04-28</span>
|
||||
{% if announcements %}
|
||||
{% for announcement in announcements %}
|
||||
<div class="notice-item {% if announcement.is_top %}pinned{% endif %}">
|
||||
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
|
||||
<div class="notice-content">
|
||||
<h3><a href="{{ url_for('announcement.announcement_detail', announcement_id=announcement.id) }}">{{ announcement.title }}</a></h3>
|
||||
<p>{{ announcement.content|striptags|truncate(100) }}</p>
|
||||
<div class="notice-meta">
|
||||
<span class="notice-time">{{ announcement.created_at.strftime('%Y-%m-%d') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="notice-item">
|
||||
<div class="notice-icon"><i class="fas fa-bell"></i></div>
|
||||
<div class="notice-content">
|
||||
<h3>您有2本图书即将到期</h3>
|
||||
<p>《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。</p>
|
||||
<div class="notice-meta">
|
||||
<span class="notice-time">2023-04-27</span>
|
||||
<button class="renew-btn">一键续借</button>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="notice-item">
|
||||
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
|
||||
<div class="notice-content">
|
||||
<h3>暂无公告</h3>
|
||||
<p>目前没有任何通知公告</p>
|
||||
<div class="notice-meta">
|
||||
<span class="notice-time">{{ now.strftime('%Y-%m-%d') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_authenticated and user_notifications %}
|
||||
<div class="notice-item">
|
||||
<div class="notice-icon"><i class="fas fa-bell"></i></div>
|
||||
<div class="notice-content">
|
||||
<h3>您有{{ user_notifications|length }}条未读通知</h3>
|
||||
<p>
|
||||
{% 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 %}
|
||||
</p>
|
||||
<div class="notice-meta">
|
||||
<span class="notice-time">{{ now.strftime('%Y-%m-%d') }}</span>
|
||||
<a href="{{ url_for('announcement.user_notifications') }}" class="renew-btn">查看详情</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -110,25 +162,48 @@
|
||||
<div class="content-section popular-section">
|
||||
<div class="section-header">
|
||||
<h2>热门图书</h2>
|
||||
<a href="#" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
|
||||
<a href="{{ url_for('book.browse_books', sort='popular') }}" class="view-all">查看全部 <i class="fas fa-arrow-right"></i></a>
|
||||
</div>
|
||||
<div class="popular-books">
|
||||
{% for i in range(5) %}
|
||||
<div class="popular-book-item">
|
||||
<div class="rank-badge">{{ i+1 }}</div>
|
||||
<div class="book-cover small">
|
||||
<img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
|
||||
</div>
|
||||
<div class="book-details">
|
||||
<h3 class="book-title">热门图书标题示例</h3>
|
||||
<p class="book-author">知名作者</p>
|
||||
<div class="book-stats">
|
||||
<span><i class="fas fa-eye"></i> 1024 次浏览</span>
|
||||
<span><i class="fas fa-bookmark"></i> 89 次借阅</span>
|
||||
{% if popular_books %}
|
||||
{% for book in popular_books %}
|
||||
<div class="popular-book-item">
|
||||
<div class="rank-badge">{{ loop.index }}</div>
|
||||
<div class="book-cover small">
|
||||
{% if book.cover_url %}
|
||||
<img src="{{ book.cover_url }}" alt="{{ book.title }} 封面">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='images/book-placeholder.jpg') }}" alt="无封面">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="book-details">
|
||||
<h3 class="book-title">{{ book.title }}</h3>
|
||||
<p class="book-author">{{ book.author }}</p>
|
||||
<div class="book-stats">
|
||||
<span><i class="fas fa-eye"></i> {{ book.view_count if book.view_count else 0 }} 次浏览</span>
|
||||
<span><i class="fas fa-bookmark"></i> {{ book.borrow_count if book.borrow_count else 0 }} 次借阅</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for i in range(5) %}
|
||||
<div class="popular-book-item">
|
||||
<div class="rank-badge">{{ i+1 }}</div>
|
||||
<div class="book-cover small">
|
||||
<img src="https://via.placeholder.com/80x120?text=Book" alt="Book Cover">
|
||||
</div>
|
||||
<div class="book-details">
|
||||
<h3 class="book-title">热门图书标题示例</h3>
|
||||
<p class="book-author">知名作者</p>
|
||||
<div class="book-stats">
|
||||
<span><i class="fas fa-eye"></i> 1024 次浏览</span>
|
||||
<span><i class="fas fa-bookmark"></i> 89 次借阅</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@ -6,7 +6,10 @@
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&display=swap">
|
||||
<!-- Updated Google Fonts Link -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=Lora:wght@400;600&family=Open+Sans:wght@300;400&family=Sacramento&family=EB+Garamond:ital@0;1&display=swap" rel="stylesheet">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -70,12 +73,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
cards.forEach(card => {
|
||||
card.addEventListener('mouseenter', function() {
|
||||
const decoration = this.querySelector('.card-decoration');
|
||||
decoration.classList.add('active');
|
||||
if (decoration) { // Check if decoration exists
|
||||
decoration.classList.add('active');
|
||||
}
|
||||
this.classList.add('hovered'); // Add class for general hover style
|
||||
});
|
||||
|
||||
card.addEventListener('mouseleave', function() {
|
||||
const decoration = this.querySelector('.card-decoration');
|
||||
decoration.classList.remove('active');
|
||||
if (decoration) { // Check if decoration exists
|
||||
decoration.classList.remove('active');
|
||||
}
|
||||
this.classList.remove('hovered'); // Remove class
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,23 +1,32 @@
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user