289 lines
8.1 KiB
Go
289 lines
8.1 KiB
Go
package translationser
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// HeaderNavItemVM 顶部 / 抽屉导航项。
|
|
type HeaderNavItemVM struct {
|
|
ID int64
|
|
Name string
|
|
Href string
|
|
Icon string
|
|
Key string // 与页面 navActive 对齐
|
|
IsActive bool
|
|
SeoInfo SeoValue
|
|
IsDownload bool
|
|
IsExternal bool
|
|
Children []HeaderNavChildVM
|
|
}
|
|
|
|
// ResolveNavigationActiveKey 将当前路由归一到 PC 顶部导航和移动侧栏共用的栏目键。
|
|
func ResolveNavigationActiveKey(activeKey, requestPath string) string {
|
|
path := normalizeNavPath(requestPath)
|
|
switch {
|
|
case path == "/":
|
|
return "home"
|
|
case path == "/list/ai-short-play" || strings.HasPrefix(path, "/list/ai-short-play/"):
|
|
return "aiShortPlay"
|
|
case path == "/series" || strings.HasPrefix(path, "/series/") || path == "/list/ai-series" || strings.HasPrefix(path, "/list/ai-series/"):
|
|
return "aiSeries"
|
|
case path == "/tag" || strings.HasPrefix(path, "/tag/") || path == "/tag-list" || strings.HasPrefix(path, "/tag-list/"):
|
|
return "tagList"
|
|
case path == "/topic" || strings.HasPrefix(path, "/topic/"):
|
|
return "topic"
|
|
case path == "/rank" || strings.HasPrefix(path, "/rank/"):
|
|
return "rank"
|
|
case path == "/today-update" || strings.HasPrefix(path, "/today-update/"):
|
|
return "todayUpdate"
|
|
case strings.HasPrefix(path, "/category/"):
|
|
return "category"
|
|
case path == "/application" || strings.HasPrefix(path, "/application/"):
|
|
return "application"
|
|
case path == "/home-road" || strings.HasPrefix(path, "/home-road/"):
|
|
return "homeRoad"
|
|
case path == "/list" || strings.HasPrefix(path, "/detail/") || strings.HasPrefix(path, "/play/") || path == "/more" || strings.HasPrefix(path, "/more/") || path == "/search" || strings.HasPrefix(path, "/search/"):
|
|
return "list"
|
|
}
|
|
if key := strings.TrimSpace(activeKey); key != "" {
|
|
return key
|
|
}
|
|
return path
|
|
}
|
|
|
|
// MarkHeaderNavActive 返回带当前页选中态的导航副本。
|
|
// PC 顶部导航仅在当前路径与导航路径完全一致时高亮,避免搜索、详情和分页页误亮栏目。
|
|
func MarkHeaderNavActive(items []HeaderNavItemVM, requestPath string) []HeaderNavItemVM {
|
|
currentPath := normalizeNavPath(requestPath)
|
|
out := append([]HeaderNavItemVM(nil), items...)
|
|
for i := range out {
|
|
out[i].IsActive = headerNavItemMatchesPath(out[i], currentPath)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func headerNavItemMatchesPath(item HeaderNavItemVM, currentPath string) bool {
|
|
if item.IsExternal || item.IsDownload {
|
|
return false
|
|
}
|
|
href := strings.TrimSpace(item.Href)
|
|
if href == "" || href == "#" || strings.HasPrefix(strings.ToLower(href), "javascript:") {
|
|
return false
|
|
}
|
|
return normalizeNavPath(href) == currentPath
|
|
}
|
|
|
|
// HeaderNavChildVM 二级导航。
|
|
type HeaderNavChildVM struct {
|
|
Name string
|
|
Href string
|
|
IsExternal bool
|
|
IsFriendAds bool
|
|
}
|
|
|
|
// DefaultHeaderNav 接口失败时的兜底导航。
|
|
func DefaultHeaderNav() []HeaderNavItemVM {
|
|
return []HeaderNavItemVM{
|
|
{Name: "首页", Href: "/", Key: "home"},
|
|
{Name: "短剧", Href: "/list/", Key: "list"},
|
|
{Name: "排行榜", Href: "/rank/", Key: "rank"},
|
|
{Name: "回家的路", Href: "/home-road/", Key: "homeRoad"},
|
|
{Name: "今日更新", Href: "/today-update/", Key: "todayUpdate"},
|
|
{Name: "下载APP", Href: "javascript:;", Key: "downloadApp", IsDownload: true},
|
|
{Name: "福利应用", Href: "/application/", Key: "application"},
|
|
}
|
|
}
|
|
|
|
// BuildHeaderNav 将上游分类转为模板可用导航(顶部 URL / 二级;底部类型留给抽屉扩展)。
|
|
func BuildHeaderNav(info *AppTranslationInfo) []HeaderNavItemVM {
|
|
if info == nil || len(info.CategoryList) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]HeaderNavItemVM, 0, len(info.CategoryList))
|
|
for _, cat := range info.CategoryList {
|
|
name := strings.TrimSpace(cat.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
// 底部导航不进顶部栏(仍可由抽屉单独渲染时可再扩展)
|
|
if cat.CategoryType == TranslationTypeBottom {
|
|
continue
|
|
}
|
|
item := HeaderNavItemVM{
|
|
ID: cat.ID,
|
|
Name: name,
|
|
Icon: strings.TrimSpace(cat.Icon),
|
|
SeoInfo: normalizeNavigationSEO(cat.SeoInfo),
|
|
}
|
|
href, external, download := normalizeNavURL(cat.URLValue, name)
|
|
item.Href = href
|
|
item.IsExternal = external
|
|
item.IsDownload = download
|
|
item.Key = navActiveKey(href, name, download)
|
|
|
|
if cat.CategoryType == TranslationTypeSub || len(cat.TranslationList) > 0 {
|
|
for _, child := range cat.TranslationList {
|
|
cname := strings.TrimSpace(child.Name)
|
|
if cname == "" {
|
|
continue
|
|
}
|
|
ch, cex, _ := normalizeNavURL(child.URLValue, cname)
|
|
if ch == "" {
|
|
continue
|
|
}
|
|
item.Children = append(item.Children, HeaderNavChildVM{
|
|
Name: cname,
|
|
Href: ch,
|
|
IsExternal: cex,
|
|
IsFriendAds: child.IsFriendAds,
|
|
})
|
|
}
|
|
}
|
|
if item.Href == "" && len(item.Children) == 0 && !item.IsDownload {
|
|
continue
|
|
}
|
|
if item.Href == "" && !item.IsDownload {
|
|
item.Href = "javascript:;"
|
|
}
|
|
out = append(out, item)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FindHeaderNavSEO 优先匹配当前请求的最长导航路径,navActive 用于无实际 URL 的导航兜底。
|
|
func FindHeaderNavSEO(items []HeaderNavItemVM, activeKey, requestPath string) (SeoValue, bool) {
|
|
requestPath = normalizeNavPath(requestPath)
|
|
bestLen := -1
|
|
var best SeoValue
|
|
for _, item := range items {
|
|
if item.IsExternal || item.IsDownload || !hasNavigationSEO(item.SeoInfo) {
|
|
continue
|
|
}
|
|
target := normalizeNavPath(item.Href)
|
|
matched := requestPath == target
|
|
if target != "/" && strings.HasPrefix(requestPath, target+"/") {
|
|
matched = true
|
|
}
|
|
if matched && len(target) > bestLen {
|
|
bestLen = len(target)
|
|
best = item.SeoInfo
|
|
}
|
|
}
|
|
if bestLen >= 0 {
|
|
return best, true
|
|
}
|
|
|
|
activeKey = strings.TrimSpace(activeKey)
|
|
if activeKey != "" {
|
|
for _, item := range items {
|
|
if item.Key == activeKey && hasNavigationSEO(item.SeoInfo) {
|
|
return item.SeoInfo, true
|
|
}
|
|
}
|
|
}
|
|
return SeoValue{}, false
|
|
}
|
|
|
|
func normalizeNavigationSEO(seo SeoValue) SeoValue {
|
|
seo.Title = strings.TrimSpace(seo.Title)
|
|
seo.Description = strings.TrimSpace(seo.Description)
|
|
keywords := make([]string, 0, len(seo.Keyworld))
|
|
seen := make(map[string]struct{}, len(seo.Keyworld))
|
|
for _, keyword := range seo.Keyworld {
|
|
keyword = strings.TrimSpace(keyword)
|
|
if keyword == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[keyword]; ok {
|
|
continue
|
|
}
|
|
seen[keyword] = struct{}{}
|
|
keywords = append(keywords, keyword)
|
|
}
|
|
seo.Keyworld = keywords
|
|
return seo
|
|
}
|
|
|
|
func hasNavigationSEO(seo SeoValue) bool {
|
|
return seo.Title != "" || seo.Description != "" || len(seo.Keyworld) > 0
|
|
}
|
|
|
|
func normalizeNavPath(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if parsed, err := url.Parse(raw); err == nil && parsed.Path != "" {
|
|
raw = parsed.Path
|
|
}
|
|
if raw == "" {
|
|
return "/"
|
|
}
|
|
if !strings.HasPrefix(raw, "/") {
|
|
raw = "/" + raw
|
|
}
|
|
raw = strings.TrimRight(raw, "/")
|
|
if raw == "" {
|
|
return "/"
|
|
}
|
|
return raw
|
|
}
|
|
|
|
func normalizeNavURL(raw, _ string) (href string, external, download bool) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "", false, false
|
|
}
|
|
lower := strings.ToLower(raw)
|
|
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") || strings.HasPrefix(raw, "//") {
|
|
return raw, true, false
|
|
}
|
|
if raw == "#" || strings.HasPrefix(lower, "javascript:") || strings.HasPrefix(lower, "mailto:") || strings.HasPrefix(lower, "tel:") {
|
|
return raw, false, false
|
|
}
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || parsed.Path == "" {
|
|
return raw, false, false
|
|
}
|
|
if parsed.Path != "/" && !strings.HasSuffix(parsed.Path, "/") {
|
|
parsed.Path += "/"
|
|
}
|
|
return parsed.String(), false, false
|
|
}
|
|
|
|
func navActiveKey(href, name string, download bool) string {
|
|
if download {
|
|
return "downloadApp"
|
|
}
|
|
path := href
|
|
if i := strings.Index(path, "?"); i >= 0 {
|
|
path = path[:i]
|
|
}
|
|
path = strings.TrimRight(path, "/")
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
switch path {
|
|
case "/":
|
|
return "home"
|
|
case "/list":
|
|
return "list"
|
|
case "/rank":
|
|
return "rank"
|
|
case "/today-update":
|
|
return "todayUpdate"
|
|
case "/application":
|
|
return "application"
|
|
case "/tag":
|
|
return "tagList"
|
|
case "/topic":
|
|
return "topic"
|
|
case "/series":
|
|
return "aiSeries"
|
|
case "/home-road":
|
|
return "homeRoad"
|
|
case "/download-app":
|
|
return "downloadApp"
|
|
default:
|
|
return path
|
|
}
|
|
}
|