feat: 新增创作中心(/studio),支持数据概览、内容管理、草稿箱、写文章
- 新增 StudioController,依赖 studioUseCase 接口,遵循 ISP 接口隔离 - PostRepo 新增 FindByUserIDAndStatus 方法,按用户+状态分页查询 - PostService 新增 ListByUser、GetOverview 及 StudioOverview 统计 - /studio 页面路由(概览/管理/草稿箱/写文章/数据分析)+ /api/studio API - 复用 Vditor 编辑器,studio-editor.js 对接 /api/studio/posts 端点 - 新增 studio.css(B站风格三栏布局+统计卡片+状态筛选tabs) - 模板引擎注册 add/subtract 函数,分页模板中直接使用
This commit is contained in:
72
templates/MetaLab-2026/html/studio/analytics.html
Normal file
72
templates/MetaLab-2026/html/studio/analytics.html
Normal file
@ -0,0 +1,72 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="studio-wrapper">
|
||||
{{template "studio/sidebar.html" .}}
|
||||
|
||||
<main class="studio-main">
|
||||
<div class="studio-header">
|
||||
<h1>数据分析</h1>
|
||||
<span class="studio-subtitle">了解你的创作表现</span>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="studio-error">{{.Error}}</div>
|
||||
{{else}}
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon">📝</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.TotalPosts}}</span>
|
||||
<span class="stat-card-label">文章总数</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-approved">
|
||||
<div class="stat-card-icon">✅</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.ApprovedCount}}</span>
|
||||
<span class="stat-card-label">已发布</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-draft">
|
||||
<div class="stat-card-icon">📄</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.DraftCount}}</span>
|
||||
<span class="stat-card-label">草稿</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="studio-section">
|
||||
<h2 class="section-title">状态分布</h2>
|
||||
<div class="status-bar">
|
||||
{{if gt .Overview.ApprovedCount 0}}
|
||||
<div class="status-bar-segment status-bar-approved" style="flex:{{.Overview.ApprovedCount}}">已发布 {{.Overview.ApprovedCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.DraftCount 0}}
|
||||
<div class="status-bar-segment status-bar-draft" style="flex:{{.Overview.DraftCount}}">草稿 {{.Overview.DraftCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.PendingCount 0}}
|
||||
<div class="status-bar-segment status-bar-pending" style="flex:{{.Overview.PendingCount}}">审核中 {{.Overview.PendingCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.RejectedCount 0}}
|
||||
<div class="status-bar-segment status-bar-rejected" style="flex:{{.Overview.RejectedCount}}">已退回 {{.Overview.RejectedCount}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="studio-section">
|
||||
<div class="studio-placeholder">
|
||||
<p>📈 更详细的阅读趋势、互动数据将在后续版本中推出</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
105
templates/MetaLab-2026/html/studio/drafts.html
Normal file
105
templates/MetaLab-2026/html/studio/drafts.html
Normal file
@ -0,0 +1,105 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="studio-wrapper">
|
||||
{{template "studio/sidebar.html" .}}
|
||||
|
||||
<main class="studio-main">
|
||||
<div class="studio-header">
|
||||
<h1>草稿箱</h1>
|
||||
<span class="studio-subtitle">共 {{.Total}} 篇草稿</span>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="studio-error">{{.Error}}</div>
|
||||
{{else}}
|
||||
{{if .Posts}}
|
||||
<div class="post-list">
|
||||
{{range .Posts}}
|
||||
<div class="post-item">
|
||||
<div class="post-item-main">
|
||||
<span class="post-item-title draft-title">{{if .Title}}{{.Title}}{{else}}无标题{{end}}</span>
|
||||
<div class="post-item-meta">
|
||||
<span class="post-status-badge status-draft">草稿</span>
|
||||
<span>{{.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-item-actions">
|
||||
<a href="/studio/write?id={{.ID}}" class="action-btn primary">继续编辑</a>
|
||||
<button class="action-btn" onclick="submitPost({{.ID}})" title="提交发布">提交</button>
|
||||
<button class="action-btn danger" onclick="deletePost({{.ID}})" title="删除">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if gt .Page 1}}
|
||||
<a href="/studio/drafts?page={{subtract .Page 1}}" class="page-btn">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页</span>
|
||||
{{if lt .Page .TotalPages}}
|
||||
<a href="/studio/drafts?page={{add .Page 1}}" class="page-btn">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="studio-empty">
|
||||
<p>✨ 草稿箱是空的</p>
|
||||
<a href="/studio/write" class="btn-primary">开始写作</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function submitPost(id) {
|
||||
if (!confirm('确定要提交这篇草稿进行审核吗?')) return;
|
||||
|
||||
fetch('/api/studio/posts/' + id + '/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': '{{.CSRFToken}}'
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '提交失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletePost(id) {
|
||||
if (!confirm('确定要删除这篇草稿吗?此操作不可恢复。')) return;
|
||||
|
||||
fetch('/api/studio/posts/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': '{{.CSRFToken}}'
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '删除失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
95
templates/MetaLab-2026/html/studio/overview.html
Normal file
95
templates/MetaLab-2026/html/studio/overview.html
Normal file
@ -0,0 +1,95 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="studio-wrapper">
|
||||
{{template "studio/sidebar.html" .}}
|
||||
|
||||
<main class="studio-main">
|
||||
<div class="studio-header">
|
||||
<h1>创作中心</h1>
|
||||
<span class="studio-subtitle">数据概览</span>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="studio-error">{{.Error}}</div>
|
||||
{{else}}
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stat-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-icon">📝</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.TotalPosts}}</span>
|
||||
<span class="stat-card-label">文章总数</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-approved">
|
||||
<div class="stat-card-icon">✅</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.ApprovedCount}}</span>
|
||||
<span class="stat-card-label">已发布</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-pending">
|
||||
<div class="stat-card-icon">⏳</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.PendingCount}}</span>
|
||||
<span class="stat-card-label">审核中</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-draft">
|
||||
<div class="stat-card-icon">📄</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.DraftCount}}</span>
|
||||
<span class="stat-card-label">草稿</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card-rejected">
|
||||
<div class="stat-card-icon">↩️</div>
|
||||
<div class="stat-card-body">
|
||||
<span class="stat-card-value">{{.Overview.RejectedCount}}</span>
|
||||
<span class="stat-card-label">已退回</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态分布 -->
|
||||
{{if gt .Overview.TotalPosts 0}}
|
||||
<div class="studio-section">
|
||||
<h2 class="section-title">内容分布</h2>
|
||||
<div class="status-bar">
|
||||
{{if gt .Overview.ApprovedCount 0}}
|
||||
<div class="status-bar-segment status-bar-approved" style="flex:{{.Overview.ApprovedCount}}">已发布 {{.Overview.ApprovedCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.DraftCount 0}}
|
||||
<div class="status-bar-segment status-bar-draft" style="flex:{{.Overview.DraftCount}}">草稿 {{.Overview.DraftCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.PendingCount 0}}
|
||||
<div class="status-bar-segment status-bar-pending" style="flex:{{.Overview.PendingCount}}">审核中 {{.Overview.PendingCount}}</div>
|
||||
{{end}}
|
||||
{{if gt .Overview.RejectedCount 0}}
|
||||
<div class="status-bar-segment status-bar-rejected" style="flex:{{.Overview.RejectedCount}}">已退回 {{.Overview.RejectedCount}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="studio-section">
|
||||
<h2 class="section-title">快捷操作</h2>
|
||||
<div class="quick-actions">
|
||||
<a href="/studio/write" class="quick-action-btn primary">✏️ 写新文章</a>
|
||||
<a href="/studio/drafts" class="quick-action-btn">📄 草稿箱{{if gt .Overview.DraftCount 0}}({{.Overview.DraftCount}}){{end}}</a>
|
||||
<a href="/studio/posts" class="quick-action-btn">📋 管理内容</a>
|
||||
<a href="/studio/analytics" class="quick-action-btn">📈 数据分析</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
105
templates/MetaLab-2026/html/studio/posts.html
Normal file
105
templates/MetaLab-2026/html/studio/posts.html
Normal file
@ -0,0 +1,105 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="studio-wrapper">
|
||||
{{template "studio/sidebar.html" .}}
|
||||
|
||||
<main class="studio-main">
|
||||
<div class="studio-header">
|
||||
<h1>内容管理</h1>
|
||||
{{if .Overview}}
|
||||
<div class="header-counts">
|
||||
<span class="header-count approved">已发布 {{.Overview.ApprovedCount}}</span>
|
||||
<span class="header-count pending">审核中 {{.Overview.PendingCount}}</span>
|
||||
<span class="header-count draft">草稿 {{.Overview.DraftCount}}</span>
|
||||
<span class="header-count rejected">已退回 {{.Overview.RejectedCount}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div class="studio-error">{{.Error}}</div>
|
||||
{{else}}
|
||||
<!-- 状态筛选 tabs -->
|
||||
<div class="status-tabs">
|
||||
<a href="/studio/posts" class="status-tab {{if not .CurrentStatus}}active{{end}}">全部</a>
|
||||
<a href="/studio/posts?status=approved" class="status-tab {{if eq .CurrentStatus "approved"}}active{{end}}">已发布</a>
|
||||
<a href="/studio/posts?status=pending" class="status-tab {{if eq .CurrentStatus "pending"}}active{{end}}">审核中</a>
|
||||
<a href="/studio/posts?status=draft" class="status-tab {{if eq .CurrentStatus "draft"}}active{{end}}">草稿</a>
|
||||
<a href="/studio/posts?status=rejected" class="status-tab {{if eq .CurrentStatus "rejected"}}active{{end}}">已退回</a>
|
||||
</div>
|
||||
|
||||
{{if .Posts}}
|
||||
<div class="post-list">
|
||||
{{range .Posts}}
|
||||
<div class="post-item">
|
||||
<div class="post-item-main">
|
||||
<a href="/posts/{{.ID}}" class="post-item-title" target="_blank">{{.Title}}</a>
|
||||
<div class="post-item-meta">
|
||||
<span class="post-status-badge status-{{.Status}}">{{index $.StatusNames .Status}}</span>
|
||||
<span>{{.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
||||
{{if .RejectReason}}
|
||||
<span class="post-reject-hint">退回理由:{{.RejectReason}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="post-item-actions">
|
||||
<a href="/studio/write?id={{.ID}}" class="action-btn">编辑</a>
|
||||
<a href="/posts/{{.ID}}" target="_blank" class="action-btn">查看</a>
|
||||
<button class="action-btn danger" onclick="deletePost({{.ID}})" title="删除">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
{{if gt .TotalPages 1}}
|
||||
<div class="pagination">
|
||||
{{if gt .Page 1}}
|
||||
<a href="/studio/posts?status={{$.CurrentStatus}}&page={{subtract .Page 1}}" class="page-btn">上一页</a>
|
||||
{{end}}
|
||||
<span class="page-info">第 {{.Page}} / {{.TotalPages}} 页(共 {{.Total}} 篇)</span>
|
||||
{{if lt .Page .TotalPages}}
|
||||
<a href="/studio/posts?status={{$.CurrentStatus}}&page={{add .Page 1}}" class="page-btn">下一页</a>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{else}}
|
||||
<div class="studio-empty">
|
||||
<p>还没有{{if .CurrentStatus}}{{index $.StatusNames .CurrentStatus}}{{end}}内容的文章</p>
|
||||
<a href="/studio/write" class="btn-primary">去写一篇</a>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function deletePost(id) {
|
||||
if (!confirm('确定要删除这篇文章吗?')) return;
|
||||
|
||||
fetch('/api/studio/posts/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': '{{.CSRFToken}}'
|
||||
}
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert(data.message || '删除失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
25
templates/MetaLab-2026/html/studio/sidebar.html
Normal file
25
templates/MetaLab-2026/html/studio/sidebar.html
Normal file
@ -0,0 +1,25 @@
|
||||
<aside class="studio-sidebar">
|
||||
<div class="sidebar-brand">创作中心</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="/studio" class="sidebar-item {{if eq .ActiveTab "overview"}}active{{end}}">
|
||||
<span class="sidebar-icon">📊</span>
|
||||
<span>数据概览</span>
|
||||
</a>
|
||||
<a href="/studio/posts" class="sidebar-item {{if eq .ActiveTab "posts"}}active{{end}}">
|
||||
<span class="sidebar-icon">📋</span>
|
||||
<span>内容管理</span>
|
||||
</a>
|
||||
<a href="/studio/drafts" class="sidebar-item {{if eq .ActiveTab "drafts"}}active{{end}}">
|
||||
<span class="sidebar-icon">📄</span>
|
||||
<span>草稿箱</span>
|
||||
</a>
|
||||
<a href="/studio/write" class="sidebar-item {{if eq .ActiveTab "write"}}active{{end}}">
|
||||
<span class="sidebar-icon">✏️</span>
|
||||
<span>写文章</span>
|
||||
</a>
|
||||
<a href="/studio/analytics" class="sidebar-item {{if eq .ActiveTab "analytics"}}active{{end}}">
|
||||
<span class="sidebar-icon">📈</span>
|
||||
<span>数据分析</span>
|
||||
</a>
|
||||
</nav>
|
||||
</aside>
|
||||
72
templates/MetaLab-2026/html/studio/write.html
Normal file
72
templates/MetaLab-2026/html/studio/write.html
Normal file
@ -0,0 +1,72 @@
|
||||
{{template "layout/header.html" .}}
|
||||
<body>
|
||||
|
||||
{{template "layout/nav.html" .}}
|
||||
|
||||
<div class="studio-wrapper">
|
||||
{{template "studio/sidebar.html" .}}
|
||||
|
||||
<main class="studio-main">
|
||||
<div class="editor-layout">
|
||||
<form id="postForm" class="editor-form">
|
||||
<input type="hidden" id="postId" value="{{if .Post}}{{.Post.ID}}{{end}}">
|
||||
{{if .Post}}
|
||||
<textarea id="editBody" style="display:none">{{.Post.Body}}</textarea>
|
||||
{{end}}
|
||||
|
||||
<div class="editor-header">
|
||||
<input type="text" id="postTitle" class="editor-title-input"
|
||||
value="{{if .Post}}{{.Post.Title}}{{end}}"
|
||||
placeholder="输入文章标题..."
|
||||
maxlength="200" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="editor-main">
|
||||
<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>></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>
|
||||
|
||||
<div class="editor-statusbar">
|
||||
<span class="status-item" id="wordCount">0 字</span>
|
||||
<span class="status-item status-draft" id="draftStatus" style="display:none">草稿已保存</span>
|
||||
<span class="status-spacer"></span>
|
||||
<button type="submit" class="btn btn-primary btn-submit">{{if .Post}}保存修改{{else}}发布帖子{{end}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{{template "layout/footer.html" .}}
|
||||
|
||||
<link rel="stylesheet" href="/static/vditor/dist/index.css">
|
||||
<script src="/static/vditor/dist/index.min.js"></script>
|
||||
<script src="/static/js/common.js"></script>
|
||||
<script src="/static/js/studio-editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user