diff --git a/internal/common/helper.go b/internal/common/helper.go
index 8be8312..654f2e6 100644
--- a/internal/common/helper.go
+++ b/internal/common/helper.go
@@ -86,6 +86,9 @@ func BuildPageData(c *gin.Context, extra gin.H) gin.H {
data["IsLoggedIn"] = true
data["Username"] = username
}
+ if avatar, exists := c.Get("avatar"); exists {
+ data["Avatar"] = avatar
+ }
// 注入 CSRF token(由 CSRF 中间件设置到上下文中)
if token, exists := c.Get("csrf_token"); exists {
data["CSRFToken"] = token
diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go
index e7b3247..650beef 100644
--- a/internal/middleware/auth.go
+++ b/internal/middleware/auth.go
@@ -77,6 +77,7 @@ func injectSessionContext(c *gin.Context, s *session.Session) {
c.Set("uid", s.UserID)
c.Set("email", s.Email)
c.Set("username", s.Username)
+ c.Set("avatar", s.Avatar)
c.Set("role", s.Role)
c.Set("sid", s.ID)
}
diff --git a/internal/repository/user_repo.go b/internal/repository/user_repo.go
index 4f0af6c..5d2dd42 100644
--- a/internal/repository/user_repo.go
+++ b/internal/repository/user_repo.go
@@ -74,11 +74,10 @@ func (r *UserRepo) ExistsByEmailExclude(email string, excludeUID uint) (bool, er
return count > 0, err
}
-// FindByIDForAuth 认证专用查询:返回 uid/email/username/role/status
-// 比 FindByID 轻量(不查 avatar、bio 等无用字段),避免全量 SELECT *
+// FindByIDForAuth 认证专用查询:返回 uid/email/username/avatar/role/status
func (r *UserRepo) FindByIDForAuth(userID uint) (*model.User, error) {
var user model.User
- err := r.db.Select("uid", "email", "username", "role", "status").
+ err := r.db.Select("uid", "email", "username", "avatar", "role", "status").
First(&user, userID).Error
if err != nil {
return nil, err
diff --git a/internal/session/manager.go b/internal/session/manager.go
index 11332fc..3123a50 100644
--- a/internal/session/manager.go
+++ b/internal/session/manager.go
@@ -37,6 +37,7 @@ func (m *Manager) Create(user *model.User, rememberMe bool, ip, userAgent string
UserID: user.ID,
Email: user.Email,
Username: user.Username,
+ Avatar: user.Avatar,
Role: user.Role,
RememberMe: rememberMe,
IP: ip,
diff --git a/internal/session/session.go b/internal/session/session.go
index 0088742..878dbc0 100644
--- a/internal/session/session.go
+++ b/internal/session/session.go
@@ -12,6 +12,7 @@ type Session struct {
UserID uint // 用户 ID
Email string // 邮箱
Username string // 用户名
+ Avatar string // 头像 URL
Role string // 角色
RememberMe bool // 是否持久化(决定 cookie maxAge)
IP string // 登录 IP
diff --git a/internal/theme/loader.go b/internal/theme/loader.go
index e678148..a9ccc0e 100644
--- a/internal/theme/loader.go
+++ b/internal/theme/loader.go
@@ -24,6 +24,17 @@ func LoadTemplates(roots ...TemplateRoot) (*template.Template, error) {
"subtract": func(a, b int) int { return a - b },
"formatTTL": formatTTL,
"assetV": common.AssetV,
+ "substr": func(s string, start, length int) string {
+ runes := []rune(s)
+ if start >= len(runes) {
+ return ""
+ }
+ end := start + length
+ if end > len(runes) {
+ end = len(runes)
+ }
+ return string(runes[start:end])
+ },
// derefUint 安全解引用 *uint 指针。
// Go 1.26 的模板引擎在将 *uint 传给 printf/print 时不会自动解引用,
// 导致打印指针地址而非值。通过此函数显式解引用后传给 printf。
diff --git a/templates/MetaLab-2026/html/layout/nav.html b/templates/MetaLab-2026/html/layout/nav.html
index c7d9c4f..d4650b6 100644
--- a/templates/MetaLab-2026/html/layout/nav.html
+++ b/templates/MetaLab-2026/html/layout/nav.html
@@ -6,21 +6,31 @@
- 首页
- - 帖子
{{if .IsLoggedIn}}
-
投稿
-
-
-
- 0
+
+
+ 0
- - {{.Username}}
- - 设置
- - 退出
+ -
+
+ {{if .Avatar}}
+
+ {{else}}
+ {{substr .Username 0 1}}
+ {{end}}
+
+
+
{{else}}
- 登录
{{end}}
diff --git a/templates/MetaLab-2026/static/css/common.css b/templates/MetaLab-2026/static/css/common.css
index 6f981be..e5a2818 100644
--- a/templates/MetaLab-2026/static/css/common.css
+++ b/templates/MetaLab-2026/static/css/common.css
@@ -79,13 +79,16 @@ header {
display: flex;
justify-content: space-between;
align-items: center;
- padding: .6rem 0;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: .6rem 2rem;
}
.logo {
font-size: 1.4rem;
font-weight: 700;
color: var(--color-primary);
+ flex-shrink: 0;
}
.logo span {
@@ -94,11 +97,19 @@ header {
.nav-links {
display: flex;
+ align-items: center;
list-style: none;
- gap: 2rem;
+ gap: 1.5rem;
+}
+
+.nav-links li {
+ display: flex;
+ align-items: center;
}
.nav-links a {
+ display: flex;
+ align-items: center;
font-weight: 500;
padding: .5rem 0;
position: relative;
@@ -131,26 +142,11 @@ header {
font-weight: 600 !important;
}
-.nav-user {
- color: var(--color-accent) !important;
- font-weight: 600 !important;
-}
-
-.nav-settings {
- font-size: .9rem;
- color: var(--color-secondary) !important;
-}
-
-.nav-logout {
- color: var(--color-secondary) !important;
-}
-
/* ---------- Nav Contribute ---------- */
.nav-contribute {
- display: inline-flex !important;
+ display: flex;
align-items: center;
- vertical-align: middle;
- line-height: 1;
+ gap: .25rem;
color: var(--color-accent) !important;
font-weight: 600 !important;
}
@@ -158,36 +154,35 @@ header {
.nav-contribute .icon {
width: 16px;
height: 16px;
- display: block;
- margin-right: 4px;
}
-.nav-contribute:hover {
- opacity: .8;
-}
-
-/* ---------- Nav Messages Bell ---------- */
-.nav-messages {
+/* ---------- Nav Envelope ---------- */
+.nav-envelope {
position: relative;
- display: inline-flex;
+ display: flex;
align-items: center;
justify-content: center;
- padding: 6px;
+ padding: 4px;
border-radius: 50%;
transition: background .15s ease;
}
-.nav-messages:hover {
+.nav-links .nav-envelope::after,
+.nav-links .nav-envelope:hover::after {
+ display: none;
+}
+
+.nav-envelope:hover {
background: rgba(52, 152, 219, .08);
}
-.nav-bell-icon {
+.nav-envelope-icon {
width: 20px;
height: 20px;
display: block;
}
-.nav-bell-badge {
+.nav-envelope-badge {
position: absolute;
top: 0;
right: -2px;
@@ -205,6 +200,97 @@ header {
line-height: 1;
}
+/* ---------- Nav Avatar Dropdown ---------- */
+.nav-avatar-wrapper {
+ position: relative;
+}
+
+.nav-avatar-link {
+ display: flex !important;
+ align-items: center;
+ padding: 2px !important;
+}
+
+.nav-links .nav-avatar-link::after,
+.nav-links .nav-avatar-link:hover::after {
+ display: none;
+}
+
+.nav-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ object-fit: cover;
+ border: 2px solid var(--color-border);
+ transition: border-color .2s ease;
+}
+
+.nav-avatar-default {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--color-accent);
+ color: #fff;
+ font-size: .85rem;
+ font-weight: 600;
+ object-fit: unset;
+}
+
+.nav-avatar-wrapper:hover .nav-avatar {
+ border-color: var(--color-accent);
+}
+
+.nav-dropdown {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ min-width: 160px;
+ background: var(--color-surface);
+ border-radius: var(--radius);
+ box-shadow: 0 4px 16px rgba(0,0,0,.12);
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-4px);
+ transition: opacity .2s ease, visibility .2s ease, transform .2s ease;
+ overflow: hidden;
+}
+
+.nav-avatar-wrapper:hover .nav-dropdown {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.nav-dropdown-header {
+ padding: .75rem 1rem;
+ font-weight: 600;
+ font-size: .9rem;
+ color: var(--color-text);
+ border-bottom: 1px solid var(--color-border);
+ white-space: nowrap;
+}
+
+.nav-dropdown-item {
+ display: block !important;
+ padding: .6rem 1rem !important;
+ font-size: .9rem;
+ color: var(--color-text-light) !important;
+ transition: background .15s ease, color .15s ease;
+}
+
+.nav-dropdown-item::after {
+ display: none !important;
+}
+
+.nav-dropdown-item:hover {
+ background: var(--color-bg);
+ color: var(--color-accent) !important;
+}
+
+.nav-dropdown-item.nav-logout:hover {
+ color: var(--color-danger) !important;
+}
+
.mobile-menu-btn {
display: none;
background: none;
@@ -309,6 +395,9 @@ footer {
/* ---------- Responsive (共享:导航) ---------- */
@media (max-width: 768px) {
+ .nav-container {
+ padding: .6rem 1rem;
+ }
.nav-links {
display: none;
position: absolute;
@@ -317,13 +406,28 @@ footer {
width: 100%;
background: var(--color-surface);
flex-direction: column;
- padding: 1.5rem;
+ padding: 1rem;
box-shadow: 0 10px 15px rgba(0,0,0,.1);
- text-align: center;
}
.nav-links.active {
display: flex;
}
+ .nav-links li {
+ justify-content: center;
+ }
+ .nav-avatar-wrapper {
+ justify-content: center;
+ }
+ .nav-dropdown {
+ position: static;
+ opacity: 1;
+ visibility: visible;
+ transform: none;
+ box-shadow: none;
+ border-top: 1px solid var(--color-border);
+ border-radius: 0;
+ margin-top: .5rem;
+ }
.mobile-menu-btn {
display: block;
}