feat: ToNav-go v1.0.0 - 内部服务导航系统

功能:
- 前台导航: 分类Tab切换、实时搜索、健康状态指示、响应式适配
- 后台管理: 服务/分类CRUD、系统设置、登录认证(bcrypt)
- 健康检查: 定时检测(5min)、独立检查URL、三态指示(在线/离线/未检测)
- 云端备份: WebDAV上传/下载/恢复/删除、定时自动备份、本地备份管理

技术栈: Go + Gin + GORM + SQLite
This commit is contained in:
2026-02-14 05:09:23 +08:00
commit efaf787981
23 changed files with 2735 additions and 0 deletions

119
handlers/views.go Normal file
View File

@@ -0,0 +1,119 @@
package handlers
import (
"encoding/json"
"fmt"
"html/template"
"net/http"
"tonav-go/database"
"tonav-go/models"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
// AuthRequired 登录验证中间件
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
userID := session.Get("user_id")
if userID == nil {
c.Redirect(http.StatusFound, "/admin/login")
c.Abort()
return
}
mustChange := session.Get("must_change")
if mustChange == true && c.Request.URL.Path != "/admin/change-password" && c.Request.URL.Path != "/admin/logout" {
c.Redirect(http.StatusFound, "/admin/change-password")
c.Abort()
return
}
c.Next()
}
}
// DashboardHandler 渲染后台首页
func DashboardHandler(c *gin.Context) {
var serviceCount, categoryCount int64
var onlineCount, offlineCount int64
database.DB.Model(&models.Service{}).Count(&serviceCount)
database.DB.Model(&models.Category{}).Count(&categoryCount)
database.DB.Model(&models.Service{}).Where("status = ?", "online").Count(&onlineCount)
database.DB.Model(&models.Service{}).Where("status = ?", "offline").Count(&offlineCount)
c.HTML(http.StatusOK, "dashboard.html", gin.H{
"service_count": serviceCount,
"category_count": categoryCount,
"online_count": onlineCount,
"offline_count": offlineCount,
})
}
// IndexHandler 渲染前台首页
func IndexHandler(c *gin.Context) {
if database.DB == nil {
c.String(http.StatusInternalServerError, "DB NIL")
return
}
// 获取所有分类
var categories []models.Category
database.DB.Order("sort_order desc").Find(&categories)
// 获取所有启用的服务
var services []models.Service
database.DB.Where("is_enabled = ?", true).Order("category_id asc, sort_order desc").Find(&services)
// 获取站点标题设置
var titleSetting models.Setting
siteTitle := "ToNav"
if err := database.DB.Where("key = ?", "site_title").First(&titleSetting).Error; err == nil && titleSetting.Value != "" {
siteTitle = titleSetting.Value
}
// 序列化为 JSON 供前端 JS 使用
categoriesJSON, _ := json.Marshal(categories)
servicesJSON, _ := json.Marshal(services)
c.HTML(http.StatusOK, "index.html", gin.H{
"site_title": siteTitle,
"categories": categories,
"categories_json": template.JS(categoriesJSON),
"services_json": template.JS(servicesJSON),
})
}
// ServicesPageHandler 渲染服务管理页面
func ServicesPageHandler(c *gin.Context) {
var categories []models.Category
database.DB.Order("sort_order desc").Find(&categories)
c.HTML(http.StatusOK, "services.html", gin.H{
"categories": categories,
})
}
// CategoriesPageHandler 渲染分类管理页面
func CategoriesPageHandler(c *gin.Context) {
c.HTML(http.StatusOK, "categories.html", nil)
}
// getSessionUserID 安全获取 session 中的 user_id
func getSessionUserID(session sessions.Session) (uint, error) {
userID := session.Get("user_id")
if userID == nil {
return 0, fmt.Errorf("user not logged in")
}
switch v := userID.(type) {
case uint:
return v, nil
case int:
return uint(v), nil
case int64:
return uint(v), nil
case float64:
return uint(v), nil
default:
return 0, fmt.Errorf("unexpected user_id type: %T", userID)
}
}