feat: v3.0 - 用户系统和收藏功能
核心功能: - 用户注册/登录系统(用户名+密码) - 工具收藏功能(一键收藏/取消收藏) - 收藏分组管理(文件夹) - 用户中心(个人资料、收藏列表) 数据库变更: - 新增 users 表(用户信息) - 新增 folders 表(收藏分组) - 新增 collections 表(收藏记录) 安全增强: - Admin 和 User 完全隔离 - 修复14个管理员路由的权限漏洞 - 所有管理功能添加用户类型检查 新增文件: - templates/auth/register.html - 注册页面 - templates/auth/login.html - 登录页面 - templates/user/profile.html - 用户中心 - templates/user/collections.html - 收藏列表 - create_user_tables.py - 数据库迁移脚本 - USER_SYSTEM_README.md - 用户系统文档 - CHANGELOG_v3.0.md - 版本更新日志 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
173
templates/auth/login.html
Normal file
173
templates/auth/login.html
Normal file
@@ -0,0 +1,173 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户登录 - ZJPB - 自己品吧</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Material Symbols -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"primary": "#0ea5e9",
|
||||
"primary-dark": "#0284c7",
|
||||
"background": "#f8fafc",
|
||||
"surface": "#ffffff",
|
||||
"input-bg": "#ffffff",
|
||||
"input-border": "#e2e8f0",
|
||||
"text-main": "#0f172a",
|
||||
"text-secondary": "#334155",
|
||||
"text-muted": "#64748b",
|
||||
},
|
||||
fontFamily: {
|
||||
"display": ["Space Grotesk", "sans-serif"],
|
||||
"body": ["Noto Sans", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
.tech-bg-grid {
|
||||
background-image: radial-gradient(circle at center, rgba(14, 165, 233, 0.04) 0%, transparent 60%),
|
||||
linear-gradient(rgba(14, 165, 233, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(14, 165, 233, 0.05) 1px, transparent 1px);
|
||||
background-size: 100% 100%, 40px 40px, 40px 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#f8fafc] font-display text-[#0f172a] min-h-screen flex flex-col items-center justify-center relative overflow-hidden selection:bg-sky-500/20 selection:text-sky-700">
|
||||
<!-- Ambient Background -->
|
||||
<div class="absolute inset-0 z-0 tech-bg-grid pointer-events-none"></div>
|
||||
<div class="absolute top-[-20%] right-[-10%] w-[600px] h-[600px] bg-sky-200/40 rounded-full blur-[120px] pointer-events-none mix-blend-multiply"></div>
|
||||
<div class="absolute bottom-[-10%] left-[-10%] w-[500px] h-[500px] bg-indigo-100/60 rounded-full blur-[100px] pointer-events-none mix-blend-multiply"></div>
|
||||
|
||||
<!-- Main Content Wrapper -->
|
||||
<div class="w-full max-w-[480px] px-4 z-10 flex flex-col gap-6">
|
||||
<!-- Back Navigation -->
|
||||
<a class="group flex items-center gap-2 text-[#64748b] hover:text-[#0f172a] transition-colors w-fit" href="{{ url_for('index') }}">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full border border-[#e2e8f0] bg-white group-hover:border-sky-500/50 group-hover:bg-sky-500/5 transition-all shadow-sm">
|
||||
<span class="material-symbols-outlined text-sm">arrow_back</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium">返回首页</span>
|
||||
</a>
|
||||
|
||||
<!-- Login Card -->
|
||||
<div class="glass-panel border border-white rounded-2xl shadow-[0_4px_30px_rgba(0,0,0,0.03)] p-8 md:p-12 relative overflow-hidden ring-1 ring-black/5">
|
||||
<!-- Decorative Accent -->
|
||||
<div class="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-sky-500/50 to-transparent"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-2 mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="p-2 rounded-lg bg-sky-500/10 text-sky-500">
|
||||
<span class="material-symbols-outlined text-2xl">login</span>
|
||||
</div>
|
||||
<span class="text-xs font-bold tracking-widest uppercase text-sky-500">User Login</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-black tracking-tight text-[#0f172a] leading-tight">用户登录</h1>
|
||||
<p class="text-[#64748b] text-base font-normal">输入您的登录凭据以访问您的收藏</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 flex items-start gap-3">
|
||||
<span class="material-symbols-outlined text-red-500 text-sm mt-0.5">error</span>
|
||||
<p class="text-red-600 text-sm">{{ message }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Form -->
|
||||
<form method="POST" action="{{ url_for('user_login') }}" class="flex flex-col gap-5">
|
||||
<!-- Username Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-[#334155] text-sm font-semibold leading-normal" for="username">用户名</label>
|
||||
<div class="relative group">
|
||||
<input class="form-input flex w-full h-12 pl-11 pr-4 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="输入您的用户名"
|
||||
type="text"
|
||||
required
|
||||
autofocus/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none">
|
||||
<span class="material-symbols-outlined text-[20px]">person</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between items-end">
|
||||
<label class="text-[#334155] text-sm font-semibold leading-normal" for="password">密码</label>
|
||||
</div>
|
||||
<div class="flex w-full items-stretch rounded-lg group relative">
|
||||
<input class="form-input flex w-full h-12 pl-11 pr-11 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm z-10"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="输入您的密码"
|
||||
type="password"
|
||||
required/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none z-20">
|
||||
<span class="material-symbols-outlined text-[20px]">lock</span>
|
||||
</div>
|
||||
<button class="absolute right-0 top-0 h-full px-3 text-slate-400 hover:text-[#334155] transition-colors flex items-center justify-center z-20 focus:outline-none"
|
||||
type="button"
|
||||
onclick="togglePassword()">
|
||||
<span class="material-symbols-outlined text-[20px]" id="toggleIcon">visibility_off</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button class="mt-4 flex w-full h-12 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-sky-500 text-white text-base font-bold leading-normal tracking-wide hover:bg-sky-600 transition-all active:scale-[0.98] shadow-lg shadow-sky-500/25 hover:shadow-sky-500/40"
|
||||
type="submit">
|
||||
<span class="truncate">登录</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Footer Meta -->
|
||||
<div class="mt-8 flex flex-col items-center gap-4 border-t border-slate-100 pt-6">
|
||||
<p class="text-[#64748b] text-sm text-center">
|
||||
还没有账号?<a href="{{ url_for('user_register') }}" class="text-sky-500 hover:text-sky-600 font-semibold">立即注册</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword() {
|
||||
const passwordInput = document.getElementById('password');
|
||||
const toggleIcon = document.getElementById('toggleIcon');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
toggleIcon.textContent = 'visibility';
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
toggleIcon.textContent = 'visibility_off';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
192
templates/auth/register.html
Normal file
192
templates/auth/register.html
Normal file
@@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户注册 - ZJPB - 自己品吧</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Noto+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Material Symbols -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"primary": "#0ea5e9",
|
||||
"primary-dark": "#0284c7",
|
||||
"background": "#f8fafc",
|
||||
"surface": "#ffffff",
|
||||
"input-bg": "#ffffff",
|
||||
"input-border": "#e2e8f0",
|
||||
"text-main": "#0f172a",
|
||||
"text-secondary": "#334155",
|
||||
"text-muted": "#64748b",
|
||||
},
|
||||
fontFamily: {
|
||||
"display": ["Space Grotesk", "sans-serif"],
|
||||
"body": ["Noto Sans", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
.tech-bg-grid {
|
||||
background-image: radial-gradient(circle at center, rgba(14, 165, 233, 0.04) 0%, transparent 60%),
|
||||
linear-gradient(rgba(14, 165, 233, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(14, 165, 233, 0.05) 1px, transparent 1px);
|
||||
background-size: 100% 100%, 40px 40px, 40px 40px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#f8fafc] font-display text-[#0f172a] min-h-screen flex flex-col items-center justify-center relative overflow-hidden selection:bg-sky-500/20 selection:text-sky-700">
|
||||
<!-- Ambient Background -->
|
||||
<div class="absolute inset-0 z-0 tech-bg-grid pointer-events-none"></div>
|
||||
<div class="absolute top-[-20%] right-[-10%] w-[600px] h-[600px] bg-sky-200/40 rounded-full blur-[120px] pointer-events-none mix-blend-multiply"></div>
|
||||
<div class="absolute bottom-[-10%] left-[-10%] w-[500px] h-[500px] bg-indigo-100/60 rounded-full blur-[100px] pointer-events-none mix-blend-multiply"></div>
|
||||
|
||||
<!-- Main Content Wrapper -->
|
||||
<div class="w-full max-w-[480px] px-4 z-10 flex flex-col gap-6">
|
||||
<!-- Back Navigation -->
|
||||
<a class="group flex items-center gap-2 text-[#64748b] hover:text-[#0f172a] transition-colors w-fit" href="{{ url_for('index') }}">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full border border-[#e2e8f0] bg-white group-hover:border-sky-500/50 group-hover:bg-sky-500/5 transition-all shadow-sm">
|
||||
<span class="material-symbols-outlined text-sm">arrow_back</span>
|
||||
</div>
|
||||
<span class="text-sm font-medium">返回首页</span>
|
||||
</a>
|
||||
|
||||
<!-- Register Card -->
|
||||
<div class="glass-panel border border-white rounded-2xl shadow-[0_4px_30px_rgba(0,0,0,0.03)] p-8 md:p-12 relative overflow-hidden ring-1 ring-black/5">
|
||||
<!-- Decorative Accent -->
|
||||
<div class="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-transparent via-sky-500/50 to-transparent"></div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-2 mb-8">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<div class="p-2 rounded-lg bg-sky-500/10 text-sky-500">
|
||||
<span class="material-symbols-outlined text-2xl">person_add</span>
|
||||
</div>
|
||||
<span class="text-xs font-bold tracking-widest uppercase text-sky-500">Create Account</span>
|
||||
</div>
|
||||
<h1 class="text-3xl font-black tracking-tight text-[#0f172a] leading-tight">用户注册</h1>
|
||||
<p class="text-[#64748b] text-base font-normal">创建您的账号,开始收藏喜欢的AI工具</p>
|
||||
</div>
|
||||
|
||||
<!-- Error Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-6 p-3 rounded-lg bg-red-50 border border-red-200 flex items-start gap-3">
|
||||
<span class="material-symbols-outlined text-red-500 text-sm mt-0.5">error</span>
|
||||
<p class="text-red-600 text-sm">{{ message }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Form -->
|
||||
<form method="POST" action="{{ url_for('user_register') }}" class="flex flex-col gap-5">
|
||||
<!-- Username Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-[#334155] text-sm font-semibold leading-normal" for="username">用户名</label>
|
||||
<div class="relative group">
|
||||
<input class="form-input flex w-full h-12 pl-11 pr-4 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="请输入用户名(3个字符以上)"
|
||||
type="text"
|
||||
required
|
||||
autofocus/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none">
|
||||
<span class="material-symbols-outlined text-[20px]">person</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-[#334155] text-sm font-semibold leading-normal" for="password">密码</label>
|
||||
<div class="flex w-full items-stretch rounded-lg group relative">
|
||||
<input class="form-input flex w-full h-12 pl-11 pr-11 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm z-10"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="请输入密码(6个字符以上)"
|
||||
type="password"
|
||||
required/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none z-20">
|
||||
<span class="material-symbols-outlined text-[20px]">lock</span>
|
||||
</div>
|
||||
<button class="absolute right-0 top-0 h-full px-3 text-slate-400 hover:text-[#334155] transition-colors flex items-center justify-center z-20 focus:outline-none"
|
||||
type="button"
|
||||
onclick="togglePassword('password')">
|
||||
<span class="material-symbols-outlined text-[20px]" id="toggleIcon1">visibility_off</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-[#334155] text-sm font-semibold leading-normal" for="confirm_password">确认密码</label>
|
||||
<div class="flex w-full items-stretch rounded-lg group relative">
|
||||
<input class="form-input flex w-full h-12 pl-11 pr-11 rounded-lg text-[#0f172a] focus:outline-0 focus:ring-2 focus:ring-sky-500/20 border border-[#e2e8f0] bg-white focus:border-sky-500 placeholder:text-slate-400 text-base font-normal leading-normal transition-all shadow-sm z-10"
|
||||
id="confirm_password"
|
||||
name="confirm_password"
|
||||
placeholder="请再次输入密码"
|
||||
type="password"
|
||||
required/>
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 group-focus-within:text-sky-500 transition-colors flex items-center justify-center pointer-events-none z-20">
|
||||
<span class="material-symbols-outlined text-[20px]">lock</span>
|
||||
</div>
|
||||
<button class="absolute right-0 top-0 h-full px-3 text-slate-400 hover:text-[#334155] transition-colors flex items-center justify-center z-20 focus:outline-none"
|
||||
type="button"
|
||||
onclick="togglePassword('confirm_password')">
|
||||
<span class="material-symbols-outlined text-[20px]" id="toggleIcon2">visibility_off</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Register Button -->
|
||||
<button class="mt-4 flex w-full h-12 cursor-pointer items-center justify-center overflow-hidden rounded-lg bg-sky-500 text-white text-base font-bold leading-normal tracking-wide hover:bg-sky-600 transition-all active:scale-[0.98] shadow-lg shadow-sky-500/25 hover:shadow-sky-500/40"
|
||||
type="submit">
|
||||
<span class="truncate">注册</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Footer Meta -->
|
||||
<div class="mt-8 flex flex-col items-center gap-4 border-t border-slate-100 pt-6">
|
||||
<p class="text-[#64748b] text-sm text-center">
|
||||
已有账号?<a href="{{ url_for('user_login') }}" class="text-sky-500 hover:text-sky-600 font-semibold">立即登录</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePassword(fieldId) {
|
||||
const passwordInput = document.getElementById(fieldId);
|
||||
const toggleIcon = document.getElementById(fieldId === 'password' ? 'toggleIcon1' : 'toggleIcon2');
|
||||
|
||||
if (passwordInput.type === 'password') {
|
||||
passwordInput.type = 'text';
|
||||
toggleIcon.textContent = 'visibility';
|
||||
} else {
|
||||
passwordInput.type = 'password';
|
||||
toggleIcon.textContent = 'visibility_off';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -174,6 +174,76 @@
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* 用户菜单 */
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.avatar-sm {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.user-btn:hover .dropdown-arrow {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-width: 180px;
|
||||
padding: 8px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dropdown-menu a {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.dropdown-menu a:hover {
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.dropdown-menu hr {
|
||||
margin: 4px 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-content {
|
||||
max-width: 1280px;
|
||||
@@ -244,8 +314,30 @@
|
||||
<span class="search-icon">🔍</span>
|
||||
<input type="text" name="q" placeholder="搜索 AI 工具..." value="{{ search_query or '' }}">
|
||||
</form>
|
||||
<a href="/admin/login" class="btn btn-secondary">登录</a>
|
||||
<a href="/admin/login" class="btn btn-primary">注册</a>
|
||||
{% if current_user.is_authenticated and current_user.__class__.__name__ == 'User' %}
|
||||
<!-- 已登录用户 -->
|
||||
<div class="user-menu">
|
||||
<button class="btn btn-secondary user-btn" onclick="toggleUserDropdown(event)">
|
||||
{% if current_user.avatar %}
|
||||
<img src="{{ current_user.avatar }}" alt="{{ current_user.username }}" class="avatar-sm">
|
||||
{% else %}
|
||||
<div class="avatar-sm avatar-placeholder">{{ current_user.username[0].upper() }}</div>
|
||||
{% endif %}
|
||||
<span>{{ current_user.username }}</span>
|
||||
<span class="dropdown-arrow">▼</span>
|
||||
</button>
|
||||
<div id="userDropdown" class="dropdown-menu" style="display: none;">
|
||||
<a href="/user/profile">👤 个人中心</a>
|
||||
<a href="/user/collections">⭐ 我的收藏</a>
|
||||
<hr>
|
||||
<a href="#" onclick="logout(event)">🚪 退出登录</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- 未登录 -->
|
||||
<a href="/login" class="btn btn-secondary">登录</a>
|
||||
<a href="/register" class="btn btn-primary">注册</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -291,6 +383,30 @@
|
||||
})(window, document, "clarity", "script", "uoa2j40sf0");
|
||||
</script>
|
||||
|
||||
<!-- User Dropdown Script -->
|
||||
<script>
|
||||
function toggleUserDropdown(event) {
|
||||
event.stopPropagation();
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function logout(event) {
|
||||
event.preventDefault();
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
window.location.href = '/logout';
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭下拉菜单
|
||||
document.addEventListener('click', function(event) {
|
||||
const dropdown = document.getElementById('userDropdown');
|
||||
if (dropdown && !event.target.closest('.user-menu')) {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -727,6 +727,12 @@
|
||||
<span>↗</span>
|
||||
</a>
|
||||
|
||||
<!-- 收藏按钮 -->
|
||||
<button type="button" id="collectBtn" class="collect-btn" onclick="toggleCollect()">
|
||||
<span class="collect-icon" id="collectIcon">⭐</span>
|
||||
<span class="collect-text" id="collectText">收藏</span>
|
||||
</button>
|
||||
|
||||
<!-- v2.5新增:社媒分享入口 -->
|
||||
<button type="button" class="share-btn" onclick="openShareModal()">
|
||||
分享
|
||||
@@ -1156,6 +1162,45 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 收藏按钮 */
|
||||
.collect-btn {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-white);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.collect-btn:hover {
|
||||
border-color: var(--primary-blue);
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.collect-btn.collected {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
border-color: rgba(251, 191, 36, 0.5);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.collect-btn.collected .collect-icon {
|
||||
animation: pulse 0.4s ease;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
@@ -1521,6 +1566,96 @@ function shareToplatform() {
|
||||
navigator.clipboard.writeText(text).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 收藏功能 ==========
|
||||
const siteCode = '{{ site.code }}';
|
||||
let isCollected = false;
|
||||
|
||||
// 页面加载时检查收藏状态
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkCollectionStatus();
|
||||
});
|
||||
|
||||
function checkCollectionStatus() {
|
||||
fetch('/api/auth/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.logged_in || data.user_type !== 'user') {
|
||||
return; // 未登录或非普通用户,不检查收藏状态
|
||||
}
|
||||
|
||||
// 已登录,检查收藏状态
|
||||
fetch(`/api/collections/status/${siteCode}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
isCollected = data.is_collected;
|
||||
updateCollectButton();
|
||||
})
|
||||
.catch(err => console.error('检查收藏状态失败:', err));
|
||||
})
|
||||
.catch(err => console.error('检查登录状态失败:', err));
|
||||
}
|
||||
|
||||
function updateCollectButton() {
|
||||
const btn = document.getElementById('collectBtn');
|
||||
const icon = document.getElementById('collectIcon');
|
||||
const text = document.getElementById('collectText');
|
||||
|
||||
if (isCollected) {
|
||||
btn.classList.add('collected');
|
||||
text.textContent = '已收藏';
|
||||
} else {
|
||||
btn.classList.remove('collected');
|
||||
text.textContent = '收藏';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCollect() {
|
||||
// 先检查登录状态
|
||||
fetch('/api/auth/status')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.logged_in) {
|
||||
// 未登录,跳转到登录页
|
||||
if (confirm('请先登录后再收藏工具')) {
|
||||
window.location.href = `/login?next=${encodeURIComponent(window.location.pathname)}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.user_type !== 'user') {
|
||||
showMessage('管理员账号无法使用收藏功能', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 已登录,执行收藏/取消收藏
|
||||
fetch('/api/collections/toggle', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ site_code: siteCode })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
isCollected = data.action === 'added';
|
||||
updateCollectButton();
|
||||
showMessage(data.message, 'success');
|
||||
} else {
|
||||
showMessage(data.message || '操作失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('收藏操作失败:', err);
|
||||
showMessage('网络请求失败', 'error');
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('检查登录状态失败:', err);
|
||||
showMessage('网络请求失败', 'error');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
258
templates/user/collections.html
Normal file
258
templates/user/collections.html
Normal file
@@ -0,0 +1,258 @@
|
||||
{% extends 'base_new.html' %}
|
||||
|
||||
{% block title %}我的收藏 - ZJPB{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.collections-container {
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.collections-header {
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.collections-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 文件夹标签 */
|
||||
.folder-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.folder-tab {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-white);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.folder-tab:hover {
|
||||
border-color: var(--primary-blue);
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.folder-tab.active {
|
||||
background: var(--primary-blue);
|
||||
border-color: var(--primary-blue);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 工具网格 */
|
||||
.tools-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 20px;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tool-card:hover {
|
||||
border-color: var(--primary-blue);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tool-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.tool-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-md);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tool-info h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool-desc {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
padding: 4px 10px;
|
||||
background: var(--bg-page);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 80px 40px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.pagination a,
|
||||
.pagination span {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-white);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.pagination a:hover {
|
||||
border-color: var(--primary-blue);
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
.pagination .active {
|
||||
background: var(--primary-blue);
|
||||
border-color: var(--primary-blue);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="main-content">
|
||||
<div class="collections-container">
|
||||
<!-- 头部 -->
|
||||
<div class="collections-header">
|
||||
<h1>我的收藏</h1>
|
||||
|
||||
<!-- 文件夹标签 -->
|
||||
<div class="folder-tabs">
|
||||
<a href="/user/collections" class="folder-tab {% if not current_folder_id %}active{% endif %}">
|
||||
📂 全部收藏 ({{ pagination.total }})
|
||||
</a>
|
||||
<a href="/user/collections?folder_id=none" class="folder-tab {% if current_folder_id == 'none' %}active{% endif %}">
|
||||
📄 未分类
|
||||
</a>
|
||||
{% for folder in folders %}
|
||||
<a href="/user/collections?folder_id={{ folder.id }}" class="folder-tab {% if current_folder_id == folder.id|string %}active{% endif %}">
|
||||
{{ folder.icon }} {{ folder.name }} ({{ folder.count }})
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收藏网格 -->
|
||||
{% if collections %}
|
||||
<div class="tools-grid">
|
||||
{% for collection in collections %}
|
||||
<a href="/site/{{ collection.site.code }}" class="tool-card">
|
||||
<div class="tool-header">
|
||||
{% if collection.site.logo %}
|
||||
<img src="{{ collection.site.logo }}" alt="{{ collection.site.name }}" class="tool-logo">
|
||||
{% else %}
|
||||
<div class="tool-logo" style="background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);"></div>
|
||||
{% endif %}
|
||||
<div class="tool-info">
|
||||
<h3>{{ collection.site.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p class="tool-desc">{{ collection.site.short_desc or collection.site.description }}</p>
|
||||
{% if collection.site.tags %}
|
||||
<div class="tool-tags">
|
||||
{% for tag in collection.site.tags[:3] %}
|
||||
<span class="tool-tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
{% if pagination.pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="?page={{ pagination.prev_num }}{% if current_folder_id %}&folder_id={{ current_folder_id }}{% endif %}">上一页</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page_num in pagination.iter_pages() %}
|
||||
{% if page_num %}
|
||||
{% if page_num == pagination.page %}
|
||||
<span class="active">{{ page_num }}</span>
|
||||
{% else %}
|
||||
<a href="?page={{ page_num }}{% if current_folder_id %}&folder_id={{ current_folder_id }}{% endif %}">{{ page_num }}</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span>...</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="?page={{ pagination.next_num }}{% if current_folder_id %}&folder_id={{ current_folder_id }}{% endif %}">下一页</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- 空状态 -->
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📦</div>
|
||||
<h2 style="font-size: 20px; margin-bottom: 8px;">暂无收藏</h2>
|
||||
<p>去首页发现喜欢的AI工具吧!</p>
|
||||
<a href="/" style="display: inline-block; margin-top: 20px; padding: 10px 20px; background: var(--primary-blue); color: white; border-radius: var(--radius-md); text-decoration: none;">浏览工具</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
245
templates/user/profile.html
Normal file
245
templates/user/profile.html
Normal file
@@ -0,0 +1,245 @@
|
||||
{% extends 'base_new.html' %}
|
||||
|
||||
{% block title %}个人中心 - ZJPB{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.profile-container {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 32px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
.sidebar-card {
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px;
|
||||
align-self: flex-start;
|
||||
position: sticky;
|
||||
top: 88px;
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.profile-username {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.profile-bio {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.nav-menu a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.nav-menu a:hover {
|
||||
background: var(--bg-page);
|
||||
}
|
||||
|
||||
.nav-menu a.active {
|
||||
background: rgba(14, 165, 233, 0.1);
|
||||
color: var(--primary-blue);
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.main-card {
|
||||
background: var(--bg-white);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 20px;
|
||||
background: var(--bg-page);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-blue);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.recent-section h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.collection-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.collection-item:hover {
|
||||
border-color: var(--primary-blue);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.collection-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: var(--radius-md);
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collection-info h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.collection-info p {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.profile-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="main-content">
|
||||
<div class="profile-container">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="sidebar-card">
|
||||
<div class="profile-avatar">
|
||||
{{ current_user.username[0].upper() }}
|
||||
</div>
|
||||
<div class="profile-username">{{ current_user.username }}</div>
|
||||
<div class="profile-bio">{{ current_user.bio or '这个人很懒,什么都没写' }}</div>
|
||||
|
||||
<ul class="nav-menu">
|
||||
<li><a href="/user/profile" class="active">👤 个人资料</a></li>
|
||||
<li><a href="/user/collections">⭐ 我的收藏</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div>
|
||||
<div class="main-card">
|
||||
<h1 style="font-size: 24px; font-weight: 700; margin-bottom: 24px;">个人中心</h1>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ collections_count }}</div>
|
||||
<div class="stat-label">收藏数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ folders_count }}</div>
|
||||
<div class="stat-label">文件夹数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近收藏 -->
|
||||
<div class="recent-section">
|
||||
<h2>最近收藏</h2>
|
||||
{% if recent_collections %}
|
||||
{% for collection in recent_collections %}
|
||||
<a href="/site/{{ collection.site.code }}" class="collection-item">
|
||||
{% if collection.site.logo %}
|
||||
<img src="{{ collection.site.logo }}" alt="{{ collection.site.name }}" class="collection-logo">
|
||||
{% else %}
|
||||
<div class="collection-logo" style="background: linear-gradient(135deg, #0ea5e9 0%, #8b5cf6 100%);"></div>
|
||||
{% endif %}
|
||||
<div class="collection-info">
|
||||
<h3>{{ collection.site.name }}</h3>
|
||||
<p>{{ collection.site.short_desc or collection.site.description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📦</div>
|
||||
<p>还没有收藏任何工具</p>
|
||||
<p style="margin-top: 8px;"><a href="/" style="color: var(--primary-blue);">去首页逛逛</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user