fix: 设计原则审查修复 — DIP/ISP, LoD, DRY, OCP, URL, 301缓存

- P0 DIP+ISP: 全链路注入接口,消除零接口紧耦合
- P0 URL: auth 301→302,修复登出后浏览器缓存陷阱
- P1 DRY: JWT 认证逻辑收敛至 TokenService+中间件
- P2 DRY: 前后端角色/状态映射统一为 model 常量
- P2 LoD: 新增 SettingsController,router 不再跨层调 repo
- P2 URL: settings ?tab= → /settings/:tab 伪静态
- P3 OCP: 角色权限 map 化,告别硬编码 switch
This commit is contained in:
2026-05-26 21:12:19 +08:00
parent 483fdd919f
commit 39d13993ba
37 changed files with 961 additions and 260 deletions

View File

@ -3,17 +3,37 @@
{{template "layout/nav.html" .}}
<main class="hero">
<div class="container">
<h1>Meta<span>Lab</span></h1>
<p class="hero-tagline">下一代开发者社区</p>
{{if .IsLoggedIn}}
<p class="hero-desc">欢迎回来,{{.Username}}</p>
{{else}}
<p class="hero-desc">一个为构建者准备的纯粹空间</p>
{{end}}
</div>
</main>
<div class="home-layout">
<main class="hero">
<div class="container">
<h1>Meta<span>Lab</span></h1>
<p class="hero-tagline">下一代开发者社区</p>
{{if .IsLoggedIn}}
<p class="hero-desc">欢迎回来,{{.Username}}</p>
{{else}}
<p class="hero-desc">一个为构建者准备的纯粹空间</p>
{{end}}
</div>
</main>
<aside class="sidebar">
<div class="sidebar-card user-card">
{{if .IsLoggedIn}}
<div class="user-card-avatar">{{slice .Username 0 1}}</div>
<h3 class="user-card-name">{{.Username}}</h3>
<p class="user-card-desc">欢迎加入 MetaLab</p>
<a href="/settings/profile" class="user-card-link">个人设置</a>
{{else}}
<div class="user-card-avatar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="28" height="28"><circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/></svg>
</div>
<h3 class="user-card-name">未登录</h3>
<p class="user-card-desc">登录后解锁社区功能</p>
<a href="/auth/login" class="user-card-btn">登录</a>
{{end}}
</div>
</aside>
</div>
{{template "layout/footer.html" .}}

View File

@ -9,4 +9,5 @@
{{if .ExtraCSS}}
<link rel="stylesheet" href="{{.ExtraCSS}}">
{{end}}
<script src="/shared/static/js/utils.js"></script>
</head>

View File

@ -7,7 +7,7 @@
<ul class="nav-links" id="navLinks">
<li><a href="/">首页</a></li>
{{if .IsLoggedIn}}
<li><a href="/settings" class="nav-user">{{.Username}}</a></li>
<li><a href="/settings/profile" class="nav-user">{{.Username}}</a></li>
<li><a href="javascript:void(0)" class="nav-logout" id="logoutBtn">退出</a></li>
{{else}}
<li><a href="/auth/login" class="nav-login">登录</a></li>

View File

@ -0,0 +1,105 @@
{{template "layout/header.html" .}}
<body>
{{template "layout/nav.html" .}}
<div class="settings-layout">
<aside class="settings-sidebar">
<nav class="settings-nav">
<a href="/settings/profile"
class="settings-nav-item {{if eq .ActiveTab "profile"}}active{{end}}">
<svg class="settings-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 4-7 8-7s8 3 8 7"/>
</svg>
个人资料
</a>
<a href="/settings/account"
class="settings-nav-item {{if eq .ActiveTab "account"}}active{{end}}">
<svg class="settings-nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
<circle cx="12" cy="16" r="1"/>
</svg>
账号信息
</a>
</nav>
</aside>
<main class="settings-main">
{{if eq .ActiveTab "profile"}}
<!-- 个人资料 -->
<div class="settings-card">
<div class="settings-card-header">
<h2>个人资料</h2>
<p class="settings-card-desc">你的公开信息和基本资料</p>
</div>
<div class="settings-card-body">
<div class="info-row">
<span class="info-label">用户名</span>
<span class="info-value">{{.User.Username}}</span>
</div>
<div class="info-row">
<span class="info-label">头像</span>
<span class="info-value">
{{if .User.Avatar}}
<img src="{{.User.Avatar}}" alt="avatar" class="avatar-preview">
{{else}}
<span class="info-muted">未设置</span>
{{end}}
</span>
</div>
<div class="info-row">
<span class="info-label">简介</span>
<span class="info-value{{if not .User.Bio}} info-muted{{end}}">
{{if .User.Bio}}{{.User.Bio}}{{else}}未填写{{end}}
</span>
</div>
<div class="info-row">
<span class="info-label">角色</span>
<span class="info-value">
<span class="role-tag role-{{.User.Role}}">{{.RoleName}}</span>
</span>
</div>
</div>
</div>
{{else}}
<!-- 账号信息 -->
<div class="settings-card">
<div class="settings-card-header">
<h2>账号信息</h2>
<p class="settings-card-desc">账号安全和基本状态</p>
</div>
<div class="settings-card-body">
<div class="info-row">
<span class="info-label">用户 ID</span>
<span class="info-value info-mono">{{.User.ID}}</span>
</div>
<div class="info-row">
<span class="info-label">邮箱</span>
<span class="info-value">{{.User.Email}}</span>
</div>
<div class="info-row">
<span class="info-label">账号状态</span>
<span class="info-value">
<span class="status-badge status-{{.User.Status}}">{{.StatusName}}</span>
</span>
</div>
<div class="info-row">
<span class="info-label">注册时间</span>
<span class="info-value">{{.User.CreatedAt.Format "2006-01-02 15:04:05"}}</span>
</div>
<div class="info-row">
<span class="info-label">最后更新</span>
<span class="info-value">{{.User.UpdatedAt.Format "2006-01-02 15:04:05"}}</span>
</div>
</div>
</div>
{{end}}
</main>
</div>
{{template "layout/footer.html" .}}
<script src="/static/js/common.js"></script>
</body>
</html>

View File

@ -26,9 +26,7 @@
box-sizing: border-box;
}
html {
scroll-snap-type: y mandatory;
}
html {}
body {
background: var(--color-bg);

View File

@ -3,16 +3,23 @@
仅在首页加载,通过 ExtraCSS 注入
============================================================ */
/* ---------- Two-column layout ---------- */
.home-layout {
display: flex;
min-height: 100vh;
align-items: stretch;
}
/* ---------- Hero ---------- */
.hero {
min-height: 100vh;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 4rem 0;
text-align: center;
background: linear-gradient(180deg, #fff 0%, #f0f4f8 100%);
scroll-snap-align: start;
min-width: 0;
}
.hero h1 {
@ -105,7 +112,6 @@
flex-direction: column;
justify-content: center;
padding: 1.5rem 0;
scroll-snap-align: start;
}
.plans .section-title {
@ -185,7 +191,6 @@
justify-content: center;
padding: 4rem 0;
background: var(--color-surface);
scroll-snap-align: start;
}
.features-grid {
@ -220,7 +225,6 @@
height: 100vh;
display: flex;
flex-direction: column;
scroll-snap-align: start;
}
.timeline-vision {
@ -316,6 +320,84 @@
font-size: .95rem;
}
/* ---------- Sidebar ---------- */
.sidebar {
width: 300px;
flex-shrink: 0;
padding: 4rem 20px 4rem 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.sidebar-card {
background: var(--color-surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--color-border);
padding: 1.5rem;
}
/* --- User Card --- */
.user-card {
text-align: center;
}
.user-card-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--color-accent);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto .8rem;
font-size: 1.5rem;
font-weight: 700;
text-transform: uppercase;
}
.user-card-name {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-primary);
margin-bottom: .3rem;
}
.user-card-desc {
font-size: .85rem;
color: var(--color-text-light);
margin-bottom: 1rem;
}
.user-card-link {
display: inline-block;
font-size: .85rem;
color: var(--color-accent);
font-weight: 500;
transition: var(--transition);
}
.user-card-link:hover {
opacity: .8;
}
.user-card-btn {
display: inline-block;
padding: .5rem 1.8rem;
background: var(--color-accent);
color: #fff;
border-radius: var(--radius);
font-size: .9rem;
font-weight: 500;
transition: var(--transition);
}
.user-card-btn:hover {
opacity: .85;
}
/* ---------- Responsive (首页专属) ---------- */
@media (max-width: 768px) {
.hero h1 {
@ -327,16 +409,15 @@
.hero-desc {
font-size: 1.05rem;
}
.timeline {
justify-content: flex-start;
gap: 1.5rem;
padding: 2rem 20px 1.5rem;
scroll-snap-type: x mandatory;
}
.timeline-item {
min-width: 240px;
max-width: 280px;
scroll-snap-align: start;
}
.timeline::before {
left: 20px;

View File

@ -0,0 +1,215 @@
/* ============================================================
MetaLab Settings Styles — 个人设置页样式
左侧导航 + 右侧卡片信息展示,只读
============================================================ */
/* ---------- Page Body ---------- */
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* ---------- Layout ---------- */
.settings-layout {
display: flex;
flex: 1;
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 2rem 20px;
gap: 2rem;
}
/* ---------- Left Sidebar Nav ---------- */
.settings-sidebar {
width: 220px;
flex-shrink: 0;
background: var(--color-surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--color-border);
padding: 1.25rem 1rem;
align-self: flex-start;
}
.settings-nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.settings-nav-item {
display: flex;
align-items: center;
gap: .6rem;
padding: .65rem 1rem;
border-radius: var(--radius);
font-size: .92rem;
color: var(--color-secondary);
font-weight: 500;
transition: all .15s ease;
}
.settings-nav-item:hover {
background: rgba(52, 152, 219, .06);
color: var(--color-accent);
}
.settings-nav-item.active {
background: rgba(52, 152, 219, .1);
color: var(--color-accent);
font-weight: 600;
}
.settings-nav-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
/* ---------- Right Content Area ---------- */
.settings-main {
flex: 1;
min-width: 0;
}
.settings-card {
background: var(--color-surface);
border-radius: var(--radius);
box-shadow: var(--shadow);
border: 1px solid var(--color-border);
}
.settings-card-header {
padding: 1.5rem 2rem 0;
}
.settings-card-header h2 {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-primary);
margin-bottom: .3rem;
}
.settings-card-desc {
font-size: .85rem;
color: var(--color-text-light);
padding-bottom: 1.25rem;
border-bottom: 1px solid var(--color-border);
margin-bottom: 0;
}
.settings-card-body {
padding: 1.25rem 2rem 1.5rem;
}
/* ---------- Info Rows ---------- */
.info-row {
display: flex;
align-items: flex-start;
padding: .9rem 0;
border-bottom: 1px solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
width: 100px;
flex-shrink: 0;
font-size: .88rem;
color: var(--color-text-light);
font-weight: 500;
padding-top: 1px;
}
.info-value {
flex: 1;
font-size: .92rem;
color: var(--color-text);
min-width: 0;
word-break: break-all;
}
.info-muted {
color: #aaa;
}
.info-mono {
font-family: 'SF Mono', 'Menlo', 'Monaco', monospace;
font-size: .85rem;
background: #f5f5f5;
padding: 1px 8px;
border-radius: 4px;
}
/* ---------- Avatar Preview ---------- */
.avatar-preview {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--color-border);
}
/* ---------- Role Tag ---------- */
.role-tag {
display: inline-block;
padding: 2px 10px;
border-radius: 4px;
font-size: .82rem;
font-weight: 600;
}
.role-user { background: #e3f2fd; color: #1976d2; }
.role-moderator { background: #e8f5e9; color: #2e7d32; }
.role-admin { background: #fce4ec; color: #c62828; }
.role-owner { background: #fff3e0; color: #e65100; }
/* ---------- Status Badge ---------- */
.status-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 12px;
font-size: .82rem;
font-weight: 600;
}
.status-active { background: #e8f5e9; color: #2e7d32; }
.status-banned { background: #ffebee; color: #c62828; }
.status-deleted { background: #fff3e0; color: #e65100; }
.status-locked { background: #eceff1; color: #546e7a; }
/* ---------- Responsive ---------- */
@media (max-width: 768px) {
.settings-layout {
flex-direction: column;
padding: 1rem 16px;
gap: 1rem;
}
.settings-sidebar {
width: 100%;
align-self: auto;
}
.settings-nav {
flex-direction: row;
gap: 2px;
}
.settings-nav-item {
padding: .5rem .9rem;
font-size: .85rem;
}
.settings-card-header {
padding: 1.25rem 1.25rem 0;
}
.settings-card-body {
padding: 1rem 1.25rem 1.25rem;
}
}

View File

@ -19,11 +19,8 @@
}, 3000);
};
// ---- CSRF token helper (reads from <meta> tag rendered by server) ----
window.MetaLab.csrfToken = function () {
var meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.getAttribute('content') : '';
};
// ---- CSRF token helper (从共享 utils.js 提供的 getCSRFToken 别名) ----
window.MetaLab.csrfToken = getCSRFToken;
// ---- Auto-attach X-CSRF-Token to all state-changing requests ----
// Monkey-patch XMLHttpRequest and fetch to inject CSRF header