user_profile_fix
This commit is contained in:
parent
814422e761
commit
a23fa40c82
@ -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
|
||||
})
|
||||
|
||||
@ -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() {
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 实际应用中应当从后端获取
|
||||
// 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 = '<div class="text-center p-4">获取活动记录失败</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染活动时间线
|
||||
@ -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() {
|
||||
<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>
|
||||
</div>
|
||||
@ -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 = `
|
||||
<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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -171,7 +171,6 @@
|
||||
</a>
|
||||
<div class="user-dropdown" id="userDropdown">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user