feat: 增强编辑器链接对话框并统一图片插入选择器

链接支持网址/文字与站内搜索,新标签由全站设置控制;文章与评论共用已上传图片选择器。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 21:22:03 +08:00
parent 2159280af5
commit d5aa36416c
10 changed files with 1195 additions and 128 deletions

View File

@@ -154,6 +154,122 @@ func (s *UploadStore) ListMedia(category, query string, page, size int) (*MediaL
}, nil
}
// ListUserPostImages 列出当前用户历史上传的帖子图片category=posts
func (s *UploadStore) ListUserPostImages(userID uint, page, size int) (*MediaListResult, error) {
if s == nil {
return nil, errors.New("上传存储未初始化")
}
if model.DB == nil {
return nil, errors.New("数据库未初始化")
}
if userID == 0 {
return nil, errors.New("未登录")
}
if page < 1 {
page = 1
}
if size < 1 {
size = 24
}
if size > 100 {
size = 100
}
var records []model.Media
if err := model.DB.Where("category = ? AND user_id = ?", UploadCategoryPosts, userID).
Order("created_at desc, id desc").
Find(&records).Error; err != nil {
return nil, err
}
// 上传会登记原图 + WebP图库按 stem 去重,优先展示 WebP与插入 URL 一致)
records = dedupePostMediaPreferWebP(records)
total := len(records)
totalPages := 1
if total > 0 {
totalPages = (total + size - 1) / size
}
if page > totalPages {
page = totalPages
}
start := (page - 1) * size
if start > total {
start = total
}
end := start + size
if end > total {
end = total
}
pageRecords := records[start:end]
files := make([]MediaItem, 0, len(pageRecords))
for _, r := range pageRecords {
mod := r.UpdatedAt
if mod.IsZero() {
mod = r.CreatedAt
}
files = append(files, MediaItem{
Category: r.Category,
Name: r.Name,
URL: r.URL,
Size: r.Size,
ModifiedAt: mod.UTC(),
ContentType: r.ContentType,
StorageType: r.StorageType,
})
}
mode, _, _, _ := s.snapshot()
storageType := config.StorageTypeLocal
if mode == config.StorageTypeS3 {
storageType = config.StorageTypeS3
}
return &MediaListResult{
Files: files,
Total: total,
Page: page,
TotalPages: totalPages,
StorageType: storageType,
CategoryCounts: map[string]int{UploadCategoryPosts: total},
}, nil
}
// dedupePostMediaPreferWebP 同一上传的原图/WebP 只保留一条,优先 WebP
func dedupePostMediaPreferWebP(records []model.Media) []model.Media {
type slot struct {
idx int
isWebP bool
}
seen := map[string]slot{}
out := make([]model.Media, 0, len(records))
for _, r := range records {
stem := mediaFileStem(r.Name)
isWebP := strings.EqualFold(filepath.Ext(r.Name), ".webp") ||
strings.EqualFold(r.ContentType, "image/webp")
if s, ok := seen[stem]; ok {
if isWebP && !s.isWebP {
out[s.idx] = r
seen[stem] = slot{idx: s.idx, isWebP: true}
}
continue
}
seen[stem] = slot{idx: len(out), isWebP: isWebP}
out = append(out, r)
}
return out
}
func mediaFileStem(name string) string {
name = filepath.Base(strings.TrimSpace(name))
ext := filepath.Ext(name)
if ext == "" {
return strings.ToLower(name)
}
return strings.ToLower(strings.TrimSuffix(name, ext))
}
// DeleteMedia 按 URL 批量删除媒体(含伴生扩展名与数据库索引)
func (s *UploadStore) DeleteMedia(urls []string) (int, error) {
if s == nil {