diff --git a/internal/controller/interfaces.go b/internal/controller/interfaces.go
index 1769af2..fc055b2 100644
--- a/internal/controller/interfaces.go
+++ b/internal/controller/interfaces.go
@@ -42,9 +42,9 @@ type postUseCase interface {
// studioUseCase StudioController 对 PostService 的最小依赖(ISP:9 个方法)
type studioUseCase interface {
- Create(userID uint, title, body string) (*model.Post, error)
+ Create(userID uint, title, body, visibility, postType, reprintSource, declaration string, reprintProhibited bool) (*model.Post, error)
GetByID(id uint) (*model.Post, error)
- Update(postID uint, title, body string) error
+ Update(postID uint, title, body, visibility, postType, reprintSource, declaration string, reprintProhibited bool) error
Delete(postID uint) error
SubmitForAudit(postID uint) error
ListByUser(userID uint, status string, page, pageSize int) ([]model.Post, int64, error)
diff --git a/internal/controller/studio_post_api_controller.go b/internal/controller/studio_post_api_controller.go
index c8ec4b4..082ffa6 100644
--- a/internal/controller/studio_post_api_controller.go
+++ b/internal/controller/studio_post_api_controller.go
@@ -30,7 +30,7 @@ func (ctrl *StudioController) Create(c *gin.Context) {
return
}
- post, err := ctrl.postService.Create(uid, req.Title, req.Body)
+ post, err := ctrl.postService.Create(uid, req.Title, req.Body, req.Visibility, req.PostType, req.ReprintSource, req.Declaration, req.ReprintProhibited)
if err != nil {
common.Error(c, http.StatusInternalServerError, "发布失败")
return
@@ -69,7 +69,7 @@ func (ctrl *StudioController) Update(c *gin.Context) {
return
}
- if err := ctrl.postService.Update(uint(id), req.Title, req.Body); err != nil {
+ if err := ctrl.postService.Update(uint(id), req.Title, req.Body, req.Visibility, req.PostType, req.ReprintSource, req.Declaration, req.ReprintProhibited); err != nil {
common.Error(c, http.StatusInternalServerError, "编辑失败")
return
}
diff --git a/internal/model/dto.go b/internal/model/dto.go
index 4a486dd..bdc807d 100644
--- a/internal/model/dto.go
+++ b/internal/model/dto.go
@@ -32,14 +32,24 @@ type PostListResult struct {
// PostCreateRequest 发帖请求
type PostCreateRequest struct {
- Title string `json:"title" binding:"required,min=1,max=200"`
- Body string `json:"body" binding:"required,min=1"`
+ Title string `json:"title" binding:"required,min=1,max=200"`
+ Body string `json:"body" binding:"required,min=1"`
+ Visibility string `json:"visibility"`
+ PostType string `json:"post_type"`
+ ReprintSource string `json:"reprint_source"`
+ Declaration string `json:"declaration"`
+ ReprintProhibited bool `json:"reprint_prohibited"`
}
// PostUpdateRequest 编辑请求
type PostUpdateRequest struct {
- Title string `json:"title" binding:"required,min=1,max=200"`
- Body string `json:"body" binding:"required,min=1"`
+ Title string `json:"title" binding:"required,min=1,max=200"`
+ Body string `json:"body" binding:"required,min=1"`
+ Visibility string `json:"visibility"`
+ PostType string `json:"post_type"`
+ ReprintSource string `json:"reprint_source"`
+ Declaration string `json:"declaration"`
+ ReprintProhibited bool `json:"reprint_prohibited"`
}
// PostRejectRequest 退回请求
diff --git a/internal/service/post_audit_service.go b/internal/service/post_audit_service.go
index 437a9b8..8d81f23 100644
--- a/internal/service/post_audit_service.go
+++ b/internal/service/post_audit_service.go
@@ -6,7 +6,7 @@ import (
)
// Update 编辑帖子(权限在 controller 层检查)
-func (s *PostService) Update(postID uint, title, body string) error {
+func (s *PostService) Update(postID uint, title, body string, visibility, postType, reprintSource, declaration string, reprintProhibited bool) error {
post, err := s.findPost(postID)
if err != nil {
return err
@@ -16,6 +16,13 @@ func (s *PostService) Update(postID uint, title, body string) error {
return common.ErrPostCannotEdit
}
+ // 文章属性随时可更新(不影响已发布内容展示)
+ post.Visibility = visibility
+ post.PostType = postType
+ post.ReprintSource = reprintSource
+ post.Declaration = declaration
+ post.ReprintProhibited = reprintProhibited
+
// 已通过帖子:编辑内容保存为待审修订,不影响当前展示
if post.Status == model.PostStatusApproved {
post.PendingTitle = title
diff --git a/internal/service/post_service.go b/internal/service/post_service.go
index 94a0b99..ee753f4 100644
--- a/internal/service/post_service.go
+++ b/internal/service/post_service.go
@@ -68,19 +68,24 @@ func (s *PostService) findPostWithAuthor(id uint) (*model.Post, error) {
// Create 创建帖子
// body 为 Vditor 输出的 Markdown,后端直接存储不做 HTML 消毒
// MD 是纯文本,无 XSS 注入风险;前端渲染时由 Vditor 转 HTML
-func (s *PostService) Create(userID uint, title, body string) (*model.Post, error) {
+func (s *PostService) Create(userID uint, title, body string, visibility, postType, reprintSource, declaration string, reprintProhibited bool) (*model.Post, error) {
status := model.PostStatusApproved
if s.auditEnabled() {
status = model.PostStatusDraft
}
post := &model.Post{
- Title: title,
- Body: body,
- Excerpt: generateExcerpt(body),
- UserID: userID,
- Status: status,
- AllowComment: true,
+ Title: title,
+ Body: body,
+ Excerpt: generateExcerpt(body),
+ UserID: userID,
+ Status: status,
+ AllowComment: true,
+ Visibility: visibility,
+ PostType: postType,
+ ReprintSource: reprintSource,
+ Declaration: declaration,
+ ReprintProhibited: reprintProhibited,
}
if err := s.repo.Create(post); err != nil {
return nil, err
diff --git a/templates/MetaLab-2026/html/posts/show.html b/templates/MetaLab-2026/html/posts/show.html
index 36c33a9..ae70611 100644
--- a/templates/MetaLab-2026/html/posts/show.html
+++ b/templates/MetaLab-2026/html/posts/show.html
@@ -18,6 +18,12 @@
收藏:{{.Post.FavoritesCount}}
评论:{{.Post.CommentsCount}}
发布时间:{{.Post.CreatedAt.Format "2006-01-02 15:04"}}
+ {{if eq .Post.Visibility "private"}}
+ 私密
+ {{end}}
+ {{if .Post.PinType}}
+ 📌 {{if eq .Post.PinType "global"}}全局置顶{{else}}分类置顶{{end}}
+ {{end}}
{{if and .Post.IsLocked (eq $.Post.UserID $.UID)}}
已锁定{{if .Post.LockReason}}:{{.Post.LockReason}}{{end}}
{{end}}
@@ -25,6 +31,19 @@
修订审核中
{{end}}
+
+ {{if or .Post.Declaration .Post.ReprintSource .Post.ReprintProhibited (eq .Post.PostType "reprint")}}
+
+ {{if .Post.Declaration}}
+
⚡ {{declarationLabel .Post.Declaration}}
+ {{end}}
+ {{if eq .Post.PostType "reprint"}}
+
📎 转载自:{{.Post.ReprintSource}}
+ {{else if .Post.ReprintProhibited}}
+
⚠ 未经作者许可,禁止转载
+ {{end}}
+
+ {{end}}
{{if .Post.RejectReason}}
diff --git a/templates/MetaLab-2026/html/studio/write.html b/templates/MetaLab-2026/html/studio/write.html
index fb0c419..fc8dd71 100644
--- a/templates/MetaLab-2026/html/studio/write.html
+++ b/templates/MetaLab-2026/html/studio/write.html
@@ -45,6 +45,40 @@
{{if and .Post .Post.IsLocked}}disabled{{end}}>
+
+
+
diff --git a/templates/MetaLab-2026/static/css/posts.css b/templates/MetaLab-2026/static/css/posts.css
index f31d486..ba3db63 100644
--- a/templates/MetaLab-2026/static/css/posts.css
+++ b/templates/MetaLab-2026/static/css/posts.css
@@ -1374,3 +1374,39 @@
margin-top: 2px;
padding-top: 6px;
}
+
+/* --- 文章属性声明 --- */
+.post-declarations {
+ margin-top: 10px;
+ padding: 10px 14px;
+ background: #fffbeb;
+ border: 1px solid #fde68a;
+ border-radius: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.post-declaration-item {
+ font-size: 13px;
+ color: #92400e;
+ line-height: 1.5;
+}
+
+.post-badge {
+ display: inline-block;
+ padding: 1px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.post-badge-private {
+ background: #f3f4f6;
+ color: #6b7280;
+}
+
+.post-badge-pinned {
+ background: #dbeafe;
+ color: #1d4ed8;
+}
diff --git a/templates/MetaLab-2026/static/css/studio.css b/templates/MetaLab-2026/static/css/studio.css
index 37c8d9e..2817e92 100644
--- a/templates/MetaLab-2026/static/css/studio.css
+++ b/templates/MetaLab-2026/static/css/studio.css
@@ -759,3 +759,48 @@
width: 100%;
}
}
+
+/* --- Editor Meta (文章属性) --- */
+.editor-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 24px;
+ padding: 10px 16px;
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ margin-bottom: 12px;
+ font-size: 14px;
+}
+
+.meta-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.meta-label {
+ font-weight: 600;
+ color: #374151;
+ min-width: 40px;
+}
+
+.meta-radio {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+ color: #4b5563;
+}
+
+.meta-radio input[type="radio"] {
+ margin: 0;
+}
+
+.meta-checkbox {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+ color: #4b5563;
+}
diff --git a/templates/MetaLab-2026/static/js/studio-editor.js b/templates/MetaLab-2026/static/js/studio-editor.js
index e884c63..310f15a 100644
--- a/templates/MetaLab-2026/static/js/studio-editor.js
+++ b/templates/MetaLab-2026/static/js/studio-editor.js
@@ -98,6 +98,16 @@ document.addEventListener('DOMContentLoaded', () => {
document.getElementById('postForm')?.addEventListener('submit', handleSubmit)
+ // 类型切换联动:转载→显示来源,原创→显示禁止转载
+ const typeRadios = document.querySelectorAll('input[name="post_type"]')
+ const reprintSourceArea = document.getElementById('reprintSourceArea')
+ const reprintProhibitArea = document.getElementById('reprintProhibitArea')
+ typeRadios.forEach(r => r.addEventListener('change', () => {
+ const isReprint = document.querySelector('input[name="post_type"]:checked')?.value === 'reprint'
+ if (reprintSourceArea) reprintSourceArea.style.display = isReprint ? '' : 'none'
+ if (reprintProhibitArea) reprintProhibitArea.style.display = isReprint ? 'none' : ''
+ }))
+
if (!isLocked) {
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
@@ -174,6 +184,13 @@ async function handleSubmit(e) {
const body = vditor ? vditor.getValue() : ''
if (!body.trim()) { showToast('请输入正文', 'warning'); return }
+ // 读取文章属性
+ const visibility = document.querySelector('input[name="visibility"]:checked')?.value || 'public'
+ const postType = document.querySelector('input[name="post_type"]:checked')?.value || 'original'
+ const reprintSource = document.getElementById('reprintSource')?.value.trim() || ''
+ const declaration = document.getElementById('declaration')?.value || ''
+ const reprintProhibited = document.getElementById('reprintProhibited')?.checked || false
+
const isEdit = !!postId
const url = isEdit ? '/api/studio/posts/' + postId : '/api/studio/posts'
const method = isEdit ? 'PUT' : 'POST'
@@ -184,7 +201,13 @@ async function handleSubmit(e) {
'Content-Type': 'application/json',
'X-CSRF-Token': getCSRFToken(),
},
- body: JSON.stringify({ title, body }),
+ body: JSON.stringify({
+ title, body,
+ visibility, post_type: postType,
+ reprint_source: reprintSource,
+ declaration,
+ reprint_prohibited: reprintProhibited,
+ }),
})
const data = await resp.json()
diff --git a/templates/admin/html/posts/index.html b/templates/admin/html/posts/index.html
index edb1668..6be590b 100644
--- a/templates/admin/html/posts/index.html
+++ b/templates/admin/html/posts/index.html
@@ -42,7 +42,7 @@
{{.ID}} |
{{.Title}}{{if .PendingBody}} 修订中{{end}} |
{{if .AuthorName}}{{.AuthorName}}({{.UserID}}){{else}}UID{{.UserID}}{{end}} |
-
{{index $.StatusNames .Status}}{{if .IsLocked}} 已锁定{{end}} |
+
{{index $.StatusNames .Status}}{{if .IsLocked}} 已锁定{{end}}{{if .PinType}} 📌{{end}} |
{{.CreatedAt.Format "2006-01-02 15:04"}} |
@@ -63,6 +63,14 @@
{{if .IsLocked}}
{{end}}
+ {{if eq .Status "approved"}}
+ {{if not .PinType}}
+
+
+ {{else}}
+
+ {{end}}
+ {{end}}
|
diff --git a/templates/admin/static/css/posts.css b/templates/admin/static/css/posts.css
index fbe08e5..9be6413 100644
--- a/templates/admin/static/css/posts.css
+++ b/templates/admin/static/css/posts.css
@@ -80,6 +80,7 @@
.status-approved { background: #d1fae5; color: #059669; }
.status-rejected { background: #fee2e2; color: #dc2626; }
.status-locked { background: #fee2e2; color: #b91c1c; }
+.status-pinned { background: #dbeafe; color: #1d4ed8; }
.revision-section { margin-bottom: 24px; }
.revision-section h2 { font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #4338ca; }
@@ -111,6 +112,7 @@
.btn-success { background: #10b981; color: #fff; }
.btn-warning { background: #f59e0b; color: #fff; }
.btn-danger { background: #ef4444; color: #fff; }
+.btn-info { background: #3b82f6; color: #fff; }
/* 分页 */
.pagination {
diff --git a/templates/admin/static/js/posts.js b/templates/admin/static/js/posts.js
index 23ba280..c6587b9 100644
--- a/templates/admin/static/js/posts.js
+++ b/templates/admin/static/js/posts.js
@@ -52,4 +52,22 @@
.then(function(d) { if (d.success) refreshPage(); else showToast(d.message || '操作失败', 'error'); })
.catch(function() { showToast('操作失败', 'error'); });
});
+
+ // 置顶
+ document.querySelectorAll('.post-pin').forEach(function(btn) {
+ btn.addEventListener('click', function() {
+ var id = btn.getAttribute('data-id');
+ var pinType = btn.getAttribute('data-pin-type');
+ api.post('/api/admin/posts/' + id + '/pin', { pin_type: pinType })
+ .then(function(d) { if (d.success) refreshPage(); else showToast(d.message || '操作失败', 'error'); })
+ .catch(function() { showToast('操作失败', 'error'); });
+ });
+ });
+
+ // 取消置顶
+ handleClick('.post-unpin', '确定取消置顶吗?', function(id) {
+ api.post('/api/admin/posts/' + id + '/unpin')
+ .then(function(d) { if (d.success) refreshPage(); else showToast(d.message || '操作失败', 'error'); })
+ .catch(function() { showToast('操作失败', 'error'); });
+ });
})();