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 app.controllers.inventory import inventory_bp
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
from app.controllers.statistics import statistics_bp
|
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
|
from app.controllers.log import log_bp
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
|
|
||||||
|
|
||||||
@ -54,6 +56,7 @@ def create_app(config=None):
|
|||||||
app.register_blueprint(statistics_bp)
|
app.register_blueprint(statistics_bp)
|
||||||
app.register_blueprint(inventory_bp)
|
app.register_blueprint(inventory_bp)
|
||||||
app.register_blueprint(log_bp)
|
app.register_blueprint(log_bp)
|
||||||
|
app.register_blueprint(announcement_bp, url_prefix='/announcement')
|
||||||
|
|
||||||
# 创建数据库表
|
# 创建数据库表
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
@ -124,9 +127,83 @@ def create_app(config=None):
|
|||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
if not current_user.is_authenticated:
|
from app.models.book import Book
|
||||||
return redirect(url_for('user.login'))
|
from app.models.user import User
|
||||||
return render_template('index.html') # 无需传递current_user,Flask-Login自动提供
|
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)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
@ -138,8 +215,32 @@ def create_app(config=None):
|
|||||||
return Markup(s.replace('\n', '<br>'))
|
return Markup(s.replace('\n', '<br>'))
|
||||||
return s
|
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
|
return app
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_now():
|
def inject_now():
|
||||||
return {'now': datetime.datetime.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.book import Book, Category
|
||||||
from app.models.user import db
|
from app.models.user import db
|
||||||
from app.utils.auth import login_required, admin_required
|
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
|
import os
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
import datetime
|
import datetime
|
||||||
@ -62,7 +62,7 @@ def admin_book_list():
|
|||||||
category_id=category_id,
|
category_id=category_id,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
order=order,
|
order=order,
|
||||||
current_user=g.user,
|
current_user=current_user, # 使用current_user替代g.user
|
||||||
is_admin_view=True) # 指明这是管理视图
|
is_admin_view=True) # 指明这是管理视图
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ def book_list():
|
|||||||
category_id=category_id,
|
category_id=category_id,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
order=order,
|
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 = []
|
borrow_records = []
|
||||||
# 防御性编程:确保 g.user 存在且有 role_id 属性
|
# 使用current_user代替g.user
|
||||||
if hasattr(g, 'user') and g.user is not None and hasattr(g.user, 'role_id') and g.user.role_id == 1:
|
if current_user.is_authenticated and current_user.role_id == 1:
|
||||||
from app.models.borrow import BorrowRecord
|
from app.models.borrow import BorrowRecord
|
||||||
borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit(
|
borrow_records = BorrowRecord.query.filter_by(book_id=book_id).order_by(BorrowRecord.borrow_date.desc()).limit(
|
||||||
10).all()
|
10).all()
|
||||||
@ -155,7 +155,7 @@ def book_detail(book_id):
|
|||||||
return render_template(
|
return render_template(
|
||||||
'book/detail.html',
|
'book/detail.html',
|
||||||
book=book,
|
book=book,
|
||||||
current_user=current_user, # 使用 flask_login 的 current_user 而不是 g.user
|
current_user=current_user,
|
||||||
borrow_records=borrow_records,
|
borrow_records=borrow_records,
|
||||||
now=now
|
now=now
|
||||||
)
|
)
|
||||||
@ -209,7 +209,7 @@ def add_book():
|
|||||||
'price': price
|
'price': price
|
||||||
}
|
}
|
||||||
return render_template('book/add.html', categories=categories,
|
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
|
cover_url = None
|
||||||
@ -269,7 +269,7 @@ def add_book():
|
|||||||
change_type='入库',
|
change_type='入库',
|
||||||
change_amount=stock,
|
change_amount=stock,
|
||||||
after_stock=stock,
|
after_stock=stock,
|
||||||
operator_id=g.user.id,
|
operator_id=current_user.id, # 使用current_user.id替代g.user.id
|
||||||
remark='新书入库',
|
remark='新书入库',
|
||||||
changed_at=datetime.datetime.now()
|
changed_at=datetime.datetime.now()
|
||||||
)
|
)
|
||||||
@ -320,10 +320,10 @@ def add_book():
|
|||||||
'price': price
|
'price': price
|
||||||
}
|
}
|
||||||
return render_template('book/add.html', categories=categories,
|
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()
|
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:
|
if not title or not author:
|
||||||
flash('书名和作者不能为空', 'danger')
|
flash('书名和作者不能为空', 'danger')
|
||||||
categories = Category.query.all()
|
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验证
|
# ISBN验证
|
||||||
if isbn and isbn.strip(): # 确保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:
|
if len(clean_isbn) != 10 and len(clean_isbn) != 13:
|
||||||
flash('ISBN必须是10位或13位', 'danger')
|
flash('ISBN必须是10位或13位', 'danger')
|
||||||
categories = Category.query.all()
|
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验证
|
# ISBN-10验证
|
||||||
if len(clean_isbn) == 10:
|
if len(clean_isbn) == 10:
|
||||||
@ -369,13 +369,13 @@ def edit_book(book_id):
|
|||||||
if not clean_isbn[:9].isdigit():
|
if not clean_isbn[:9].isdigit():
|
||||||
flash('ISBN-10的前9位必须是数字', 'danger')
|
flash('ISBN-10的前9位必须是数字', 'danger')
|
||||||
categories = Category.query.all()
|
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'
|
# 检查最后一位是否为数字或'X'
|
||||||
if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'):
|
if not (clean_isbn[9].isdigit() or clean_isbn[9].upper() == 'X'):
|
||||||
flash('ISBN-10的最后一位必须是数字或X', 'danger')
|
flash('ISBN-10的最后一位必须是数字或X', 'danger')
|
||||||
categories = Category.query.all()
|
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
|
sum = 0
|
||||||
@ -388,7 +388,7 @@ def edit_book(book_id):
|
|||||||
if sum % 11 != 0:
|
if sum % 11 != 0:
|
||||||
flash('ISBN-10校验和无效', 'danger')
|
flash('ISBN-10校验和无效', 'danger')
|
||||||
categories = Category.query.all()
|
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验证
|
# ISBN-13验证
|
||||||
if len(clean_isbn) == 13:
|
if len(clean_isbn) == 13:
|
||||||
@ -396,7 +396,7 @@ def edit_book(book_id):
|
|||||||
if not clean_isbn.isdigit():
|
if not clean_isbn.isdigit():
|
||||||
flash('ISBN-13必须全是数字', 'danger')
|
flash('ISBN-13必须全是数字', 'danger')
|
||||||
categories = Category.query.all()
|
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
|
sum = 0
|
||||||
@ -408,7 +408,7 @@ def edit_book(book_id):
|
|||||||
if check_digit != int(clean_isbn[12]):
|
if check_digit != int(clean_isbn[12]):
|
||||||
flash('ISBN-13校验和无效', 'danger')
|
flash('ISBN-13校验和无效', 'danger')
|
||||||
categories = Category.query.all()
|
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
|
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_type=change_type,
|
||||||
change_amount=abs(change_amount),
|
change_amount=abs(change_amount),
|
||||||
after_stock=new_stock,
|
after_stock=new_stock,
|
||||||
operator_id=g.user.id,
|
operator_id=current_user.id, # 使用current_user.id替代g.user.id
|
||||||
remark=f'管理员编辑图书库存 - {book.title}',
|
remark=f'管理员编辑图书库存 - {book.title}',
|
||||||
changed_at=datetime.datetime.now()
|
changed_at=datetime.datetime.now()
|
||||||
)
|
)
|
||||||
@ -490,11 +490,11 @@ def edit_book(book_id):
|
|||||||
|
|
||||||
flash(f'保存失败: {str(e)}', 'danger')
|
flash(f'保存失败: {str(e)}', 'danger')
|
||||||
categories = Category.query.all()
|
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 请求
|
# GET 请求
|
||||||
categories = Category.query.all()
|
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="访问图书分类管理页面"
|
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_type='入库',
|
||||||
change_amount=book.stock,
|
change_amount=book.stock,
|
||||||
after_stock=book.stock,
|
after_stock=book.stock,
|
||||||
operator_id=g.user.id,
|
operator_id=current_user.id, # 使用current_user.id
|
||||||
remark='批量导入图书',
|
remark='批量导入图书',
|
||||||
changed_at=datetime.datetime.now()
|
changed_at=datetime.datetime.now()
|
||||||
)
|
)
|
||||||
@ -784,7 +784,7 @@ def import_books():
|
|||||||
flash('只支持Excel文件(.xlsx, .xls)', 'danger')
|
flash('只支持Excel文件(.xlsx, .xls)', 'danger')
|
||||||
return redirect(request.url)
|
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,
|
categories=categories,
|
||||||
category_id=category_id,
|
category_id=category_id,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
order=order, )
|
order=order) # current_user自动传递到模板
|
||||||
|
|
||||||
|
|
||||||
@book_bp.route('/template/download')
|
@book_bp.route('/template/download')
|
||||||
@ -1016,4 +1016,3 @@ def download_template():
|
|||||||
as_attachment=True,
|
as_attachment=True,
|
||||||
download_name=filename
|
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;
|
font-weight: 600;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: block; /* 修改为block以占据整个单元格 */
|
||||||
|
text-align: center; /* 确保文本居中 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table .borrow-count:after {
|
.data-table .borrow-count:after {
|
||||||
@ -247,34 +248,40 @@ tr:hover .book-title:after {
|
|||||||
display: inline-block;
|
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 {
|
.data-table tr:nth-child(1) .rank:before {
|
||||||
content: '🏆';
|
content: '🏆';
|
||||||
position: absolute;
|
|
||||||
top: -15px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tr:nth-child(2) .rank:before {
|
.data-table tr:nth-child(2) .rank:before {
|
||||||
content: '🥈';
|
content: '🥈';
|
||||||
position: absolute;
|
|
||||||
top: -15px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tr:nth-child(3) .rank:before {
|
.data-table tr:nth-child(3) .rank:before {
|
||||||
content: '🥉';
|
content: '🥉';
|
||||||
position: absolute;
|
|
||||||
top: -15px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 调整排名单元格的内边距,为图标留出空间 */
|
||||||
|
.data-table .rank {
|
||||||
|
padding-left: 35px; /* 增加左内边距为图标腾出空间 */
|
||||||
|
text-align: left; /* 使数字左对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 加载动画美化 */
|
/* 加载动画美化 */
|
||||||
.loading-animation {
|
.loading-animation {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -533,25 +533,35 @@ ul {
|
|||||||
display: flex;
|
display: flex;
|
||||||
background-color: #f8fafc;
|
background-color: #f8fafc;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 15px;
|
padding: 20px 15px 15px; /* 增加顶部内边距,为角标留出空间 */
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
position: relative;
|
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 {
|
.rank-badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -10px;
|
top: -8px; /* 略微调高一点 */
|
||||||
left: 10px;
|
left: 10px;
|
||||||
background-color: #4a89dc;
|
background-color: #4a89dc;
|
||||||
color: white;
|
color: white;
|
||||||
width: 24px;
|
width: 28px; /* 增加尺寸 */
|
||||||
height: 24px;
|
height: 28px; /* 增加尺寸 */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
font-size: 0.8rem;
|
font-size: 0.85rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
z-index: 10; /* 确保它位于其他元素之上 */
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2); /* 添加阴影使其更突出 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-cover.small {
|
.book-cover.small {
|
||||||
@ -559,6 +569,14 @@ ul {
|
|||||||
height: 90px;
|
height: 90px;
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden; /* 确保图片不会溢出容器 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-cover.small img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover; /* 确保图片正确填充容器 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-details {
|
.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 = `
|
document.getElementById('ranking-table-body').innerHTML = `
|
||||||
<tr class="loading-row">
|
<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>
|
</tr>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -56,15 +56,16 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
let tableHtml = '';
|
let tableHtml = '';
|
||||||
|
|
||||||
data.forEach((book, index) => {
|
data.forEach((book, index) => {
|
||||||
|
// 给每个单元格添加适当的类名以匹配CSS
|
||||||
tableHtml += `
|
tableHtml += `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="rank">${index + 1}</td>
|
<td class="rank">${index + 1}</td>
|
||||||
<td>
|
<td class="book-cover">
|
||||||
<img src="${book.cover_url || '/static/images/book-placeholder.jpg'}" alt="${book.title}">
|
<img src="${book.cover_url || '/static/images/book-placeholder.jpg'}" alt="${book.title}">
|
||||||
</td>
|
</td>
|
||||||
<td>${book.title}</td>
|
<td class="book-title-cell"><span class="book-title">${book.title}</span></td>
|
||||||
<td>${book.author}</td>
|
<td class="author">${book.author}</td>
|
||||||
<td class="borrow-count">${book.borrow_count}</td>
|
<td><span class="borrow-count">${book.borrow_count}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
@ -95,8 +96,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: '借阅次数',
|
label: '借阅次数',
|
||||||
data: borrowCounts,
|
data: borrowCounts,
|
||||||
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
backgroundColor: 'rgba(183, 110, 121, 0.6)', // 玫瑰金色调
|
||||||
borderColor: 'rgba(54, 162, 235, 1)',
|
borderColor: 'rgba(140, 45, 90, 1)', // 浆果红
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
@ -108,13 +109,41 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
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: {
|
x: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
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: {
|
title: {
|
||||||
display: true,
|
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 overdueRangeChart = null;
|
||||||
let overdueStatusChart = null;
|
let overdueStatusChart = null;
|
||||||
|
|
||||||
// 初始加载
|
// Initial load
|
||||||
loadOverdueStatistics();
|
loadOverdueStatistics();
|
||||||
|
|
||||||
function loadOverdueStatistics() {
|
function loadOverdueStatistics() {
|
||||||
// 调用API获取数据
|
// Call API to get data
|
||||||
fetch('/statistics/api/overdue-statistics')
|
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 => {
|
.then(data => {
|
||||||
|
if (!data) {
|
||||||
|
console.error('加载逾期统计数据失败: API返回空数据');
|
||||||
|
// Optionally update UI to show error for cards
|
||||||
|
return;
|
||||||
|
}
|
||||||
updateOverdueCards(data);
|
updateOverdueCards(data);
|
||||||
updateOverdueRangeChart(data.overdue_ranges);
|
updateOverdueRangeChart(data.overdue_ranges);
|
||||||
updateOverdueStatusChart(data);
|
updateOverdueStatusChart(data);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('加载逾期统计数据失败:', 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) {
|
function updateOverdueCards(data) {
|
||||||
document.getElementById('total-borrows').querySelector('.card-value').textContent = data.total_borrows;
|
document.getElementById('total-borrows').querySelector('.card-value').textContent = data.total_borrows || 0;
|
||||||
document.getElementById('current-overdue').querySelector('.card-value').textContent = data.current_overdue;
|
document.getElementById('current-overdue').querySelector('.card-value').textContent = data.current_overdue || 0;
|
||||||
document.getElementById('returned-overdue').querySelector('.card-value').textContent = data.returned_overdue;
|
document.getElementById('returned-overdue').querySelector('.card-value').textContent = data.returned_overdue || 0;
|
||||||
document.getElementById('overdue-rate').querySelector('.card-value').textContent = data.overdue_rate + '%';
|
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) {
|
function updateOverdueRangeChart(rangeData) {
|
||||||
// 销毁旧图表
|
// Destroy old chart
|
||||||
if (overdueRangeChart) {
|
if (overdueRangeChart) {
|
||||||
overdueRangeChart.destroy();
|
overdueRangeChart.destroy();
|
||||||
|
overdueRangeChart = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canvas = document.getElementById('overdue-range-chart');
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
if (!rangeData || rangeData.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备图表数据
|
// Prepare chart data
|
||||||
const labels = rangeData.map(item => item.range);
|
const labels = rangeData.map(item => item.range);
|
||||||
const counts = rangeData.map(item => item.count);
|
const counts = rangeData.map(item => item.count);
|
||||||
|
|
||||||
// 创建图表
|
// Create chart
|
||||||
const ctx = document.getElementById('overdue-range-chart').getContext('2d');
|
|
||||||
overdueRangeChart = new Chart(ctx, {
|
overdueRangeChart = new Chart(ctx, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
datasets: [{
|
datasets: [{
|
||||||
label: '逾期数量',
|
label: '逾期数量', // This will appear in the legend
|
||||||
data: counts,
|
data: counts,
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgba(255, 206, 86, 0.7)',
|
'rgba(255, 206, 86, 0.7)', // 1-7天
|
||||||
'rgba(255, 159, 64, 0.7)',
|
'rgba(255, 159, 64, 0.7)', // 8-14天
|
||||||
'rgba(255, 99, 132, 0.7)',
|
'rgba(255, 99, 132, 0.7)', // 15-30天
|
||||||
'rgba(255, 0, 0, 0.7)'
|
'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
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
@ -69,12 +109,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
display: true,
|
display: true,
|
||||||
text: '数量'
|
text: '数量'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
// No title for X-axis needed here based on current config
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
title: {
|
title: { // Chart.js internal title
|
||||||
display: true,
|
display: false, // Set to false to use HTML <h3> title
|
||||||
text: '逾期时长分布'
|
// 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) {
|
function updateOverdueStatusChart(data) {
|
||||||
// 销毁旧图表
|
// Destroy old chart
|
||||||
if (overdueStatusChart) {
|
if (overdueStatusChart) {
|
||||||
overdueStatusChart.destroy();
|
overdueStatusChart.destroy();
|
||||||
|
overdueStatusChart = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 准备图表数据
|
const canvas = document.getElementById('overdue-status-chart');
|
||||||
const statusLabels = ['当前逾期', '历史逾期', '正常'];
|
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 = [
|
const statusData = [
|
||||||
data.current_overdue,
|
currentOverdue,
|
||||||
data.returned_overdue,
|
returnedOverdue,
|
||||||
data.total_borrows - data.current_overdue - data.returned_overdue
|
normalCount < 0 ? 0 : normalCount // Ensure not negative
|
||||||
];
|
];
|
||||||
|
|
||||||
// 创建图表
|
// Create chart
|
||||||
const ctx = document.getElementById('overdue-status-chart').getContext('2d');
|
|
||||||
overdueStatusChart = new Chart(ctx, {
|
overdueStatusChart = new Chart(ctx, {
|
||||||
type: 'pie',
|
type: 'pie',
|
||||||
data: {
|
data: {
|
||||||
@ -104,9 +177,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
data: statusData,
|
data: statusData,
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgba(255, 99, 132, 0.7)',
|
'rgba(255, 99, 132, 0.7)', // 当前逾期
|
||||||
'rgba(255, 206, 86, 0.7)',
|
'rgba(255, 206, 86, 0.7)', // 历史逾期
|
||||||
'rgba(75, 192, 192, 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
|
borderWidth: 1
|
||||||
}]
|
}]
|
||||||
@ -115,10 +193,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
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,
|
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获取数据
|
// 调用API获取数据
|
||||||
fetch('/statistics/api/user-activity')
|
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 => {
|
.then(data => {
|
||||||
updateUserTable(data);
|
updateUserTable(data);
|
||||||
updateUserChart(data);
|
updateUserChart(data);
|
||||||
@ -27,13 +32,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<td colspan="4">加载数据失败,请稍后重试</td>
|
<td colspan="4">加载数据失败,请稍后重试</td>
|
||||||
</tr>
|
</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) {
|
function updateUserTable(data) {
|
||||||
const tableBody = document.getElementById('user-table-body');
|
const tableBody = document.getElementById('user-table-body');
|
||||||
|
|
||||||
if (data.length === 0) {
|
if (!data || data.length === 0) { // 增加了对 data 本身的检查
|
||||||
tableBody.innerHTML = `
|
tableBody.innerHTML = `
|
||||||
<tr class="no-data-row">
|
<tr class="no-data-row">
|
||||||
<td colspan="4">暂无数据</td>
|
<td colspan="4">暂无数据</td>
|
||||||
@ -48,8 +62,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
tableHtml += `
|
tableHtml += `
|
||||||
<tr>
|
<tr>
|
||||||
<td class="rank">${index + 1}</td>
|
<td class="rank">${index + 1}</td>
|
||||||
<td>${user.username}</td>
|
<td>${user.username || 'N/A'}</td>
|
||||||
<td>${user.nickname}</td>
|
<td>${user.nickname || 'N/A'}</td>
|
||||||
<td class="borrow-count">${user.borrow_count}</td>
|
<td class="borrow-count">${user.borrow_count}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
@ -62,18 +76,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// 销毁旧图表
|
// 销毁旧图表
|
||||||
if (activityChart) {
|
if (activityChart) {
|
||||||
activityChart.destroy();
|
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;
|
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 borrowCounts = data.map(user => user.borrow_count);
|
||||||
|
|
||||||
// 创建图表
|
// 创建图表
|
||||||
const ctx = document.getElementById('user-activity-chart').getContext('2d');
|
|
||||||
activityChart = new Chart(ctx, {
|
activityChart = new Chart(ctx, {
|
||||||
type: 'bar',
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
@ -100,17 +123,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
x: {
|
x: {
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: '用户'
|
text: '用户' // 这个标题现在应该有空间显示了
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false // 保持不显示图例
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true, // 如果HTML中已有h3标题,这里可以设为 false
|
||||||
text: '最活跃用户排行'
|
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>
|
<a href="{{ url_for('borrow.my_borrows') }}"><i class="fas fa-bookmark"></i> 我的借阅</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{% if '/announcement' in request.path %}active{% endif %}">
|
<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>
|
</li>
|
||||||
{% if current_user.is_authenticated and current_user.role_id == 1 %}
|
{% if current_user.is_authenticated and current_user.role_id == 1 %}
|
||||||
<li class="nav-category">管理功能</li>
|
<li class="nav-category">管理功能</li>
|
||||||
@ -57,6 +57,9 @@
|
|||||||
<li class="{% if '/log' in request.path %}active{% endif %}">
|
<li class="{% if '/log' in request.path %}active{% endif %}">
|
||||||
<a href="{{ url_for('log.log_list') }}"><i class="fas fa-history"></i> 日志管理</a>
|
<a href="{{ url_for('log.log_list') }}"><i class="fas fa-history"></i> 日志管理</a>
|
||||||
</li>
|
</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 %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@ -70,9 +73,52 @@
|
|||||||
<input type="text" placeholder="搜索图书..." class="search-input">
|
<input type="text" placeholder="搜索图书..." class="search-input">
|
||||||
</div>
|
</div>
|
||||||
<div class="user-menu">
|
<div class="user-menu">
|
||||||
<div class="notifications">
|
<div class="notifications dropdown">
|
||||||
|
<a href="#" class="notification-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<i class="fas fa-bell"></i>
|
<i class="fas fa-bell"></i>
|
||||||
<span class="badge">3</span>
|
{% 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>
|
</div>
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
|
|||||||
@ -20,28 +20,28 @@
|
|||||||
<i class="fas fa-book stat-icon"></i>
|
<i class="fas fa-book stat-icon"></i>
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<h3>馆藏总量</h3>
|
<h3>馆藏总量</h3>
|
||||||
<p class="stat-number">8,567</p>
|
<p class="stat-number">{{ stats.total_books }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<i class="fas fa-users stat-icon"></i>
|
<i class="fas fa-users stat-icon"></i>
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<h3>注册用户</h3>
|
<h3>注册用户</h3>
|
||||||
<p class="stat-number">1,245</p>
|
<p class="stat-number">{{ stats.total_users }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<i class="fas fa-exchange-alt stat-icon"></i>
|
<i class="fas fa-exchange-alt stat-icon"></i>
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<h3>当前借阅</h3>
|
<h3>当前借阅</h3>
|
||||||
<p class="stat-number">352</p>
|
<p class="stat-number">{{ stats.active_borrows }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<i class="fas fa-clock stat-icon"></i>
|
<i class="fas fa-clock stat-icon"></i>
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<h3>待还图书</h3>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -52,9 +52,35 @@
|
|||||||
<div class="content-section book-section">
|
<div class="content-section book-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>最新图书</h2>
|
<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>
|
||||||
<div class="book-grid">
|
<div class="book-grid">
|
||||||
|
{% 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>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
{% for i in range(4) %}
|
{% for i in range(4) %}
|
||||||
<div class="book-card">
|
<div class="book-card">
|
||||||
<div class="book-cover">
|
<div class="book-cover">
|
||||||
@ -71,6 +97,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -78,30 +105,55 @@
|
|||||||
<div class="content-section notice-section">
|
<div class="content-section notice-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>通知公告</h2>
|
<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>
|
||||||
<div class="notice-list">
|
<div class="notice-list">
|
||||||
|
{% 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>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
<div class="notice-item">
|
<div class="notice-item">
|
||||||
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
|
<div class="notice-icon"><i class="fas fa-bullhorn"></i></div>
|
||||||
<div class="notice-content">
|
<div class="notice-content">
|
||||||
<h3>关于五一假期图书馆开放时间调整的通知</h3>
|
<h3>暂无公告</h3>
|
||||||
<p>五一期间(5月1日-5日),图书馆开放时间调整为上午9:00-下午5:00。</p>
|
<p>目前没有任何通知公告</p>
|
||||||
<div class="notice-meta">
|
<div class="notice-meta">
|
||||||
<span class="notice-time">2023-04-28</span>
|
<span class="notice-time">{{ now.strftime('%Y-%m-%d') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated and user_notifications %}
|
||||||
<div class="notice-item">
|
<div class="notice-item">
|
||||||
<div class="notice-icon"><i class="fas fa-bell"></i></div>
|
<div class="notice-icon"><i class="fas fa-bell"></i></div>
|
||||||
<div class="notice-content">
|
<div class="notice-content">
|
||||||
<h3>您有2本图书即将到期</h3>
|
<h3>您有{{ user_notifications|length }}条未读通知</h3>
|
||||||
<p>《Python编程》《算法导论》将于3天后到期,请及时归还或办理续借。</p>
|
<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">
|
<div class="notice-meta">
|
||||||
<span class="notice-time">2023-04-27</span>
|
<span class="notice-time">{{ now.strftime('%Y-%m-%d') }}</span>
|
||||||
<button class="renew-btn">一键续借</button>
|
<a href="{{ url_for('announcement.user_notifications') }}" class="renew-btn">查看详情</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,9 +162,31 @@
|
|||||||
<div class="content-section popular-section">
|
<div class="content-section popular-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>热门图书</h2>
|
<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>
|
||||||
<div class="popular-books">
|
<div class="popular-books">
|
||||||
|
{% 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>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
{% for i in range(5) %}
|
{% for i in range(5) %}
|
||||||
<div class="popular-book-item">
|
<div class="popular-book-item">
|
||||||
<div class="rank-badge">{{ i+1 }}</div>
|
<div class="rank-badge">{{ i+1 }}</div>
|
||||||
@ -129,6 +203,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -6,7 +6,10 @@
|
|||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/statistics.css') }}">
|
<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://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 %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -70,12 +73,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
card.addEventListener('mouseenter', function() {
|
card.addEventListener('mouseenter', function() {
|
||||||
const decoration = this.querySelector('.card-decoration');
|
const decoration = this.querySelector('.card-decoration');
|
||||||
|
if (decoration) { // Check if decoration exists
|
||||||
decoration.classList.add('active');
|
decoration.classList.add('active');
|
||||||
|
}
|
||||||
|
this.classList.add('hovered'); // Add class for general hover style
|
||||||
});
|
});
|
||||||
|
|
||||||
card.addEventListener('mouseleave', function() {
|
card.addEventListener('mouseleave', function() {
|
||||||
const decoration = this.querySelector('.card-decoration');
|
const decoration = this.querySelector('.card-decoration');
|
||||||
|
if (decoration) { // Check if decoration exists
|
||||||
decoration.classList.remove('active');
|
decoration.classList.remove('active');
|
||||||
|
}
|
||||||
|
this.classList.remove('hovered'); // Remove class
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,23 +1,32 @@
|
|||||||
from functools import wraps
|
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):
|
def login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
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')
|
flash('请先登录', 'warning')
|
||||||
return redirect(url_for('user.login', next=request.url))
|
return redirect(url_for('user.login', next=request.url))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
def admin_required(f):
|
def admin_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
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')
|
flash('请先登录', 'warning')
|
||||||
return redirect(url_for('user.login', next=request.url))
|
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')
|
flash('权限不足', 'danger')
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user