新增个人中心页面,在注册后填写班级功能

This commit is contained in:
DSQ
2026-03-08 11:13:33 +08:00
parent 193f739693
commit ee7987aa23
6 changed files with 283 additions and 3 deletions

View 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>

View 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()">&times;</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>

View File

@@ -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"),
]

View File

@@ -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