user_profile_fix

This commit is contained in:
superlishunqin 2025-05-17 15:59:13 +08:00
parent 814422e761
commit a23fa40c82
3 changed files with 279 additions and 69 deletions

View File

@ -10,7 +10,8 @@ from datetime import datetime, timedelta
from app.services.user_service import UserService from app.services.user_service import UserService
from flask_login import login_user, logout_user, current_user, login_required from flask_login import login_user, logout_user, current_user, login_required
from app.models.user import User from app.models.user import User
from app.models.log import Log # 导入日志模型
from app.models.borrow import BorrowRecord # 导入借阅记录模型
# 创建蓝图 # 创建蓝图
user_bp = Blueprint('user', __name__) user_bp = Blueprint('user', __name__)
@ -755,3 +756,116 @@ def add_user():
# GET请求显示添加用户表单 # GET请求显示添加用户表单
return render_template('user/add.html', roles=roles) 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
})

View File

@ -131,22 +131,21 @@ document.addEventListener('DOMContentLoaded', function() {
// 获取用户统计数据 // 获取用户统计数据
function fetchUserStats() { function fetchUserStats() {
// 这里使用虚拟数据,实际应用中应当从后端获取 fetch('/user/api/stats')
// fetch('/api/user/stats') .then(response => {
// .then(response => response.json()) if (!response.ok) {
// .then(data => { throw new Error('Network response was not ok');
// updateUserStats(data); }
// }); return response.json();
})
// 模拟数据 .then(data => {
setTimeout(() => { updateUserStats(data);
const mockData = { })
borrow: 2, .catch(error => {
returned: 15, console.error('Error fetching user stats:', error);
overdue: 0 // 出错时使用默认值
}; updateUserStats({borrow: 0, returned: 0, overdue: 0});
updateUserStats(mockData); });
}, 500);
} }
// 更新用户统计显示 // 更新用户统计显示
@ -175,58 +174,20 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
`; `;
// 实际应用中应当从后端获取 fetch(`/user/api/activities?type=${type}`)
// fetch(`/api/user/activities?type=${type}`) .then(response => {
// .then(response => response.json()) if (!response.ok) {
// .then(data => { throw new Error('Network response was not ok');
// 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);
} }
return response.json();
renderActivityTimeline(filteredActivities, timelineContainer); })
}, 800); .then(data => {
renderActivityTimeline(data.activities, timelineContainer);
})
.catch(error => {
console.error('Error fetching activities:', error);
timelineContainer.innerHTML = '<div class="text-center p-4">获取活动记录失败</div>';
});
} }
// 渲染活动时间线 // 渲染活动时间线
@ -247,6 +208,8 @@ document.addEventListener('DOMContentLoaded', function() {
iconClass = 'fas fa-book'; iconClass = 'fas fa-book';
} else if (activity.type === 'return') { } else if (activity.type === 'return') {
iconClass = 'fas fa-undo'; iconClass = 'fas fa-undo';
} else if (activity.type === 'other') {
iconClass = 'fas fa-cog';
} }
const isLast = index === activities.length - 1; const isLast = index === activities.length - 1;
@ -264,6 +227,10 @@ document.addEventListener('DOMContentLoaded', function() {
<div class="timeline-details"> <div class="timeline-details">
${activity.details} ${activity.details}
${activity.ip ? `<div class="text-muted small">IP: ${activity.ip}</div>` : ''} ${activity.ip ? `<div class="text-muted small">IP: ${activity.ip}</div>` : ''}
${activity.target_type && activity.target_id ? `
<div class="text-muted small">
${activity.target_type === 'book' ? '图书ID: ' : '目标ID: '}${activity.target_id}
</div>` : ''}
</div> </div>
</div> </div>
</div> </div>
@ -272,4 +239,134 @@ document.addEventListener('DOMContentLoaded', function() {
container.innerHTML = timelineHTML; 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 = `
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="sr-only">Loading...</span>
</div>
<span class="ml-2">加载更多...</span>
`;
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 = `
<div class="timeline-icon">
<i class="${iconClass}"></i>
</div>
<div class="timeline-content">
<div class="timeline-header">
<h5 class="timeline-title">${activity.title}</h5>
<div class="timeline-time">${activity.time}</div>
</div>
<div class="timeline-details">
${activity.details}
${activity.ip ? `<div class="text-muted small">IP: ${activity.ip}</div>` : ''}
${activity.target_type && activity.target_id ? `
<div class="text-muted small">
${activity.target_type === 'book' ? '图书ID: ' : '目标ID: '}${activity.target_id}
</div>` : ''}
</div>
</div>
`;
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');
}
});
}); });

View File

@ -171,7 +171,6 @@
</a> </a>
<div class="user-dropdown" id="userDropdown"> <div class="user-dropdown" id="userDropdown">
<a href="{{ url_for('user.user_profile') }}"><i class="fas fa-user-circle"></i> 个人中心</a> <a href="{{ url_for('user.user_profile') }}"><i class="fas fa-user-circle"></i> 个人中心</a>
<a href="#"><i class="fas fa-cog"></i> 设置</a>
<a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a> <a href="{{ url_for('user.logout') }}"><i class="fas fa-sign-out-alt"></i> 退出登录</a>
</div> </div>
</div> </div>