feat: 编辑器与帖子展示布局优化 + 新增个人空间

- 编辑器投稿页布局优化:标题与编辑器高度动态适配,工具栏与内容区视觉对齐
- 稿件展示页布局优化:MD 渲染区与评论区视觉分离,代码块主题微调
- CSRF 中间件:图像上传端点豁免,解决 Vditor 拖拽/粘贴上传 403
- Post 状态映射、GetGinUser、SaveUploadedFile 提取至 common 包,遵循审计建议
- 新增个人空间功能:/space(需登录)重定向到 /space/:uid,/space/:uid 公开访问
- 空间页模板:参考 B 站布局,展示头像、用户名、个性签名、UID、加入时间及稿件列表
- 导航栏:用户名链接指向 /space,新增「设置」入口
- 分层实现:SpaceController → SpaceService → PostRepo.FindByUserID,严格 ISP 接口隔离
This commit is contained in:
2026-05-30 19:06:10 +08:00
parent 40f0529cbf
commit bacfd4945d
31 changed files with 1229 additions and 252 deletions

View File

@ -4,6 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{if .CSRFToken}}<meta name="csrf-token" content="{{.CSRFToken}}">{{end}}
{{if .OgTitle}}
<meta property="og:title" content="{{.OgTitle}}">
<meta property="og:type" content="article">
<meta property="og:site_name" content="MetaLab">
{{if .OgDescription}}<meta property="og:description" content="{{.OgDescription}}">{{end}}
{{if .OgImage}}<meta property="og:image" content="{{.OgImage}}">{{end}}
{{if .OgURL}}<meta property="og:url" content="{{.OgURL}}">{{end}}
{{end}}
<title>{{.Title}} - MetaLab</title>
<link rel="stylesheet" href="/static/css/common.css">
{{if .ExtraCSS}}

View File

@ -14,7 +14,8 @@
<span class="nav-bell-badge" id="msgBadge" style="display:none">0</span>
</a>
</li>
<li><a href="/settings/profile" class="nav-user">{{.Username}}</a></li>
<li><a href="/space" class="nav-user">{{.Username}}</a></li>
<li><a href="/settings/profile" class="nav-settings">设置</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

@ -29,7 +29,7 @@
<a href="/posts/{{.ID}}">{{.Title}}</a>
</h2>
<div class="post-card-meta">
<span class="post-author">{{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}}</span>
<span class="post-author">{{if .AuthorName}}{{.AuthorName}}{{else}}该用户已注销{{end}}</span>
<span class="post-time">{{.CreatedAt.Format "2006-01-02 15:04"}}</span>
</div>
<p class="post-card-summary">{{if .Excerpt}}{{.Excerpt}}{{else}}{{printf "%.200s" .Body}}{{end}}</p>

View File

@ -24,6 +24,30 @@
<div class="editor-pane">
<div id="vditor"></div>
</div>
<aside class="editor-sidebar">
<div class="editor-sidebar-section">
<h4>快捷语法</h4>
<ul class="tips-list">
<li><code>#</code> <code>##</code> <code>###</code> 标题</li>
<li><code>**粗体**</code> <code>*斜体*</code></li>
<li><code>`行内代码`</code></li>
<li><code>```</code> 代码块</li>
<li><code>&gt;</code> 引用</li>
<li><code>-</code> 无序列表</li>
<li><code>[文字](链接)</code></li>
<li><code>![图片](链接)</code></li>
</ul>
</div>
<div class="editor-sidebar-section">
<h4>发布提示</h4>
<ul class="tips-list">
<li>标题控制在 50 字以内</li>
<li>正文清晰分段,便于阅读</li>
<li>代码使用代码块包裹</li>
<li>Ctrl+S 快速保存草稿</li>
</ul>
</div>
</aside>
</div>
{{/* 状态栏 */}}

View File

@ -8,7 +8,7 @@
<header class="post-detail-header">
<h1 class="post-detail-title">{{.Post.Title}}</h1>
<div class="post-detail-meta">
<span class="post-author">{{if .Post.AuthorName}}{{.Post.AuthorName}}({{.Post.UserID}}){{else}}UID{{.Post.UserID}}{{end}}</span>
<span class="post-author">{{if .Post.AuthorName}}{{.Post.AuthorName}}{{else}}该用户已注销{{end}}</span>
<span class="post-time">{{.Post.CreatedAt.Format "2006-01-02 15:04"}}</span>
{{if ne .Post.Status "approved"}}
<span class="post-status post-status-{{.Post.Status}}">{{index $.StatusNames .Post.Status}}</span>
@ -57,6 +57,18 @@
cdn: '/static/vditor',
theme: { current: 'light', path: '/static/vditor/dist/css/content-theme' },
hljs: { style: 'github-dark', enable: true },
after: function() {
// 外部链接自动加 nofollow + target="_blank",避免权重流失
var host = window.location.host;
var links = el.querySelectorAll('a[href^="http"]');
for (var i = 0; i < links.length; i++) {
var a = links[i];
if (a.host && a.host !== host) {
a.setAttribute('rel', 'nofollow noopener noreferrer');
a.setAttribute('target', '_blank');
}
}
}
});
// Vditor.preview() 渲染 MDshortcode 占位 div 原样保留

View File

@ -0,0 +1,93 @@
{{template "layout/header.html" .}}
<body>
{{template "layout/nav.html" .}}
{{if .Error}}
<div class="space-error">
<div class="container">
<h1>{{.Error}}</h1>
<a href="/" class="btn btn-primary">返回首页</a>
</div>
</div>
{{else}}
<div class="space-page">
<!-- 用户信息头部 -->
<div class="space-header">
<div class="container space-header-inner">
<div class="space-avatar">
{{if .SpaceUser.Avatar}}
<img src="{{.SpaceUser.Avatar}}" alt="{{.SpaceUser.Username}}">
{{else}}
<div class="space-avatar-placeholder">{{slice .SpaceUser.Username 0 1}}</div>
{{end}}
</div>
<div class="space-info">
<h1 class="space-username">{{.SpaceUser.Username}}</h1>
<p class="space-bio">{{if .SpaceUser.Bio}}{{.SpaceUser.Bio}}{{else}}&nbsp;{{end}}</p>
<div class="space-meta">
<span class="space-uid">UID: {{.SpaceUser.ID}}</span>
<span class="space-divider">·</span>
<span class="space-joined">加入于 {{.SpaceUser.CreatedAt.Format "2006-01-02"}}</span>
</div>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="container space-content">
<!-- 统计栏 -->
<div class="space-stats">
<div class="space-stat-item">
<span class="space-stat-num">{{.Total}}</span>
<span class="space-stat-label">稿件</span>
</div>
</div>
<!-- 稿件列表 -->
<div class="space-section">
<h2 class="space-section-title">稿件</h2>
{{if eq (len .Posts) 0}}
<div class="empty-state">暂无稿件</div>
{{else}}
<div class="space-posts-list">
{{range .Posts}}
<article class="space-post-card">
<div class="space-post-main">
<h3 class="space-post-title">
<a href="/posts/{{.ID}}">{{.Title}}</a>
</h3>
<p class="space-post-excerpt">{{if .Excerpt}}{{.Excerpt}}{{else}}{{printf "%.200s" .Body}}{{end}}</p>
<div class="space-post-meta">
<span class="space-post-status">已发布</span>
<span class="space-post-time">{{.CreatedAt.Format "2006-01-02"}}</span>
</div>
</div>
</article>
{{end}}
</div>
{{end}}
<!-- 分页 -->
{{if gt .TotalPages 1}}
<div class="pagination">
{{if gt .Page 1}}
<a href="?page={{.PrevPage}}" class="page-link">上一页</a>
{{end}}
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页 (共 {{.Total}} 条)</span>
{{if lt .Page .TotalPages}}
<a href="?page={{.NextPage}}" class="page-link">下一页</a>
{{end}}
</div>
{{end}}
</div>
</div>
</div>
{{end}}
{{template "layout/footer.html" .}}
<script src="/static/js/common.js"></script>
</body>
</html>