feat: 新增发布日期自选、站点 Logo、回收站清空等十项改进
- 编辑页标题改 filled 变体;标签改 tags 选择器(可勾选既有/输入新建) - 笔记发布日期可自选(schema v2 新增 published_at 并回填),前台展示发布日期 - 登录页移除「请输入管理密码以继续」「连续失败将被暂时锁定」文案 - 站点设置每页条数收敛为 10/20/30/50 选择器,后端白名单同源校验 - 站点设置两卡片宽屏左右/窄屏上下;修改密码按钮改常规大小 - 后台各页顶栏固定高度,切换页面不再抖动 - 站点 Logo:settings 新增 site_logo(站内路径),gc/孤儿清单豁免 Logo 图片, 博客 header 站点名前展示,设置页支持上传/更换/清除 - 回收站右上角一键清空(DELETE /api/admin/trash,含确认弹窗) - pn init 写入 Markdown 语法示例文档(slug welcome,公开)与程序生成的示例图片
This commit is contained in:
+33
-21
@@ -9,31 +9,34 @@ import (
|
||||
)
|
||||
|
||||
// Note 笔记实体。DeletedAt 非 nil 表示处于回收站(软删除)。
|
||||
// PublishedAt 为对外展示的发布日期(可自选,§8.2);排序仍按 updated_at。
|
||||
type Note struct {
|
||||
ID int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Tags []string `json:"tags"`
|
||||
Pinned bool `json:"pinned"`
|
||||
DeletedAt *int64 `json:"deleted_at,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Tags []string `json:"tags"`
|
||||
Pinned bool `json:"pinned"`
|
||||
DeletedAt *int64 `json:"deleted_at,omitempty"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
PublishedAt int64 `json:"published_at"`
|
||||
}
|
||||
|
||||
// ErrNotFound 统一的「不存在」错误。
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
const noteColumns = "id, slug, title, summary, content, status, tags, pinned, deleted_at, created_at, updated_at"
|
||||
// published_at 经 v2 迁移回填,理论上恒非 NULL;COALESCE 兜底历史异常行。
|
||||
const noteColumns = "id, slug, title, summary, content, status, tags, pinned, deleted_at, created_at, updated_at, COALESCE(published_at, created_at) AS published_at"
|
||||
|
||||
func scanNote(scan func(dest ...any) error) (*Note, error) {
|
||||
var n Note
|
||||
var tagsJSON string
|
||||
var pinned int
|
||||
var content string
|
||||
if err := scan(&n.ID, &n.Slug, &n.Title, &n.Summary, &content, &n.Status, &tagsJSON, &pinned, &n.DeletedAt, &n.CreatedAt, &n.UpdatedAt); err != nil {
|
||||
if err := scan(&n.ID, &n.Slug, &n.Title, &n.Summary, &content, &n.Status, &tagsJSON, &pinned, &n.DeletedAt, &n.CreatedAt, &n.UpdatedAt, &n.PublishedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n.Content = content
|
||||
@@ -54,9 +57,9 @@ func (s *Store) CreateNote(n *Note) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
res, err := s.db.Exec(
|
||||
`INSERT INTO notes (slug, title, summary, content, status, tags, pinned, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
n.Slug, n.Title, n.Summary, n.Content, n.Status, tags, boolToInt(n.Pinned), n.CreatedAt, n.UpdatedAt)
|
||||
`INSERT INTO notes (slug, title, summary, content, status, tags, pinned, published_at, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
n.Slug, n.Title, n.Summary, n.Content, n.Status, tags, boolToInt(n.Pinned), n.PublishedAt, n.CreatedAt, n.UpdatedAt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -70,8 +73,8 @@ func (s *Store) UpdateNote(n *Note) error {
|
||||
return err
|
||||
}
|
||||
res, err := s.db.Exec(
|
||||
`UPDATE notes SET title=?, summary=?, content=?, status=?, tags=?, pinned=?, updated_at=? WHERE id=? AND deleted_at IS NULL`,
|
||||
n.Title, n.Summary, n.Content, n.Status, tags, boolToInt(n.Pinned), n.UpdatedAt, n.ID)
|
||||
`UPDATE notes SET title=?, summary=?, content=?, status=?, tags=?, pinned=?, published_at=?, updated_at=? WHERE id=? AND deleted_at IS NULL`,
|
||||
n.Title, n.Summary, n.Content, n.Status, tags, boolToInt(n.Pinned), n.PublishedAt, n.UpdatedAt, n.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,7 +167,7 @@ func (s *Store) ListPublicNotes(page, pageSize int, tag string) ([]Note, int, er
|
||||
if err := s.db.QueryRow(`SELECT COUNT(*) FROM notes n WHERE `+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
q := `SELECT n.id, n.slug, n.title, n.summary, '' AS content, n.status, n.tags, n.pinned, n.deleted_at, n.created_at, n.updated_at
|
||||
q := `SELECT n.id, n.slug, n.title, n.summary, '' AS content, n.status, n.tags, n.pinned, n.deleted_at, n.created_at, n.updated_at, COALESCE(n.published_at, n.created_at) AS published_at
|
||||
FROM notes n WHERE ` + where + `
|
||||
ORDER BY n.pinned DESC, n.updated_at DESC, n.id DESC LIMIT ? OFFSET ?`
|
||||
rows, err := s.db.Query(q, append(args, pageSize, (page-1)*pageSize)...)
|
||||
@@ -234,7 +237,7 @@ func (s *Store) ListPublicNotesFull(limit int) ([]Note, error) {
|
||||
|
||||
// ListAdminNotes 全部正常笔记(含私有,不含回收站),不含全文。
|
||||
func (s *Store) ListAdminNotes() ([]Note, error) {
|
||||
q := `SELECT id, slug, title, summary, '' AS content, status, tags, pinned, deleted_at, created_at, updated_at
|
||||
q := `SELECT id, slug, title, summary, '' AS content, status, tags, pinned, deleted_at, created_at, updated_at, COALESCE(published_at, created_at) AS published_at
|
||||
FROM notes WHERE deleted_at IS NULL
|
||||
ORDER BY pinned DESC, updated_at DESC, id DESC`
|
||||
return s.queryNotes(q)
|
||||
@@ -242,12 +245,21 @@ func (s *Store) ListAdminNotes() ([]Note, error) {
|
||||
|
||||
// ListTrash 回收站列表(软删除中),不含全文。
|
||||
func (s *Store) ListTrash() ([]Note, error) {
|
||||
q := `SELECT id, slug, title, summary, '' AS content, status, tags, pinned, deleted_at, created_at, updated_at
|
||||
q := `SELECT id, slug, title, summary, '' AS content, status, tags, pinned, deleted_at, created_at, updated_at, COALESCE(published_at, created_at) AS published_at
|
||||
FROM notes WHERE deleted_at IS NOT NULL
|
||||
ORDER BY deleted_at DESC`
|
||||
return s.queryNotes(q)
|
||||
}
|
||||
|
||||
// EmptyTrash 物理删除回收站全部笔记(refs 级联,需 foreign_keys=1),返回删除数。
|
||||
func (s *Store) EmptyTrash() (int64, error) {
|
||||
res, err := s.db.Exec(`DELETE FROM notes WHERE deleted_at IS NOT NULL`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// ListExpiredTrash 过期待物理删除的笔记 id(gc 用)。
|
||||
func (s *Store) ListExpiredTrash(before int64) ([]int64, error) {
|
||||
rows, err := s.db.Query(`SELECT id FROM notes WHERE deleted_at IS NOT NULL AND deleted_at < ?`, before)
|
||||
|
||||
Reference in New Issue
Block a user