Files
web_huangguo_web/app/service/translationser/view.go
T
2026-10-28 16:20:38 +07:00

288 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 == "/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},
}
}
// 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)
// 福利应用页已下线,后台残留的导航项不再展示
if !external && normalizeNavPath(href) == "/application" {
continue
}
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 "/tag":
return "tagList"
case "/topic":
return "topic"
case "/series":
return "aiSeries"
case "/home-road":
return "homeRoad"
case "/download-app":
return "downloadApp"
default:
return path
}
}