新增个人资料功能
This commit is contained in:
Binary file not shown.
@@ -92,6 +92,11 @@ def get_profile():
|
|||||||
"""获取个人资料API"""
|
"""获取个人资料API"""
|
||||||
user_id = session['user_id']
|
user_id = session['user_id']
|
||||||
result = AuthService.get_user_by_id(user_id)
|
result = AuthService.get_user_by_id(user_id)
|
||||||
|
if result['success']:
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': result['user']
|
||||||
|
})
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
|
|
||||||
@auth_bp.route('/api/profile', methods=['PUT'])
|
@auth_bp.route('/api/profile', methods=['PUT'])
|
||||||
@@ -147,3 +152,57 @@ def check_login():
|
|||||||
'success': True,
|
'success': True,
|
||||||
'logged_in': False
|
'logged_in': False
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@auth_bp.route('/api/profile/stats', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_profile_stats():
|
||||||
|
"""获取用户使用统计API"""
|
||||||
|
user_id = session['user_id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
from src.flask_prompt_master.models.models import Prompt
|
||||||
|
from src.flask_prompt_master.models.favorites import Favorite
|
||||||
|
from sqlalchemy import func
|
||||||
|
|
||||||
|
# 统计生成的提示词数量
|
||||||
|
total_prompts = Prompt.query.filter_by(user_id=user_id).count()
|
||||||
|
|
||||||
|
# 统计收藏数量
|
||||||
|
total_favorites = Favorite.query.filter_by(user_id=str(user_id)).count()
|
||||||
|
|
||||||
|
# 计算活跃天数(从注册时间到现在)
|
||||||
|
from src.flask_prompt_master.models.models import User
|
||||||
|
user = User.query.get(user_id)
|
||||||
|
days_active = 0
|
||||||
|
if user and user.created_time:
|
||||||
|
from datetime import datetime
|
||||||
|
days_active = (datetime.now() - user.created_time).days + 1
|
||||||
|
|
||||||
|
# 获取最近活动
|
||||||
|
recent_prompts = Prompt.query.filter_by(user_id=user_id)\
|
||||||
|
.order_by(Prompt.created_at.desc())\
|
||||||
|
.limit(5).all()
|
||||||
|
|
||||||
|
recent_activities = []
|
||||||
|
for prompt in recent_prompts:
|
||||||
|
recent_activities.append({
|
||||||
|
'type': '生成提示词',
|
||||||
|
'content': prompt.input_text[:30] + '...' if len(prompt.input_text) > 30 else prompt.input_text,
|
||||||
|
'time': prompt.created_at.strftime('%Y-%m-%d %H:%M:%S') if prompt.created_at else ''
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'data': {
|
||||||
|
'total_prompts': total_prompts,
|
||||||
|
'total_favorites': total_favorites,
|
||||||
|
'days_active': days_active,
|
||||||
|
'recent_activities': recent_activities
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'获取统计信息失败: {str(e)}'
|
||||||
|
})
|
||||||
|
|||||||
408
src/flask_prompt_master/templates/auth/profile.html
Normal file
408
src/flask_prompt_master/templates/auth/profile.html
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}个人资料 - 提示词大师{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<div class="row">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">个人中心</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="nav flex-column nav-pills" id="profile-tab" role="tablist">
|
||||||
|
<button class="nav-link active" id="profile-info-tab" data-bs-toggle="pill" data-bs-target="#profile-info" type="button" role="tab">
|
||||||
|
<i class="fas fa-user me-2"></i>基本信息
|
||||||
|
</button>
|
||||||
|
<button class="nav-link" id="change-password-tab" data-bs-toggle="pill" data-bs-target="#change-password" type="button" role="tab">
|
||||||
|
<i class="fas fa-lock me-2"></i>修改密码
|
||||||
|
</button>
|
||||||
|
<button class="nav-link" id="usage-stats-tab" data-bs-toggle="pill" data-bs-target="#usage-stats" type="button" role="tab">
|
||||||
|
<i class="fas fa-chart-bar me-2"></i>使用统计
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<div class="col-md-9">
|
||||||
|
<div class="tab-content" id="profile-tabContent">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="tab-pane fade show active" id="profile-info" role="tabpanel">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">基本信息</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="profile-form">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="nickname" class="form-label">昵称 <span class="text-danger">*</span></label>
|
||||||
|
<input type="text" class="form-control" id="nickname" name="nickname" required>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="login_name" class="form-label">用户名</label>
|
||||||
|
<input type="text" class="form-control" id="login_name" readonly>
|
||||||
|
<small class="text-muted">用户名不可修改</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="email" class="form-label">邮箱</label>
|
||||||
|
<input type="email" class="form-control" id="email" name="email">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="mobile" class="form-label">手机号</label>
|
||||||
|
<input type="tel" class="form-control" id="mobile" name="mobile">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="sex" class="form-label">性别</label>
|
||||||
|
<select class="form-select" id="sex" name="sex">
|
||||||
|
<option value="0">保密</option>
|
||||||
|
<option value="1">男</option>
|
||||||
|
<option value="2">女</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="avatar" class="form-label">头像</label>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<img id="avatar-preview" src="/static/images/default-avatar.png" alt="头像" class="rounded-circle me-3" style="width: 60px; height: 60px; object-fit: cover;">
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="document.getElementById('avatar-input').click()">
|
||||||
|
更换头像
|
||||||
|
</button>
|
||||||
|
<input type="file" id="avatar-input" accept="image/*" style="display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="created_time" class="form-label">注册时间</label>
|
||||||
|
<input type="text" class="form-control" id="created_time" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="last_login" class="form-label">最后登录</label>
|
||||||
|
<input type="text" class="form-control" id="last_login" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save me-2"></i>保存修改
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改密码 -->
|
||||||
|
<div class="tab-pane fade" id="change-password" role="tabpanel">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">修改密码</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form id="password-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="old_password" class="form-label">当前密码 <span class="text-danger">*</span></label>
|
||||||
|
<input type="password" class="form-control" id="old_password" name="old_password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="new_password" class="form-label">新密码 <span class="text-danger">*</span></label>
|
||||||
|
<input type="password" class="form-control" id="new_password" name="new_password" required>
|
||||||
|
<div class="form-text">密码长度至少6位,建议包含字母、数字和特殊字符</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirm_password" class="form-label">确认新密码 <span class="text-danger">*</span></label>
|
||||||
|
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-warning">
|
||||||
|
<i class="fas fa-key me-2"></i>修改密码
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用统计 -->
|
||||||
|
<div class="tab-pane fade" id="usage-stats" role="tabpanel">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4 class="mb-0">使用统计</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card bg-primary text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 id="total-prompts">0</h3>
|
||||||
|
<p class="mb-0">生成提示词</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card bg-success text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 id="total-favorites">0</h3>
|
||||||
|
<p class="mb-0">收藏数量</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<div class="card bg-info text-white">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<h3 id="days-active">0</h3>
|
||||||
|
<p class="mb-0">活跃天数</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h5>最近活动</h5>
|
||||||
|
<div id="recent-activity">
|
||||||
|
<p class="text-muted">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 成功提示模态框 -->
|
||||||
|
<div class="modal fade" id="successModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">操作成功</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="success-message">操作已成功完成</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误提示模态框 -->
|
||||||
|
<div class="modal fade" id="errorModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">操作失败</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="error-message">操作失败,请重试</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// 页面加载完成后执行
|
||||||
|
$(document).ready(function() {
|
||||||
|
loadProfile();
|
||||||
|
loadUsageStats();
|
||||||
|
|
||||||
|
// 基本信息表单提交
|
||||||
|
$('#profile-form').submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
updateProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改密码表单提交
|
||||||
|
$('#password-form').submit(function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
changePassword();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 头像上传
|
||||||
|
$('#avatar-input').change(function() {
|
||||||
|
const file = this.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
$('#avatar-preview').attr('src', e.target.result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载个人资料
|
||||||
|
function loadProfile() {
|
||||||
|
$.get('/api/profile')
|
||||||
|
.done(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
const user = response.data;
|
||||||
|
$('#nickname').val(user.nickname || '');
|
||||||
|
$('#login_name').val(user.login_name || '');
|
||||||
|
$('#email').val(user.email || '');
|
||||||
|
$('#mobile').val(user.mobile || '');
|
||||||
|
$('#sex').val(user.sex || 0);
|
||||||
|
$('#created_time').val(user.created_time || '');
|
||||||
|
$('#last_login').val(user.last_login || '');
|
||||||
|
|
||||||
|
if (user.avatar) {
|
||||||
|
$('#avatar-preview').attr('src', user.avatar);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(response.message || '加载个人资料失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
showError('加载个人资料失败');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新个人资料
|
||||||
|
function updateProfile() {
|
||||||
|
const formData = {
|
||||||
|
nickname: $('#nickname').val(),
|
||||||
|
email: $('#email').val(),
|
||||||
|
mobile: $('#mobile').val(),
|
||||||
|
sex: parseInt($('#sex').val())
|
||||||
|
};
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/profile',
|
||||||
|
method: 'PUT',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
.done(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
showSuccess('个人资料更新成功');
|
||||||
|
// 更新session中的昵称
|
||||||
|
if (formData.nickname) {
|
||||||
|
// 这里可以更新页面上的用户昵称显示
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(response.message || '更新失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
showError('更新失败,请重试');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改密码
|
||||||
|
function changePassword() {
|
||||||
|
const oldPassword = $('#old_password').val();
|
||||||
|
const newPassword = $('#new_password').val();
|
||||||
|
const confirmPassword = $('#confirm_password').val();
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
showError('两次输入的新密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
showError('新密码长度至少6位');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/change-password',
|
||||||
|
method: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
old_password: oldPassword,
|
||||||
|
new_password: newPassword
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.done(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
showSuccess('密码修改成功,请重新登录');
|
||||||
|
// 清空表单
|
||||||
|
$('#password-form')[0].reset();
|
||||||
|
// 可以选择跳转到登录页面
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = '/login';
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
showError(response.message || '密码修改失败');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
showError('密码修改失败,请重试');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载使用统计
|
||||||
|
function loadUsageStats() {
|
||||||
|
$.get('/api/profile/stats')
|
||||||
|
.done(function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
const data = response.data;
|
||||||
|
$('#total-prompts').text(data.total_prompts || 0);
|
||||||
|
$('#total-favorites').text(data.total_favorites || 0);
|
||||||
|
$('#days-active').text(data.days_active || 0);
|
||||||
|
|
||||||
|
// 加载最近活动
|
||||||
|
let activityHtml = '<div class="list-group">';
|
||||||
|
if (data.recent_activities && data.recent_activities.length > 0) {
|
||||||
|
data.recent_activities.forEach(activity => {
|
||||||
|
activityHtml += `
|
||||||
|
<div class="list-group-item">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>${activity.type}: ${activity.content}</span>
|
||||||
|
<small class="text-muted">${activity.time}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
activityHtml += '<div class="list-group-item"><span class="text-muted">暂无活动记录</span></div>';
|
||||||
|
}
|
||||||
|
activityHtml += '</div>';
|
||||||
|
$('#recent-activity').html(activityHtml);
|
||||||
|
} else {
|
||||||
|
$('#recent-activity').html('<p class="text-danger">加载统计信息失败</p>');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
$('#recent-activity').html('<p class="text-danger">加载统计信息失败</p>');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成功提示
|
||||||
|
function showSuccess(message) {
|
||||||
|
$('#success-message').text(message);
|
||||||
|
new bootstrap.Modal(document.getElementById('successModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误提示
|
||||||
|
function showError(message) {
|
||||||
|
$('#error-message').text(message);
|
||||||
|
new bootstrap.Modal(document.getElementById('errorModal')).show();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -258,6 +258,10 @@
|
|||||||
<i class="fas fa-heart"></i>
|
<i class="fas fa-heart"></i>
|
||||||
我的收藏
|
我的收藏
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{{ url_for('auth.profile_page') }}" class="nav-link">
|
||||||
|
<i class="fas fa-user"></i>
|
||||||
|
个人资料
|
||||||
|
</a>
|
||||||
<div class="user-menu" id="userMenu">
|
<div class="user-menu" id="userMenu">
|
||||||
<!-- 用户菜单将通过JavaScript动态加载 -->
|
<!-- 用户菜单将通过JavaScript动态加载 -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user