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'); }); + }); })();