feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
@@ -66,7 +66,7 @@ const (
|
||||
SettingKeyNecroReplyPenalty = "necro_reply_penalty" // 旧帖回复扣除积分;0=仅提醒不扣分
|
||||
|
||||
// 积分增长规则(正整数奖励;0=关闭/不限,缺行=默认值)
|
||||
SettingKeyPointsCheckinBase = "points_checkin_base" // 签到基础分;缺行=5
|
||||
SettingKeyPointsCheckinBase = "points_checkin_base" // 签到基础分;缺行=5
|
||||
SettingKeyPointsStreakEveryDays = "points_streak_every_days" // 每满 N 天连续签到触发加成;0=关闭;缺行=7
|
||||
SettingKeyPointsStreakBonus = "points_streak_bonus" // 触发加成时额外加分;缺行=0
|
||||
SettingKeyPointsPostReward = "points_post_reward" // 发帖奖励;0=关闭;缺行=0
|
||||
@@ -83,6 +83,10 @@ const (
|
||||
|
||||
// SettingKeyUrlStyle 详情页伪静态风格;缺行视为 default(标准动态路由风格)
|
||||
SettingKeyUrlStyle = "url_style"
|
||||
|
||||
// SettingKeyMobileTabs 移动端底栏显示的 tab(JSON []string,元素取值见 MobileTabKeys);
|
||||
// 缺行/非法/为空视为全量默认
|
||||
SettingKeyMobileTabs = "mobile_tabs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -118,8 +122,19 @@ const (
|
||||
|
||||
// DefaultUrlStyle 标准动态路由风格(/post/123)
|
||||
DefaultUrlStyle = "default"
|
||||
|
||||
// MaxMobileTabs 底栏配置元素上限(与白名单等长)
|
||||
MaxMobileTabs = 5
|
||||
)
|
||||
|
||||
// MobileTabKeys 移动端底栏允许的 tab key 白名单(同时是默认顺序,与前端 lib/mobileTabs.ts 保持一致)
|
||||
var MobileTabKeys = []string{"home", "library", "compose", "messages", "me"}
|
||||
|
||||
// DefaultMobileTabs 底栏默认全量配置
|
||||
func DefaultMobileTabs() []string {
|
||||
return append([]string(nil), MobileTabKeys...)
|
||||
}
|
||||
|
||||
// DefaultAttachmentExts 论坛向默认允许扩展名(含安装包/脚本/网页)
|
||||
var DefaultAttachmentExts = []string{
|
||||
"pdf", "txt", "md", "csv", "json",
|
||||
@@ -146,34 +161,34 @@ var accentHexRe = regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
|
||||
|
||||
// PublicSiteSettings 对前台公开的站点配置(SEO / 注册入口 / 发帖冷静期提示)
|
||||
type PublicSiteSettings struct {
|
||||
Accent string `json:"accent"`
|
||||
TrustReviewedPublish bool `json:"trust_reviewed_publish"`
|
||||
PublishWithoutReview bool `json:"publish_without_review"`
|
||||
SiteName string `json:"site_name"`
|
||||
SiteDescription string `json:"site_description"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
AllowComments bool `json:"allow_comments"`
|
||||
CommentsRequireLogin bool `json:"comments_require_login"`
|
||||
AllowMessages bool `json:"allow_messages"`
|
||||
PostCooldownHours int `json:"post_cooldown_hours"`
|
||||
CodeBlockAutoFold bool `json:"code_block_auto_fold"`
|
||||
CodeBlockFoldLines int `json:"code_block_fold_lines"`
|
||||
UIAnimations bool `json:"ui_animations"`
|
||||
AnimCodeFold bool `json:"anim_code_fold"`
|
||||
AnimSmoothScroll bool `json:"anim_smooth_scroll"`
|
||||
AnimChrome bool `json:"anim_chrome"`
|
||||
PostLinkNewTab bool `json:"post_link_new_tab"`
|
||||
AttachmentExtLimit bool `json:"attachment_ext_limit"`
|
||||
AttachmentExts []string `json:"attachment_exts"`
|
||||
AttachmentMaxMB int `json:"attachment_max_mb"`
|
||||
AttachmentMaxCount int `json:"attachment_max_count"`
|
||||
ImageMaxMB int `json:"image_max_mb"`
|
||||
Accent string `json:"accent"`
|
||||
TrustReviewedPublish bool `json:"trust_reviewed_publish"`
|
||||
PublishWithoutReview bool `json:"publish_without_review"`
|
||||
SiteName string `json:"site_name"`
|
||||
SiteDescription string `json:"site_description"`
|
||||
AllowRegister bool `json:"allow_register"`
|
||||
AllowComments bool `json:"allow_comments"`
|
||||
CommentsRequireLogin bool `json:"comments_require_login"`
|
||||
AllowMessages bool `json:"allow_messages"`
|
||||
PostCooldownHours int `json:"post_cooldown_hours"`
|
||||
CodeBlockAutoFold bool `json:"code_block_auto_fold"`
|
||||
CodeBlockFoldLines int `json:"code_block_fold_lines"`
|
||||
UIAnimations bool `json:"ui_animations"`
|
||||
AnimCodeFold bool `json:"anim_code_fold"`
|
||||
AnimSmoothScroll bool `json:"anim_smooth_scroll"`
|
||||
AnimChrome bool `json:"anim_chrome"`
|
||||
PostLinkNewTab bool `json:"post_link_new_tab"`
|
||||
AttachmentExtLimit bool `json:"attachment_ext_limit"`
|
||||
AttachmentExts []string `json:"attachment_exts"`
|
||||
AttachmentMaxMB int `json:"attachment_max_mb"`
|
||||
AttachmentMaxCount int `json:"attachment_max_count"`
|
||||
ImageMaxMB int `json:"image_max_mb"`
|
||||
|
||||
// 内容锁定与旧帖回复(0=关闭;公开保持规则透明)
|
||||
PostEditLockHours int `json:"post_edit_lock_hours"`
|
||||
CommentEditLockHours int `json:"comment_edit_lock_hours"`
|
||||
NecroReplyAfterHours int `json:"necro_reply_after_hours"`
|
||||
NecroReplyPenalty int `json:"necro_reply_penalty"`
|
||||
PostEditLockHours int `json:"post_edit_lock_hours"`
|
||||
CommentEditLockHours int `json:"comment_edit_lock_hours"`
|
||||
NecroReplyAfterHours int `json:"necro_reply_after_hours"`
|
||||
NecroReplyPenalty int `json:"necro_reply_penalty"`
|
||||
BgSiteURL string `json:"bg_site_url"`
|
||||
BgSiteMode string `json:"bg_site_mode"`
|
||||
BgAdminURL string `json:"bg_admin_url"`
|
||||
@@ -206,6 +221,12 @@ type PublicSiteSettings struct {
|
||||
|
||||
// UrlStyle 详情页伪静态风格:default(/post/123)、html(/post-123.html)、discuz(/thread-123.html)
|
||||
UrlStyle string `json:"url_style"`
|
||||
|
||||
// MobileTabs 移动端底栏显示的 tab(有序子集,数组顺序即展示顺序);空/非法由读取端回落默认
|
||||
MobileTabs []string `json:"mobile_tabs"`
|
||||
|
||||
// HeaderNav 桌面端 Header 主导航(数组顺序即展示顺序);空数组=全部隐藏;缺省=内置三项
|
||||
HeaderNav []HeaderNavItem `json:"header_nav"`
|
||||
}
|
||||
|
||||
// SettingService 站点级键值设置
|
||||
@@ -310,6 +331,7 @@ func (s *SettingService) Public() (PublicSiteSettings, error) {
|
||||
BrandLogoFit: BrandLogoFitContain,
|
||||
FooterLinks: []FooterLink{},
|
||||
UrlStyle: DefaultUrlStyle,
|
||||
HeaderNav: DefaultHeaderNav(),
|
||||
}
|
||||
accent, err := s.AccentColor()
|
||||
if err != nil {
|
||||
@@ -513,6 +535,9 @@ func (s *SettingService) Public() (PublicSiteSettings, error) {
|
||||
}
|
||||
out.UrlStyle = urlStyle
|
||||
|
||||
out.MobileTabs = s.MobileTabs()
|
||||
out.HeaderNav = s.HeaderNav()
|
||||
|
||||
if err := s.fillBrand(&out); err != nil {
|
||||
return out, err
|
||||
}
|
||||
@@ -551,6 +576,84 @@ func (s *SettingService) SetUrlStyle(style string) error {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
|
||||
// normalizeMobileTabs 校验底栏 tab 列表:去空白、去重、只留白名单内元素
|
||||
func normalizeMobileTabs(tabs []string) ([]string, bool) {
|
||||
allowed := make(map[string]struct{}, len(MobileTabKeys))
|
||||
for _, k := range MobileTabKeys {
|
||||
allowed[k] = struct{}{}
|
||||
}
|
||||
seen := make(map[string]struct{}, len(tabs))
|
||||
out := make([]string, 0, len(tabs))
|
||||
for _, t := range tabs {
|
||||
k := strings.TrimSpace(strings.ToLower(t))
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := allowed[k]; !ok {
|
||||
return nil, false
|
||||
}
|
||||
if _, dup := seen[k]; dup {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
out = append(out, k)
|
||||
if len(out) > MaxMobileTabs {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
// mobileTabsEqual 判断两个列表是否元素一致(忽略顺序)
|
||||
func mobileTabsEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
set := make(map[string]struct{}, len(b))
|
||||
for _, k := range b {
|
||||
set[k] = struct{}{}
|
||||
}
|
||||
for _, k := range a {
|
||||
if _, ok := set[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MobileTabs 移动端底栏配置;缺行/为空/非法/解析失败回落默认全量
|
||||
func (s *SettingService) MobileTabs() []string {
|
||||
v, found, err := s.getValue(SettingKeyMobileTabs)
|
||||
if err != nil || !found || strings.TrimSpace(v) == "" {
|
||||
return DefaultMobileTabs()
|
||||
}
|
||||
var tabs []string
|
||||
if err := json.Unmarshal([]byte(v), &tabs); err != nil {
|
||||
return DefaultMobileTabs()
|
||||
}
|
||||
out, ok := normalizeMobileTabs(tabs)
|
||||
if !ok || len(out) == 0 {
|
||||
return DefaultMobileTabs()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SetMobileTabs 校验并保存底栏配置;与默认全量一致时删键保持「缺行=默认」语义
|
||||
func (s *SettingService) SetMobileTabs(tabs []string) error {
|
||||
out, ok := normalizeMobileTabs(tabs)
|
||||
if !ok || len(out) == 0 {
|
||||
return ErrInvalidSiteSetting
|
||||
}
|
||||
if mobileTabsEqual(out, MobileTabKeys) {
|
||||
return s.deleteKey(SettingKeyMobileTabs)
|
||||
}
|
||||
b, err := json.Marshal(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.putValue(SettingKeyMobileTabs, string(b))
|
||||
}
|
||||
|
||||
// AccentColor 返回配置的主题色(小写 #rrggbb);未配置时返回空串,表示走前端默认色
|
||||
func (s *SettingService) AccentColor() (string, error) {
|
||||
v, found, err := s.getValue(SettingKeyAccent)
|
||||
|
||||
Reference in New Issue
Block a user