Files
zlqy/templates/profile.html
2026-02-27 10:37:11 +08:00

549 lines
36 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}个人中心 - 智联青云{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto space-y-6">
<!-- 顶部横幅与头像:高级游戏化设计 -->
<div class="bg-white rounded-3xl shadow-xl border border-slate-100 overflow-hidden relative group">
<div class="h-48 bg-gradient-to-r from-indigo-600 via-purple-600 to-blue-600 relative overflow-hidden">
<!-- 高级光影动画背景 -->
<div class="absolute inset-0 bg-[url('data:image/svg+xml,%3Csvg width=\'40\' height=\'40\' viewBox=\'0 0 40 40\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cpath d=\'M20 20.5V18H0v-2h20v-2H0v-2h20v-2H0V8h20V6H0V4h20V2H0V0h22v20h2V0h2v20h2V0h2v20h2V0h2v20h2V0h2v20h2v2H20v-1.5zM0 20h2v20H0V20zm4 0h2v20H4V20zm4 0h2v20H8V20zm4 0h2v20h-2V20zm4 0h2v20h-2V20zm4 4h20v2H20v-2zm0 4h20v2H20v-2zm0 4h20v2H20v-2zm0 4h20v2H20v-2z\' fill=\'%23ffffff\' fill-opacity=\'0.05\' fill-rule=\'evenodd\'/%3E%3C/svg%3E')] opacity-50"></div>
<div class="absolute top-0 right-0 w-96 h-96 bg-white/20 blur-3xl rounded-full translate-x-1/2 -translate-y-1/2 transform group-hover:scale-110 transition-transform duration-1000"></div>
<div class="absolute bottom-0 left-0 w-64 h-64 bg-indigo-500/30 blur-3xl rounded-full -translate-x-1/2 translate-y-1/2"></div>
</div>
<div class="px-6 pb-8 sm:px-12 relative">
<div class="flex flex-col sm:flex-row items-center sm:items-end -mt-20 sm:-mt-24 sm:space-x-8">
<!-- 头像区域(呼吸发光效果) -->
<div class="relative group/avatar">
<div class="absolute inset-0 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full blur-md opacity-40 group-hover/avatar:opacity-60 animate-pulse transition-opacity"></div>
<div id="avatar-display" class="relative w-36 h-36 rounded-full overflow-hidden border-4 border-white shadow-xl flex items-center justify-center bg-gradient-to-br from-indigo-50 to-blue-50 text-indigo-600 text-5xl font-black cursor-pointer transform group-hover/avatar:scale-105 group-hover/avatar:rotate-3 transition-all duration-300 z-10" onclick="uploadAvatar()">
{% if profile_user.avatar %}
<img src="{{ profile_user.avatar }}" class="w-full h-full object-cover" id="avatar-img">
{% else %}
<span id="avatar-letter">{{ profile_user.name[0]|upper }}</span>
{% endif %}
</div>
{% if profile_user.id == user.id %}
<div class="absolute bottom-1 right-1 bg-white p-2.5 rounded-full shadow-lg border-2 border-slate-100 cursor-pointer text-slate-500 hover:text-indigo-600 hover:border-indigo-200 transition-all z-20 transform hover:scale-110" onclick="uploadAvatar()">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</div>
{% endif %}
<!-- 浮动等级徽章 -->
<div class="absolute -top-2 -right-2 bg-slate-900 text-white text-xs font-black px-3 py-1.5 rounded-full shadow-lg border-2 border-white z-20 transform -rotate-12 group-hover/avatar:rotate-0 transition-transform">
Lv.{{ level }}
</div>
</div>
<!-- 个人信息 -->
<div class="mt-5 sm:mt-0 text-center sm:text-left flex-1 pb-2">
<h1 class="text-4xl font-extrabold text-slate-900 flex items-center justify-center sm:justify-start gap-4 tracking-tight drop-shadow-sm">
{{ get_display_name(profile_user.id, profile_user.name) if profile_user.id == user.id else profile_user.name }}
{% if profile_user.role == 'admin' %}
<span class="px-3 py-1 rounded-xl text-xs font-bold bg-gradient-to-r from-red-500 to-rose-600 text-white shadow-sm shadow-red-200 transform hover:scale-105 transition-transform">管理员</span>
{% elif profile_user.role == 'teacher' %}
<span class="px-3 py-1 rounded-xl text-xs font-bold bg-gradient-to-r from-blue-500 to-indigo-600 text-white shadow-sm shadow-blue-200 transform hover:scale-105 transition-transform flex items-center gap-1">👨‍🏫 认证教师</span>
{% else %}
<span class="px-3 py-1 rounded-xl text-xs font-bold bg-gradient-to-r from-emerald-400 to-teal-500 text-white shadow-sm shadow-emerald-200 transform hover:scale-105 transition-transform flex items-center gap-1">🎓 学生</span>
{% endif %}
</h1>
<div class="flex flex-wrap items-center justify-center sm:justify-start gap-5 mt-4 text-sm font-medium text-slate-500">
{% if profile_user.email %}
<span class="flex items-center gap-1.5 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100 hover:bg-slate-100 transition-colors"><svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>{{ profile_user.email }}</span>
{% endif %}
{% if profile_user.phone %}
<span class="flex items-center gap-1.5 bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-100 hover:bg-slate-100 transition-colors"><svg class="w-4 h-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/></svg>{{ profile_user.phone }}</span>
{% endif %}
<span class="flex items-center gap-1.5 bg-indigo-50 text-indigo-600 px-3 py-1.5 rounded-lg border border-indigo-100"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>加入于 {{ profile_user.created_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- 左侧:统计与信息 -->
<div class="space-y-6 lg:col-span-1">
<!-- 游戏化数据统计Bento 风格) -->
<div class="bg-white rounded-3xl p-6 shadow-sm border border-slate-100 relative overflow-hidden group">
<div class="absolute top-0 right-0 w-32 h-32 bg-indigo-50 rounded-bl-full -z-10 group-hover:scale-110 transition-transform duration-500"></div>
<h3 class="text-xl font-extrabold text-slate-900 mb-5 flex items-center">
<span class="w-8 h-8 bg-indigo-100 text-indigo-600 rounded-xl flex items-center justify-center mr-3 shadow-sm">📊</span>
活跃数据
</h3>
<!-- 积分进度条 -->
<div class="mb-6 bg-slate-50 rounded-2xl p-4 border border-slate-100">
<div class="flex justify-between text-xs font-bold text-slate-600 mb-2">
<span>当前等级进度</span>
<span class="text-indigo-600">{{ points }} / {{ (level * 100) }} XP</span>
</div>
<div class="h-3 w-full bg-slate-200 rounded-full overflow-hidden shadow-inner relative">
{% set progress = (points / (level * 100) * 100) | int %}
{% set p_width = progress if progress <= 100 else 100 %}
<div class="absolute top-0 left-0 h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full transition-all duration-1000 ease-out overflow-hidden" style="width: {{ p_width }}%;">
<div class="absolute inset-0 bg-white/30 w-full h-full transform -skew-x-12 translate-x-full" style="animation: shimmer 2s infinite;"></div>
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="bg-gradient-to-br from-indigo-50 to-blue-50 rounded-2xl p-4 text-center border border-indigo-100/50 hover:shadow-md hover:-translate-y-1 transition-all duration-300">
<div class="text-3xl font-black text-indigo-600 drop-shadow-sm">{{ points }}</div>
<div class="text-[11px] font-bold text-indigo-400 uppercase tracking-widest mt-1">总积分</div>
</div>
<div class="bg-gradient-to-br from-emerald-50 to-teal-50 rounded-2xl p-4 text-center border border-emerald-100/50 hover:shadow-md hover:-translate-y-1 transition-all duration-300">
<div class="text-3xl font-black text-emerald-600 drop-shadow-sm">{{ post_count }}</div>
<div class="text-[11px] font-bold text-emerald-500 uppercase tracking-widest mt-1">发帖数</div>
</div>
<div class="bg-gradient-to-br from-amber-50 to-orange-50 rounded-2xl p-4 text-center border border-amber-100/50 hover:shadow-md hover:-translate-y-1 transition-all duration-300">
<div class="text-3xl font-black text-amber-600 drop-shadow-sm">{{ reply_count }}</div>
<div class="text-[11px] font-bold text-amber-500 uppercase tracking-widest mt-1">回复数</div>
</div>
<div class="bg-gradient-to-br from-rose-50 to-pink-50 rounded-2xl p-4 text-center border border-rose-100/50 hover:shadow-md hover:-translate-y-1 transition-all duration-300">
<div class="text-3xl font-black text-rose-500 drop-shadow-sm">{{ likes_received }}</div>
<div class="text-[11px] font-bold text-rose-400 uppercase tracking-widest mt-1">获赞数</div>
</div>
</div>
</div>
<!-- 账号设置 -->
{% if profile_user.id == user.id %}
<div class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<h3 class="text-lg font-bold text-slate-900 mb-4 flex items-center">
<svg class="w-5 h-5 mr-2 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
账号设置
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between p-3 bg-slate-50 rounded-xl">
<div>
<div class="text-sm font-medium text-slate-900">用户名</div>
<div class="text-xs text-slate-500 mt-0.5" id="display-username">{{ profile_user.name }}</div>
</div>
<button onclick="changeName()" class="px-3 py-1.5 text-xs font-medium text-primary bg-blue-50 hover:bg-blue-100 rounded-lg transition-colors">修改</button>
</div>
{% if user.role == 'student' %}
<a href="/apply-teacher" class="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-slate-100 transition-colors group">
<div>
<div class="text-sm font-medium text-slate-900 group-hover:text-primary transition-colors">申请成为老师</div>
<div class="text-xs text-slate-500 mt-0.5">获取发布赛事和考试的权限</div>
</div>
<svg class="w-5 h-5 text-slate-400 group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</a>
{% endif %}
{% if user.role == 'admin' or user.role == 'teacher' %}
<a href="/admin" class="flex items-center justify-between p-3 bg-slate-50 rounded-xl hover:bg-slate-100 transition-colors group">
<div>
<div class="text-sm font-medium text-slate-900 group-hover:text-primary transition-colors">进入管理后台</div>
<div class="text-xs text-slate-500 mt-0.5">管理系统各项数据</div>
</div>
<svg class="w-5 h-5 text-slate-400 group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
<!-- 右侧:主要内容 -->
<div class="space-y-6 lg:col-span-2">
<!-- 快捷入口(玻璃拟物化卡片) -->
{% if profile_user.id == user.id %}
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<a href="/notifications" class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100 hover:shadow-lg hover:border-blue-200 hover:-translate-y-1 transition-all duration-300 text-center group">
<div class="w-12 h-12 mx-auto bg-gradient-to-br from-blue-100 to-indigo-100 rounded-2xl flex items-center justify-center text-blue-600 shadow-inner group-hover:scale-110 group-hover:rotate-6 transition-transform mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
</div>
<div class="text-sm font-bold text-slate-700 group-hover:text-blue-600 transition-colors">通知中心</div>
</a>
<a href="/chat" class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100 hover:shadow-lg hover:border-emerald-200 hover:-translate-y-1 transition-all duration-300 text-center group">
<div class="w-12 h-12 mx-auto bg-gradient-to-br from-emerald-100 to-teal-100 rounded-2xl flex items-center justify-center text-emerald-600 shadow-inner group-hover:scale-110 group-hover:-rotate-6 transition-transform mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/></svg>
</div>
<div class="text-sm font-bold text-slate-700 group-hover:text-emerald-600 transition-colors">我的消息</div>
</a>
<div class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100 hover:shadow-lg hover:border-purple-200 hover:-translate-y-1 transition-all duration-300 text-center group cursor-pointer" onclick="document.getElementById('exam-history-tab').scrollIntoView({behavior:'smooth'})">
<div class="w-12 h-12 mx-auto bg-gradient-to-br from-purple-100 to-fuchsia-100 rounded-2xl flex items-center justify-center text-purple-600 shadow-inner group-hover:scale-110 group-hover:rotate-6 transition-transform mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
</div>
<div class="text-sm font-bold text-slate-700 group-hover:text-purple-600 transition-colors">考试记录</div>
</div>
<div class="bg-white rounded-2xl p-5 shadow-sm border border-slate-100 hover:shadow-lg hover:border-amber-200 hover:-translate-y-1 transition-all duration-300 text-center group cursor-pointer" onclick="document.getElementById('bookmarks-tab').scrollIntoView({behavior:'smooth'})">
<div class="w-12 h-12 mx-auto bg-gradient-to-br from-amber-100 to-orange-100 rounded-2xl flex items-center justify-center text-amber-600 shadow-inner group-hover:scale-110 group-hover:-rotate-6 transition-transform mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"/></svg>
</div>
<div class="text-sm font-bold text-slate-700 group-hover:text-amber-600 transition-colors">我的收藏</div>
</div>
</div>
{% endif %}
<!-- 高级选项卡内容区 -->
<div class="bg-white shadow-sm rounded-3xl border border-slate-100 overflow-hidden">
<!-- 游戏化标签栏 -->
<div class="p-2 bg-slate-50/80 border-b border-slate-100">
<div class="flex gap-2 overflow-x-auto hide-scrollbar">
<button class="flex-1 min-w-[120px] px-6 py-3.5 text-sm font-bold text-indigo-600 bg-white rounded-xl shadow-sm transition-all duration-300 whitespace-nowrap" id="tab-btn-history" onclick="switchTab('history')">📜 考试经历</button>
<button class="flex-1 min-w-[120px] px-6 py-3.5 text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-white/60 bg-transparent rounded-xl transition-all duration-300 whitespace-nowrap" id="tab-btn-posts" onclick="switchTab('posts')">📝 我的帖子</button>
<button class="flex-1 min-w-[120px] px-6 py-3.5 text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-white/60 bg-transparent rounded-xl transition-all duration-300 whitespace-nowrap" id="tab-btn-bookmarks" onclick="switchTab('bookmarks')">⭐ 收藏试卷</button>
<button class="flex-1 min-w-[120px] px-6 py-3.5 text-sm font-medium text-slate-500 hover:text-slate-800 hover:bg-white/60 bg-transparent rounded-xl transition-all duration-300 whitespace-nowrap" id="tab-btn-friends" onclick="switchTab('friends')">👥 好友列表</button>
</div>
</div>
<div class="p-6 min-h-[400px]">
<!-- 考试经历 -->
<div id="tab-content-history" class="space-y-4" id="exam-history-tab">
<div id="exam-history">
<div class="flex justify-center items-center py-12 text-slate-400">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
加载中...
</div>
</div>
</div>
<!-- 我的帖子 -->
<div id="tab-content-posts" class="space-y-4 hidden">
<div id="my-posts"></div>
</div>
<!-- 收藏试卷 -->
<div id="tab-content-bookmarks" class="space-y-4 hidden" id="bookmarks-tab">
<div id="bookmarked-exams"></div>
</div>
<!-- 好友列表 -->
<div id="tab-content-friends" class="space-y-4 hidden">
<!-- 搜索用户 -->
{% if profile_user.id == user.id %}
<div class="bg-slate-50 rounded-xl p-4 space-y-3">
<h4 class="text-sm font-bold text-slate-700">搜索用户</h4>
<div class="flex gap-2">
<input id="friend-search-input" type="text" placeholder="输入用户名搜索..." class="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary">
<button onclick="searchUsers()" class="px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-blue-600 transition-colors">搜索</button>
</div>
<div id="search-results"></div>
</div>
<!-- 好友请求 -->
<div class="bg-amber-50 rounded-xl p-4 space-y-3">
<h4 class="text-sm font-bold text-amber-700">好友请求</h4>
<div id="friend-requests"></div>
</div>
{% endif %}
<!-- 好友列表 -->
<div id="friends-list" class="grid grid-cols-1 sm:grid-cols-2 gap-4"></div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function uploadAvatar() {
const inp = document.createElement('input');
inp.type = 'file'; inp.accept = 'image/*';
inp.onchange = () => { if (inp.files.length) doUploadAvatar(inp.files[0]); };
inp.click();
}
function cameraAvatar() {
const inp = document.createElement('input');
inp.type = 'file'; inp.accept = 'image/*'; inp.capture = 'user';
inp.onchange = () => { if (inp.files.length) doUploadAvatar(inp.files[0]); };
inp.click();
}
async function doUploadAvatar(file) {
if (file.size > 10*1024*1024) { alert('文件不能超过10MB'); return; }
const fd = new FormData();
fd.append('file', file);
try {
const res = await fetch('/api/user/avatar', {method:'POST', body: fd});
const d = await res.json();
if (d.success) {
const display = document.getElementById('avatar-display');
display.innerHTML = `<img src="${d.url}" class="w-full h-full object-cover" id="avatar-img">`;
alert('头像更新成功');
} else { alert(d.message); }
} catch(e) { alert('上传失败'); }
}
async function loadFriends() {
const container = document.getElementById('friends-list');
try {
const res = await fetch('/api/user/friends');
const data = await res.json();
if (!data.success) throw new Error(data.message);
if (data.friends.length === 0) {
container.innerHTML = '<div class="col-span-2 text-center py-4 text-slate-400">暂无好友</div>';
return;
}
let html = '';
data.friends.forEach(f => {
html += `
<div class="flex items-center space-x-3 p-3 border border-slate-100 rounded-xl bg-white">
<div class="w-10 h-10 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-sm font-bold overflow-hidden">
${f.avatar ? `<img src="${f.avatar}" class="w-full h-full object-cover">` : f.name.charAt(0).toUpperCase()}
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-slate-900 truncate">${f.name}</div>
<div class="text-xs text-slate-400">好友 · ${f.created_at}</div>
</div>
<a href="/chat?dm=${f.id}" class="px-3 py-1.5 text-xs bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors font-medium">私聊</a>
</div>`;
});
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="col-span-2 text-center py-4 text-red-500">加载失败</div>';
}
loadFriendRequests();
}
async function searchUsers() {
const q = document.getElementById('friend-search-input').value.trim();
const container = document.getElementById('search-results');
if (!q) { container.innerHTML = ''; return; }
try {
const res = await fetch('/api/users/search?q=' + encodeURIComponent(q));
const data = await res.json();
if (!data.success || data.users.length === 0) {
container.innerHTML = '<div class="text-sm text-slate-400 py-2">未找到用户</div>';
return;
}
let html = '';
data.users.forEach(u => {
let actionBtn = '';
if (u.friend_status === 'accepted') {
actionBtn = '<span class="text-xs text-green-600 font-medium">已是好友</span>';
} else if (u.friend_status === 'pending') {
actionBtn = '<span class="text-xs text-amber-600 font-medium">已申请</span>';
} else {
actionBtn = `<button onclick="addFriend(${u.id}, this)" class="px-3 py-1 text-xs bg-primary text-white rounded-lg hover:bg-blue-600 transition-colors">加好友</button>`;
}
html += `
<div class="flex items-center space-x-3 p-2 rounded-lg hover:bg-white transition-colors">
<div class="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-xs font-bold overflow-hidden">
${u.avatar ? `<img src="${u.avatar}" class="w-full h-full object-cover">` : u.name.charAt(0).toUpperCase()}
</div>
<div class="flex-1 min-w-0"><div class="text-sm font-medium text-slate-800 truncate">${u.name}</div></div>
${actionBtn}
</div>`;
});
container.innerHTML = html;
} catch(e) { container.innerHTML = '<div class="text-sm text-red-500 py-2">搜索失败</div>'; }
}
async function addFriend(userId, btn) {
try {
btn.disabled = true; btn.textContent = '发送中...';
const res = await fetch('/api/friend/add', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({friend_id: userId})});
const data = await res.json();
if (data.success) { btn.outerHTML = '<span class="text-xs text-amber-600 font-medium">已申请</span>'; }
else { alert(data.message); btn.disabled = false; btn.textContent = '加好友'; }
} catch(e) { alert('发送失败'); btn.disabled = false; btn.textContent = '加好友'; }
}
async function loadFriendRequests() {
const container = document.getElementById('friend-requests');
if (!container) return;
try {
const res = await fetch('/api/friend/requests');
const data = await res.json();
if (!data.success || data.requests.length === 0) {
container.innerHTML = '<div class="text-sm text-amber-600/60 py-1">暂无待处理请求</div>';
return;
}
let html = '';
data.requests.forEach(r => {
html += `
<div class="flex items-center space-x-3 p-2 rounded-lg bg-white" id="freq-${r.id}">
<div class="w-8 h-8 bg-slate-200 rounded-full flex items-center justify-center text-slate-600 text-xs font-bold overflow-hidden">
${r.avatar ? `<img src="${r.avatar}" class="w-full h-full object-cover">` : r.name.charAt(0).toUpperCase()}
</div>
<div class="flex-1 min-w-0"><div class="text-sm font-medium text-slate-800 truncate">${r.name}</div></div>
<button onclick="acceptFriend(${r.id})" class="px-2.5 py-1 text-xs bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors">同意</button>
<button onclick="rejectFriend(${r.id})" class="px-2.5 py-1 text-xs bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors">拒绝</button>
</div>`;
});
container.innerHTML = html;
} catch(e) { container.innerHTML = '<div class="text-sm text-red-500 py-1">加载失败</div>'; }
}
async function acceptFriend(id) {
try {
const res = await fetch('/api/friend/accept/' + id, {method:'POST'});
const data = await res.json();
if (data.success) { document.getElementById('freq-' + id).remove(); loadFriends(); }
else alert(data.message);
} catch(e) { alert('操作失败'); }
}
async function rejectFriend(id) {
try {
const res = await fetch('/api/friend/reject/' + id, {method:'POST'});
const data = await res.json();
if (data.success) { document.getElementById('freq-' + id).remove(); }
else alert(data.message);
} catch(e) { alert('操作失败'); }
}
async function loadPosts() {
const container = document.getElementById('my-posts');
try {
const res = await fetch('/api/user/posts');
const data = await res.json();
if (!data.success) throw new Error(data.message);
if (data.posts.length === 0) {
container.innerHTML = '<div class="text-center py-4 text-slate-400">暂无帖子</div>';
return;
}
let html = '';
data.posts.slice(0, 5).forEach(p => {
html += `
<div class="p-3 border border-slate-100 rounded-lg hover:bg-slate-50 cursor-pointer" onclick="location.href='/forum#post-${p.id}'">
<div class="text-sm font-medium text-slate-900 truncate">${p.title}</div>
<div class="text-xs text-slate-500 mt-1">${p.created_at} · ${p.replies} 回复 · ${p.likes} 赞</div>
</div>`;
});
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="text-center py-4 text-red-500">加载失败</div>';
}
}
async function changeName() {
const newName = prompt('请输入新用户名(每月仅可修改一次):');
if (!newName || !newName.trim()) return;
try {
const res = await fetch('/api/user/change-name', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: newName.trim()})
});
const d = await res.json();
if (d.success) {
document.getElementById('display-username').textContent = d.name;
alert('用户名修改成功');
location.reload();
} else {
alert(d.message);
}
} catch(e) { alert('修改失败'); }
}
async function loadExamHistory() {
const container = document.getElementById('exam-history');
try {
const res = await fetch('/api/user/exam-history');
const data = await res.json();
if (!data.success) throw new Error(data.message);
if (data.history.length === 0) {
container.innerHTML = '<div class="text-center py-4 text-slate-400">暂无考试经历</div>';
return;
}
let html = '';
data.history.forEach(h => {
let scoreText = '';
if (!h.graded) {
scoreText = '<span class="text-amber-500">待批改</span>';
} else if (h.score === null) {
scoreText = '<span class="text-slate-400">成绩未公布</span>';
} else {
scoreText = `<span class="text-green-600 font-medium">${h.score}/${h.total_score}</span>`;
}
const contestTag = h.contest_name ? `<span class="text-xs bg-indigo-50 text-indigo-600 px-1.5 py-0.5 rounded">${h.contest_name}</span>` : '';
html += `
<div class="p-3 border border-slate-100 rounded-lg hover:bg-slate-50 cursor-pointer flex items-center justify-between" onclick="location.href='/exams/${h.exam_id}/result'">
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-slate-900 truncate">${h.title} ${contestTag}</div>
<div class="text-xs text-slate-500 mt-1">${h.submitted_at}</div>
</div>
<div class="ml-3 text-sm">${scoreText}</div>
</div>`;
});
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="text-center py-4 text-red-500">加载失败</div>';
}
}
async function loadBookmarks() {
const container = document.getElementById('bookmarked-exams');
try {
const res = await fetch('/api/user/exam-bookmarks');
const data = await res.json();
if (!data.success) throw new Error(data.message);
if (data.bookmarks.length === 0) {
container.innerHTML = '<div class="text-center py-4 text-slate-400">暂无收藏试卷</div>';
return;
}
let html = '';
data.bookmarks.slice(0, 5).forEach(e => {
html += `
<div class="p-3 border border-slate-100 rounded-lg hover:bg-slate-50 cursor-pointer" onclick="location.href='/exams/${e.id}'">
<div class="text-sm font-medium text-slate-900 truncate">${e.title}</div>
<div class="text-xs text-slate-500 mt-1">${e.subject} · 收藏于 ${e.bookmarked_at}</div>
</div>`;
});
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="text-center py-4 text-red-500">加载失败</div>';
}
}
function switchTab(tabName) {
// 隐藏所有内容并重置动画
['history', 'posts', 'bookmarks', 'friends'].forEach(t => {
const content = document.getElementById(`tab-content-${t}`);
if(content) {
content.classList.add('hidden');
content.style.animation = 'none';
}
const btn = document.getElementById(`tab-btn-${t}`);
if(btn) {
btn.classList.remove('text-indigo-600', 'bg-white', 'shadow-sm', 'font-bold');
btn.classList.add('text-slate-500', 'bg-transparent', 'hover:bg-white/60', 'font-medium');
}
});
// 显示选中内容并添加动画
const activeContent = document.getElementById(`tab-content-${tabName}`);
if(activeContent) {
activeContent.classList.remove('hidden');
void activeContent.offsetWidth; // 触发重绘
activeContent.style.animation = 'fadeIn 0.3s ease-out';
}
// 激活按钮样式
const activeBtn = document.getElementById(`tab-btn-${tabName}`);
if(activeBtn) {
activeBtn.classList.remove('text-slate-500', 'bg-transparent', 'hover:bg-white/60', 'font-medium');
activeBtn.classList.add('text-indigo-600', 'bg-white', 'shadow-sm', 'font-bold');
}
}
loadExamHistory();
loadFriends();
loadPosts();
loadBookmarks();
</script>
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes shimmer {
100% { transform: translateX(100%); }
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
{% endblock %}