diff --git a/app/controllers/user.py b/app/controllers/user.py index d672bb3..6250610 100644 --- a/app/controllers/user.py +++ b/app/controllers/user.py @@ -10,7 +10,8 @@ from datetime import datetime, timedelta from app.services.user_service import UserService from flask_login import login_user, logout_user, current_user, login_required from app.models.user import User - +from app.models.log import Log # 导入日志模型 +from app.models.borrow import BorrowRecord # 导入借阅记录模型 # 创建蓝图 user_bp = Blueprint('user', __name__) @@ -755,3 +756,116 @@ def add_user(): # GET请求,显示添加用户表单 return render_template('user/add.html', roles=roles) + + +@user_bp.route('/api/activities') +@login_required +def user_activities(): + """获取当前用户的活动记录""" + activity_type = request.args.get('type', 'all') + page = request.args.get('page', 1, type=int) + per_page = request.args.get('per_page', 10, type=int) + + # 从日志表中查询该用户的日志 + query = Log.query.filter(Log.user_id == current_user.id).order_by(Log.created_at.desc()) + + # 根据活动类型筛选 + if activity_type == 'login': + query = query.filter(Log.action.in_(['登录', '登出', '登录失败'])) + elif activity_type == 'borrow': + query = query.filter(Log.action.in_(['借书', '预约'])) + elif activity_type == 'return': + query = query.filter(Log.action.in_(['还书', '续借'])) + + # 分页 + pagination = query.paginate(page=page, per_page=per_page, error_out=False) + activities = pagination.items + + # 格式化活动数据 + result = [] + for log in activities: + activity_type = determine_activity_type(log.action) + + activity = { + 'id': log.id, + 'type': activity_type, + 'title': get_activity_title(log.action), + 'details': log.description or '无详细信息', + 'time': log.created_at.strftime('%Y-%m-%d %H:%M:%S'), + 'ip': log.ip_address + } + + # 如果存在目标ID和类型,添加到结果中 + if log.target_id and log.target_type: + activity['target_id'] = log.target_id + activity['target_type'] = log.target_type + + result.append(activity) + + return jsonify({ + 'activities': result, + 'total': pagination.total, + 'pages': pagination.pages, + 'current_page': pagination.page + }) + + +def determine_activity_type(action): + """根据日志动作确定活动类型""" + login_actions = ['登录', '登出', '登录失败'] + borrow_actions = ['借书', '预约'] + return_actions = ['还书', '续借'] + + if action in login_actions: + return 'login' + elif action in borrow_actions: + return 'borrow' + elif action in return_actions: + return 'return' + else: + return 'other' + + +def get_activity_title(action): + """根据动作返回活动标题""" + action_titles = { + '登录': '系统登录', + '登出': '退出登录', + '登录失败': '登录失败', + '借书': '借阅图书', + '还书': '归还图书', + '预约': '预约图书', + '续借': '续借图书' + } + + return action_titles.get(action, action) + + +@user_bp.route('/api/stats') +@login_required +def user_stats(): + """获取用户统计数据""" + # 查询用户的借阅统计 + borrowed = db.session.query(db.func.count(BorrowRecord.id)) \ + .filter(BorrowRecord.user_id == current_user.id, + BorrowRecord.status == 1, # 借阅中状态 + BorrowRecord.return_date == None) \ + .scalar() or 0 + + returned = db.session.query(db.func.count(BorrowRecord.id)) \ + .filter(BorrowRecord.user_id == current_user.id, + BorrowRecord.return_date != None) \ + .scalar() or 0 + + overdue = db.session.query(db.func.count(BorrowRecord.id)) \ + .filter(BorrowRecord.user_id == current_user.id, + BorrowRecord.status == 1, # 借阅中状态 + BorrowRecord.return_date == None, + BorrowRecord.due_date < datetime.now()) \ + .scalar() or 0 + + return jsonify({ + 'borrow': borrowed, + 'returned': returned, + 'overdue': overdue + }) diff --git a/app/static/js/user-profile.js b/app/static/js/user-profile.js index e2c8ccf..b2d1230 100644 --- a/app/static/js/user-profile.js +++ b/app/static/js/user-profile.js @@ -131,22 +131,21 @@ document.addEventListener('DOMContentLoaded', function() { // 获取用户统计数据 function fetchUserStats() { - // 这里使用虚拟数据,实际应用中应当从后端获取 - // fetch('/api/user/stats') - // .then(response => response.json()) - // .then(data => { - // updateUserStats(data); - // }); - - // 模拟数据 - setTimeout(() => { - const mockData = { - borrow: 2, - returned: 15, - overdue: 0 - }; - updateUserStats(mockData); - }, 500); + fetch('/user/api/stats') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + updateUserStats(data); + }) + .catch(error => { + console.error('Error fetching user stats:', error); + // 出错时使用默认值 + updateUserStats({borrow: 0, returned: 0, overdue: 0}); + }); } // 更新用户统计显示 @@ -175,58 +174,20 @@ document.addEventListener('DOMContentLoaded', function() { `; - // 实际应用中应当从后端获取 - // fetch(`/api/user/activities?type=${type}`) - // .then(response => response.json()) - // .then(data => { - // renderActivityTimeline(data, timelineContainer); - // }); - - // 模拟数据 - setTimeout(() => { - const mockActivities = [ - { - id: 1, - type: 'login', - title: '系统登录', - details: '成功登录系统', - time: '2023-04-28 15:30:22', - ip: '192.168.1.1' - }, - { - id: 2, - type: 'borrow', - title: '借阅图书', - details: '借阅《JavaScript高级编程》', - time: '2023-04-27 11:45:10', - book_id: 101 - }, - { - id: 3, - type: 'return', - title: '归还图书', - details: '归还《Python数据分析》', - time: '2023-04-26 09:15:33', - book_id: 95 - }, - { - id: 4, - type: 'login', - title: '系统登录', - details: '成功登录系统', - time: '2023-04-25 08:22:15', - ip: '192.168.1.1' - } - ]; - - // 根据筛选条件过滤活动 - let filteredActivities = mockActivities; - if (type !== 'all') { - filteredActivities = mockActivities.filter(activity => activity.type === type); + fetch(`/user/api/activities?type=${type}`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); } - - renderActivityTimeline(filteredActivities, timelineContainer); - }, 800); + return response.json(); + }) + .then(data => { + renderActivityTimeline(data.activities, timelineContainer); + }) + .catch(error => { + console.error('Error fetching activities:', error); + timelineContainer.innerHTML = '
获取活动记录失败
'; + }); } // 渲染活动时间线 @@ -247,6 +208,8 @@ document.addEventListener('DOMContentLoaded', function() { iconClass = 'fas fa-book'; } else if (activity.type === 'return') { iconClass = 'fas fa-undo'; + } else if (activity.type === 'other') { + iconClass = 'fas fa-cog'; } const isLast = index === activities.length - 1; @@ -264,6 +227,10 @@ document.addEventListener('DOMContentLoaded', function() {
${activity.details} ${activity.ip ? `
IP: ${activity.ip}
` : ''} + ${activity.target_type && activity.target_id ? ` +
+ ${activity.target_type === 'book' ? '图书ID: ' : '目标ID: '}${activity.target_id} +
` : ''}
@@ -272,4 +239,134 @@ document.addEventListener('DOMContentLoaded', function() { container.innerHTML = timelineHTML; } + + // 添加分页加载更多功能 + let currentPage = 1; + + function loadMoreActivities(type) { + currentPage++; + const timelineContainer = document.getElementById('activityTimeline'); + + // 显示加载中指示器 + const loadingIndicator = document.createElement('div'); + loadingIndicator.className = 'text-center mt-3 mb-3'; + loadingIndicator.id = 'loadingMoreIndicator'; + loadingIndicator.innerHTML = ` +
+ Loading... +
+ 加载更多... + `; + + timelineContainer.appendChild(loadingIndicator); + + fetch(`/user/api/activities?type=${type}&page=${currentPage}`) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + // 移除加载指示器 + const indicator = document.getElementById('loadingMoreIndicator'); + if (indicator) indicator.remove(); + + if (data.activities && data.activities.length > 0) { + // 添加新活动到现有时间线 + appendActivitiesToTimeline(data.activities, timelineContainer); + + // 如果已经是最后一页,隐藏加载更多按钮 + if (data.current_page >= data.pages) { + const loadMoreBtn = document.getElementById('loadMoreBtn'); + if (loadMoreBtn) loadMoreBtn.style.display = 'none'; + } + } else { + // 没有更多活动 + const noMoreDiv = document.createElement('div'); + noMoreDiv.className = 'text-center p-3 text-muted'; + noMoreDiv.textContent = '没有更多活动记录'; + timelineContainer.appendChild(noMoreDiv); + + const loadMoreBtn = document.getElementById('loadMoreBtn'); + if (loadMoreBtn) loadMoreBtn.style.display = 'none'; + } + }) + .catch(error => { + console.error('Error loading more activities:', error); + // 移除加载指示器 + const indicator = document.getElementById('loadingMoreIndicator'); + if (indicator) indicator.remove(); + + // 显示错误消息 + const errorDiv = document.createElement('div'); + errorDiv.className = 'text-center p-3 text-danger'; + errorDiv.textContent = '加载更多活动失败'; + timelineContainer.appendChild(errorDiv); + }); + } + + // 将新活动附加到现有时间线 + function appendActivitiesToTimeline(activities, container) { + // 移除之前的 "last" 类 + const lastItems = container.querySelectorAll('.timeline-item.last'); + lastItems.forEach(item => { + item.classList.remove('last'); + }); + + activities.forEach((activity, index) => { + let iconClass = 'fas fa-info'; + + if (activity.type === 'login') { + iconClass = 'fas fa-sign-in-alt'; + } else if (activity.type === 'borrow') { + iconClass = 'fas fa-book'; + } else if (activity.type === 'return') { + iconClass = 'fas fa-undo'; + } else if (activity.type === 'other') { + iconClass = 'fas fa-cog'; + } + + const isLast = index === activities.length - 1; + + const timelineItem = document.createElement('div'); + timelineItem.className = `timeline-item ${isLast ? 'last' : ''} timeline-type-${activity.type}`; + + timelineItem.innerHTML = ` +
+ +
+
+
+
${activity.title}
+
${activity.time}
+
+
+ ${activity.details} + ${activity.ip ? `
IP: ${activity.ip}
` : ''} + ${activity.target_type && activity.target_id ? ` +
+ ${activity.target_type === 'book' ? '图书ID: ' : '目标ID: '}${activity.target_id} +
` : ''} +
+
+ `; + + container.appendChild(timelineItem); + }); + + // 如果显示了加载更多按钮,确保它在末尾 + const loadMoreBtn = document.getElementById('loadMoreBtn'); + if (loadMoreBtn) { + container.appendChild(loadMoreBtn); + } + } + + // 添加加载更多按钮点击事件 + document.body.addEventListener('click', function(e) { + if (e.target && e.target.id === 'loadMoreBtn') { + const activityFilter = document.getElementById('activityFilter'); + loadMoreActivities(activityFilter ? activityFilter.value : 'all'); + } + }); }); diff --git a/app/templates/base.html b/app/templates/base.html index 1ca92cd..401b04b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -171,7 +171,6 @@
个人中心 - 设置 退出登录