feat: add navigation links management and user registration
Features: - Custom navigation links management in admin settings - Multi-language support for navigation links (Chinese/English) - Control link behavior (open in new window) - Sort order and enable/disable toggle for nav links - User registration system with validation - Admin can enable/disable user registration - Registration form with username, display name, email, password - Link to registration from login page Navigation Links: - Admin interface to add/edit/delete custom nav links - Support for both internal and external URLs - Display links in header navigation bar - Respects language preference User Registration: - Username validation (3-32 characters, alphanumeric + underscore/dash) - Password validation (minimum 6 characters) - Password confirmation matching - Optional display name and email fields - Can be toggled on/off by admin in site settings Templates: - Added navigation links settings page - Added user registration page - Updated login page with registration link - Updated base layout to render custom nav links Documentation: - NAV_LINKS_FEATURE.md - Navigation links feature documentation - REGISTRATION_FEATURE.md - User registration documentation - IMPLEMENTATION_SUMMARY.md - Overall implementation summary Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ var configCache = struct {
|
||||
comment *CommentConfig
|
||||
types []UploadFileType
|
||||
baseURLs []DownloadBaseURL
|
||||
navLinks []NavLink
|
||||
}{
|
||||
site: &SiteSetting{},
|
||||
upload: &UploadConfig{Enabled: true, DefaultMaxSize: DefaultUploadMaxSize, StorageDir: "attachments"},
|
||||
@@ -58,6 +59,11 @@ func LoadConfigCache(db *gorm.DB) {
|
||||
if err := db.Order("is_default desc, priority asc, id asc").Find(&b).Error; err == nil {
|
||||
configCache.baseURLs = b
|
||||
}
|
||||
|
||||
var n []NavLink
|
||||
if err := db.Where("enabled = ?", true).Order("sort asc, id asc").Find(&n).Error; err == nil {
|
||||
configCache.navLinks = n
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshConfigCache reloads all cached platform configuration. Admin handlers
|
||||
@@ -118,3 +124,10 @@ func DefaultDownloadBaseURL() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetNavLinks returns the cached list of enabled navigation links, sorted by sort order.
|
||||
func GetNavLinks() []NavLink {
|
||||
configCache.mu.RLock()
|
||||
defer configCache.mu.RUnlock()
|
||||
return configCache.navLinks
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// NavLink represents a custom navigation link in the header.
|
||||
type NavLink struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
TitleZh string `gorm:"size:100;not null" json:"title_zh"` // Link text (Chinese)
|
||||
TitleEn string `gorm:"size:100;not null" json:"title_en"` // Link text (English)
|
||||
URL string `gorm:"size:512;not null" json:"url"` // Target URL
|
||||
OpenNew bool `gorm:"default:false" json:"open_new"` // Open in new window
|
||||
Enabled bool `gorm:"default:true" json:"enabled"` // Show/hide link
|
||||
Sort int `gorm:"default:0;index" json:"sort"` // Display order (lower = first)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UpdatedBy uint `gorm:"index" json:"updated_by"`
|
||||
}
|
||||
|
||||
// TableName overrides the default GORM table name.
|
||||
func (NavLink) TableName() string {
|
||||
return "nav_links"
|
||||
}
|
||||
|
||||
// Title returns the link text for the given language code, falling back to
|
||||
// the other language when the requested one is empty.
|
||||
func (n *NavLink) Title(lang string) string {
|
||||
if lang == "zh" {
|
||||
if n.TitleZh != "" {
|
||||
return n.TitleZh
|
||||
}
|
||||
return n.TitleEn
|
||||
}
|
||||
if n.TitleEn != "" {
|
||||
return n.TitleEn
|
||||
}
|
||||
return n.TitleZh
|
||||
}
|
||||
+16
-15
@@ -6,21 +6,22 @@ import "time"
|
||||
// logo, top-left title, header banner text, and footer text, each with
|
||||
// zh/en variants that fall back to i18n defaults when empty.
|
||||
type SiteSetting struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Logo string `gorm:"size:512" json:"logo"` // local filename (served under /uploads/logos) OR a full URL
|
||||
Favicon string `gorm:"size:512" json:"favicon"` // local filename (served under /uploads/logos) OR a full URL
|
||||
LogoTextZh string `gorm:"size:255" json:"logo_text_zh"` // top-left title (zh)
|
||||
LogoTextEn string `gorm:"size:255" json:"logo_text_en"` // top-left title (en)
|
||||
HeaderTextZh string `gorm:"size:512" json:"header_text_zh"` // header banner text (zh)
|
||||
HeaderTextEn string `gorm:"size:512" json:"header_text_en"` // header banner text (en)
|
||||
HomeWelcomeZh string `gorm:"size:255" json:"home_welcome_zh"` // home page welcome heading (zh)
|
||||
HomeWelcomeEn string `gorm:"size:255" json:"home_welcome_en"` // home page welcome heading (en)
|
||||
HomeSubtitleZh string `gorm:"size:512" json:"home_subtitle_zh"` // home page subtitle (zh)
|
||||
HomeSubtitleEn string `gorm:"size:512" json:"home_subtitle_en"` // home page subtitle (en)
|
||||
FooterTextZh string `gorm:"size:512" json:"footer_text_zh"` // footer text (zh)
|
||||
FooterTextEn string `gorm:"size:512" json:"footer_text_en"` // footer text (en)
|
||||
UpdatedBy uint `gorm:"index" json:"updated_by"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Logo string `gorm:"size:512" json:"logo"` // local filename (served under /uploads/logos) OR a full URL
|
||||
Favicon string `gorm:"size:512" json:"favicon"` // local filename (served under /uploads/logos) OR a full URL
|
||||
LogoTextZh string `gorm:"size:255" json:"logo_text_zh"` // top-left title (zh)
|
||||
LogoTextEn string `gorm:"size:255" json:"logo_text_en"` // top-left title (en)
|
||||
HeaderTextZh string `gorm:"size:512" json:"header_text_zh"` // header banner text (zh)
|
||||
HeaderTextEn string `gorm:"size:512" json:"header_text_en"` // header banner text (en)
|
||||
HomeWelcomeZh string `gorm:"size:255" json:"home_welcome_zh"` // home page welcome heading (zh)
|
||||
HomeWelcomeEn string `gorm:"size:255" json:"home_welcome_en"` // home page welcome heading (en)
|
||||
HomeSubtitleZh string `gorm:"size:512" json:"home_subtitle_zh"` // home page subtitle (zh)
|
||||
HomeSubtitleEn string `gorm:"size:512" json:"home_subtitle_en"` // home page subtitle (en)
|
||||
FooterTextZh string `gorm:"size:512" json:"footer_text_zh"` // footer text (zh)
|
||||
FooterTextEn string `gorm:"size:512" json:"footer_text_en"` // footer text (en)
|
||||
AllowRegistration bool `gorm:"default:false" json:"allow_registration"` // whether users can self-register
|
||||
UpdatedBy uint `gorm:"index" json:"updated_by"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName overrides the default GORM table name.
|
||||
|
||||
Reference in New Issue
Block a user