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 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
|
||||||
|
})
|
||||||
|
|||||||
@ -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'
|
|
||||||
}
|
}
|
||||||
];
|
return response.json();
|
||||||
|
})
|
||||||
// 根据筛选条件过滤活动
|
.then(data => {
|
||||||
let filteredActivities = mockActivities;
|
renderActivityTimeline(data.activities, timelineContainer);
|
||||||
if (type !== 'all') {
|
})
|
||||||
filteredActivities = mockActivities.filter(activity => activity.type === type);
|
.catch(error => {
|
||||||
}
|
console.error('Error fetching activities:', error);
|
||||||
|
timelineContainer.innerHTML = '<div class="text-center p-4">获取活动记录失败</div>';
|
||||||
renderActivityTimeline(filteredActivities, timelineContainer);
|
});
|
||||||
}, 800);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染活动时间线
|
// 渲染活动时间线
|
||||||
@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user