新增个人中心页面,在注册后填写班级功能
This commit is contained in:
75
accounts/templates/accounts/class_info.html
Normal file
75
accounts/templates/accounts/class_info.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>填写班级信息</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #f5f6fa; }
|
||||
.container { max-width: 400px; margin: 15vh auto; padding: 24px; background: #fff; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
||||
h1 { font-size: 20px; margin: 0 0 16px; text-align: center; }
|
||||
label { display:block; margin: 12px 0 6px; color:#333; }
|
||||
input { width:100%; padding:10px 0px; border:1px solid #dcdde1; border-radius:6px; box-sizing: border-box; }
|
||||
button { width:100%; margin-top:16px; padding:10px 12px; background:#2d8cf0; color:#fff; border:none; border-radius:6px; cursor:pointer; }
|
||||
button:disabled { background:#9bbcf0; cursor:not-allowed; }
|
||||
.error { color:#d93025; margin-top:10px; min-height:20px; font-size: 14px; }
|
||||
.hint { color:#888; font-size:12px; margin-top:10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>完善班级信息</h1>
|
||||
<form id="classForm">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" id="user_id" value="{{ user_id }}">
|
||||
<label for="class_name">所在班级</label>
|
||||
<input id="class_name" name="class_name" type="text" placeholder="例:2024级计算机专业1班" required />
|
||||
<div class="hint">格式要求:XXXX级YY专业Z班(记得加“专业”两个字)</div>
|
||||
<button id="submitBtn" type="submit">提交并进入登录页</button>
|
||||
<div id="error" class="error"></div>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
||||
|
||||
document.getElementById('classForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const err = document.getElementById('error');
|
||||
err.textContent = '';
|
||||
const className = document.getElementById('class_name').value.trim();
|
||||
const userId = document.getElementById('user_id').value;
|
||||
|
||||
// 正则校验:2024级**专业*班
|
||||
const pattern = /^\d{4}级.+专业\d+班$/;
|
||||
if (!pattern.test(className)) {
|
||||
err.textContent = '班级格式不正确,请重新输入(例:2024级计算机专业1班)';
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
const resp = await fetch('/accounts/class-info/submit/', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrftoken || ''
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId, class_name: className })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok || !data.ok) {
|
||||
throw new Error(data.message || '提交失败');
|
||||
}
|
||||
window.location.href = '/accounts/login/';
|
||||
} catch (e) {
|
||||
err.textContent = e.message || '发生错误';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
129
accounts/templates/accounts/profile.html
Normal file
129
accounts/templates/accounts/profile.html
Normal file
@@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>个人中心</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #f5f6fa; margin: 0; }
|
||||
/* 侧边栏样式 */
|
||||
.sidebar { position: fixed; top: 0; left: 0; width: 180px; height: 100vh; background: #1e1e2e; color: white; padding: 20px; box-shadow: 2px 0 5px rgba(0,0,0,0.1); z-index: 1000; display: flex; flex-direction: column; align-items: center; }
|
||||
.user-id-sidebar { text-align: center; margin-bottom: 0px; }
|
||||
.sidebar h3 { margin-top: 0; font-size: 18px; color: #add8e6; text-align: center; margin-bottom: 20px; }
|
||||
.navigation-links { width: 100%; margin-top: 60px; }
|
||||
.sidebar a { display: block; color: #8be9fd; text-decoration: none; margin: 10px 0; font-size: 16px; padding: 15px; border-radius: 4px; transition: all 0.2s ease; }
|
||||
.sidebar a:hover { color: #ff79c6; background-color: rgba(139, 233, 253, 0.2); }
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content { margin-left: 220px; padding: 40px; }
|
||||
.profile-card { background: #fff; border-radius: 14px; box-shadow: 0 10px 24px rgba(31,35,40,0.08); padding: 30px; margin-bottom: 30px; }
|
||||
.profile-header { display: flex; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 20px; }
|
||||
.profile-info h2 { margin: 0; color: #1e1e2e; }
|
||||
.profile-info p { margin: 5px 0; color: #666; }
|
||||
.label { font-weight: bold; color: #333; margin-right: 10px; }
|
||||
|
||||
.section-title { font-size: 20px; font-weight: bold; margin-bottom: 20px; color: #1e1e2e; }
|
||||
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
|
||||
.image-item { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05); transition: transform 0.2s; }
|
||||
.image-item:hover { transform: translateY(-5px); }
|
||||
.image-item img { width: 100%; height: 150px; object-fit: cover; cursor: pointer; }
|
||||
.image-item .info { padding: 10px; font-size: 12px; color: #888; text-align: center; }
|
||||
|
||||
.no-data { text-align: center; color: #999; padding: 40px; }
|
||||
|
||||
/* 图片放大模态框 */
|
||||
.image-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: none; align-items: center; justify-content: center; z-index: 2000; }
|
||||
.image-modal-content { max-width: 90%; max-height: 90%; border-radius: 8px; }
|
||||
.image-modal-close { position: absolute; top: 20px; right: 30px; color: white; font-size: 40px; font-weight: bold; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar">
|
||||
<div class="user-id-sidebar">
|
||||
<h3>你好,{{ username|default:"访客" }}</h3>
|
||||
</div>
|
||||
<div class="navigation-links">
|
||||
<a href="{% url 'main:home' %}">返回主页</a>
|
||||
<a id="logoutBtn" style="cursor:pointer;">退出登录</a>
|
||||
{% csrf_token %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="profile-card">
|
||||
<div class="profile-header">
|
||||
<div class="profile-info">
|
||||
<h2>个人信息</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="profile-details">
|
||||
<p><span class="label">用户名:</span> {{ profile_user.username }}</p>
|
||||
<p><span class="label">用户ID:</span> {{ profile_user.user_id }}</p>
|
||||
<p><span class="label">所属班级:</span> {{ user_class|default:"未填写" }}</p>
|
||||
<p><span class="label">权限级别:</span> {{ permission_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">我的提交</div>
|
||||
{% if achievements %}
|
||||
<div class="image-grid">
|
||||
{% for item in achievements %}
|
||||
<div class="image-item">
|
||||
{% if item.image_url %}
|
||||
<img src="{{ item.image_url }}" alt="提交的图片" onclick="openModal(this.src)">
|
||||
{% else %}
|
||||
<div style="height: 150px; background: #eee; display: flex; align-items: center; justify-content: center; color: #ccc;">无图片</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="profile-card no-data">
|
||||
<p>你还没有提交过任何图片。</p>
|
||||
<a href="/elastic/upload/" style="color: #2d8cf0; text-decoration: none;">去上传第一张图片吧!</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 图片放大模态框 -->
|
||||
<div id="imageModal" class="image-modal">
|
||||
<span class="image-modal-close" onclick="closeModal()">×</span>
|
||||
<img id="modalImg" class="image-modal-content">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
||||
|
||||
// 登出功能
|
||||
document.getElementById('logoutBtn').addEventListener('click', async () => {
|
||||
if(!confirm('确定要退出登录吗?')) return;
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
try {
|
||||
const resp = await fetch('/accounts/logout/', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRFToken': csrftoken || '' }
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.ok) window.location.href = data.redirect_url;
|
||||
} catch (e) { alert('登出失败'); }
|
||||
});
|
||||
|
||||
// 图片放大功能
|
||||
function openModal(src) {
|
||||
const modal = document.getElementById('imageModal');
|
||||
const modalImg = document.getElementById('modalImg');
|
||||
modal.style.display = "flex";
|
||||
modalImg.src = src;
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('imageModal').style.display = "none";
|
||||
}
|
||||
|
||||
window.onclick = function(event) {
|
||||
const modal = document.getElementById('imageModal');
|
||||
if (event.target == modal) closeModal();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,4 +12,7 @@ urlpatterns = [
|
||||
path("register/", views.register_page, name="register"),
|
||||
path("register/submit/", views.register_submit, name="register_submit"),
|
||||
path("email/send-code/", views.send_email_code, name="send_email_code"),
|
||||
path("class-info/", views.class_info_page, name="class_info"),
|
||||
path("class-info/submit/", views.class_info_submit, name="class_info_submit"),
|
||||
path("profile/", views.profile_page, name="profile"),
|
||||
]
|
||||
@@ -15,7 +15,7 @@ from django.conf import settings
|
||||
|
||||
from .es_client import get_user_by_username
|
||||
from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password, generate_rsa_private_pem_b64, public_spki_b64_from_private_pem_b64, rsa_oaep_decrypt_b64_with_private_pem
|
||||
from elastic.es_connect import get_registration_code, get_user_by_username as es_get_user_by_username, get_all_users as es_get_all_users, write_user_data
|
||||
from elastic.es_connect import get_registration_code, get_user_by_username as es_get_user_by_username, get_all_users as es_get_all_users, write_user_data, update_user_by_id, get_user_by_id
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -71,6 +71,40 @@ def set_session_key(request):
|
||||
request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii")
|
||||
return JsonResponse({"ok": True})
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@ensure_csrf_cookie
|
||||
def profile_page(request):
|
||||
session_user_id = request.session.get("user_id")
|
||||
if session_user_id is None:
|
||||
return redirect("/accounts/login/")
|
||||
|
||||
# 获取用户信息
|
||||
user = get_user_by_id(session_user_id)
|
||||
if not user:
|
||||
return redirect("/accounts/login/")
|
||||
|
||||
# 获取个人提交的成就(图片)
|
||||
from elastic.es_connect import search_all
|
||||
from elastic.views import _attach_image_urls
|
||||
|
||||
raw_results = [r for r in search_all() if str(r.get("writer_id", "")) == str(session_user_id)]
|
||||
achievements = _attach_image_urls(request, raw_results)
|
||||
|
||||
# 提取班级信息 (key 字段中的第一个值)
|
||||
keys = user.get("key") or []
|
||||
user_class = keys[0] if keys else ""
|
||||
|
||||
permission_name = "管理员" if int(user.get("permission", 1)) == 0 else "普通用户"
|
||||
|
||||
context = {
|
||||
"username": request.session.get("username"),
|
||||
"profile_user": user,
|
||||
"user_class": user_class,
|
||||
"permission_name": permission_name,
|
||||
"achievements": achievements,
|
||||
}
|
||||
return render(request, "accounts/profile.html", context)
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@csrf_protect
|
||||
def secure_login_submit(request):
|
||||
@@ -238,7 +272,43 @@ def register_submit(request):
|
||||
del request.session["email_verify"]
|
||||
except Exception:
|
||||
pass
|
||||
return JsonResponse({"ok": True, "redirect_url": "/accounts/login/"})
|
||||
# 修改:注册成功后跳转到完善班级信息页面
|
||||
return JsonResponse({"ok": True, "redirect_url": f"/accounts/class-info/?user_id={next_id}"})
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@ensure_csrf_cookie
|
||||
def class_info_page(request):
|
||||
user_id = request.GET.get("user_id")
|
||||
if not user_id:
|
||||
return redirect("/accounts/register/")
|
||||
return render(request, "accounts/class_info.html", {"user_id": user_id})
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@csrf_protect
|
||||
def class_info_submit(request):
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest("Invalid JSON")
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
class_name = (payload.get("class_name") or "").strip()
|
||||
|
||||
if not user_id or not class_name:
|
||||
return HttpResponseBadRequest("Missing fields")
|
||||
|
||||
# 后端校验:2024级**专业*班
|
||||
import re
|
||||
pattern = r"^\d{4}级.+专业\d+班$"
|
||||
if not re.match(pattern, class_name):
|
||||
return JsonResponse({"ok": False, "message": "班级格式不正确"}, status=400)
|
||||
|
||||
# 更新用户信息,将班级信息存入 key 列表
|
||||
ok = update_user_by_id(user_id, key=[class_name])
|
||||
if not ok:
|
||||
return JsonResponse({"ok": False, "message": "保存班级信息失败"}, status=500)
|
||||
|
||||
return JsonResponse({"ok": True})
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@csrf_protect
|
||||
|
||||
@@ -797,7 +797,7 @@ def delete_user_by_id(user_id):
|
||||
print(f"删除用户失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def update_user_by_id(user_id, username=None, permission=None, password=None):
|
||||
def update_user_by_id(user_id, username=None, permission=None, password=None, key=None):
|
||||
try:
|
||||
search = UserDocument.search()
|
||||
search = search.query("term", user_id=int(user_id))
|
||||
@@ -813,6 +813,8 @@ def update_user_by_id(user_id, username=None, permission=None, password=None):
|
||||
salt_b64, hash_b64 = hash_password_random_salt(str(password))
|
||||
doc.password_hash = hash_b64
|
||||
doc.password_salt = salt_b64
|
||||
if key is not None:
|
||||
doc.key = list(key)
|
||||
doc.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<a href="{% url 'elastic:upload_page' %}" onclick="return handleNavClick(this, '/elastic/upload/');">图片上传与识别</a>
|
||||
<a href="{% url 'elastic:manage_page' %}" onclick="return handleNavClick(this, '/elastic/manage/');">数据管理</a>
|
||||
<a href="{% url 'elastic:user_manage' %}" onclick="return handleNavClick(this, '/elastic/user_manage/');">用户管理</a>
|
||||
<a href="/accounts/profile/">个人中心</a>
|
||||
{% if is_admin %}
|
||||
<a href="{% url 'elastic:registration_code_manage_page' %}" onclick="return handleNavClick(this, '/elastic/registration-codes/manage/');">注册码管理</a>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user