diff --git a/app/api/web/account_pagination.go b/app/api/web/account_pagination.go deleted file mode 100644 index 8bbdbf8..0000000 --- a/app/api/web/account_pagination.go +++ /dev/null @@ -1,52 +0,0 @@ -package web - -import ( - "strconv" - "strings" - - "github.com/gin-gonic/gin" -) - -const maxAccountPage = 10000 - -type accountPage struct { - Current int - Total int - Count int64 - PrevURL string - NextURL string -} - -func accountPageNum(c *gin.Context) (int, bool) { - if c.Request.URL.Query().Has("page") { - return 0, false - } - raw := strings.TrimSpace(c.Param("page")) - if raw == "" { - return 1, true - } - page, err := strconv.Atoi(raw) - if err != nil || page < 1 || page > maxAccountPage { - return 0, false - } - return page, true -} - -func makeAccountPage(path string, page, size int, count int64) accountPage { - total := int((count + int64(size) - 1) / int64(size)) - p := accountPage{Current: page, Total: total, Count: count} - link := func(n int) string { - path = strings.TrimRight(path, "/") - if n <= 1 { - return path - } - return path + "/page/" + strconv.Itoa(n) - } - if page > 1 { - p.PrevURL = link(page - 1) - } - if page < total { - p.NextURL = link(page + 1) - } - return p -} diff --git a/app/api/web/auth.go b/app/api/web/auth.go deleted file mode 100644 index 8697013..0000000 --- a/app/api/web/auth.go +++ /dev/null @@ -1,290 +0,0 @@ -package web - -import ( - "net/http" - "strings" - "videoweb/app/service/authser" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -type registerBody struct { - AccountNo string `json:"accountNo"` - Email string `json:"email"` - Password string `json:"password"` - AgentInviteCode string `json:"agentInviteCode"` - DevID string `json:"devID"` - Channel string `json:"channel"` -} - -type loginBody struct { - Account string `json:"account"` - Password string `json:"password"` - DevID string `json:"devID"` - Channel string `json:"channel"` -} - -// Register 代理上游注册(POST /api/app/auth/register),成功后写入登录 cookie。 -// 请求头带游客 Authorization 时升级该游客;邮箱缺省时用账号生成占位邮箱。 -func Register(c *gin.Context) { - var body registerBody - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 1001, - "msg": "参数错误", - "tip": "参数错误", - "data": nil, - }) - return - } - - body.AccountNo = strings.TrimSpace(body.AccountNo) - body.Email = strings.TrimSpace(body.Email) - body.AgentInviteCode = strings.TrimSpace(body.AgentInviteCode) - body.DevID = strings.TrimSpace(body.DevID) - body.Channel = authser.ResolveChannel(c, body.Channel) - - if body.AccountNo == "" || body.Password == "" { - c.JSON(http.StatusOK, gin.H{ - "code": 1001, - "msg": "参数错误", - "tip": "请填写账号和密码", - "data": nil, - }) - return - } - - // 上游 registerReq 要求 email;前端无邮箱时用账号生成唯一占位邮箱。 - if body.Email == "" { - body.Email = body.AccountNo + "@hgdj.local" - } - - env, user, err := authser.Register(c, authser.RegisterReq{ - AccountNo: body.AccountNo, - Email: body.Email, - Password: body.Password, - AgentInviteCode: body.AgentInviteCode, - DevID: body.DevID, - Channel: body.Channel, - }) - if err != nil { - log.Error("register upstream failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "register failed", - "tip": "注册失败,请稍后重试", - "data": nil, - }) - return - } - - if env == nil || env.Code != 200 || user == nil { - tip := "注册失败" - msg := "" - code := 500 - if env != nil { - code = env.Code - msg = env.Msg - if t := strings.TrimSpace(env.Tip); t != "" { - tip = t - } else if m := strings.TrimSpace(env.Msg); m != "" { - tip = m - } - } - c.JSON(http.StatusOK, gin.H{ - "code": code, - "msg": msg, - "tip": tip, - "data": nil, - }) - return - } - - authser.SetAuthCookies(c, user) - c.JSON(http.StatusOK, gin.H{ - "code": 200, - "msg": firstNonEmptyAuth(env.Msg, "success"), - "tip": firstNonEmptyAuth(env.Tip, "注册成功"), - "data": user, - }) -} - -// Login 代理上游登录(POST /api/app/auth/login),成功后写入登录 cookie。 -func Login(c *gin.Context) { - var body loginBody - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 1001, - "msg": "参数错误", - "tip": "参数错误", - "data": nil, - }) - return - } - - body.Account = strings.TrimSpace(body.Account) - body.DevID = strings.TrimSpace(body.DevID) - body.Channel = authser.ResolveChannel(c, body.Channel) - if body.Account == "" || body.Password == "" { - c.JSON(http.StatusOK, gin.H{ - "code": 1001, - "msg": "参数错误", - "tip": "请填写账号和密码", - "data": nil, - }) - return - } - - env, user, err := authser.Login(c, authser.LoginReq{ - Account: body.Account, - Password: body.Password, - DevID: body.DevID, - Channel: body.Channel, - }) - if err != nil { - log.Error("login upstream failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "login failed", - "tip": "登录失败,请稍后重试", - "data": nil, - }) - return - } - - if env == nil || env.Code != 200 || user == nil { - tip := "登录失败" - msg := "" - code := 500 - if env != nil { - code = env.Code - msg = env.Msg - if t := strings.TrimSpace(env.Tip); t != "" { - tip = t - } else if m := strings.TrimSpace(env.Msg); m != "" { - tip = m - } - } - c.JSON(http.StatusOK, gin.H{ - "code": code, - "msg": msg, - "tip": tip, - "data": nil, - }) - return - } - - authser.SetAuthCookies(c, user) - c.JSON(http.StatusOK, gin.H{ - "code": 200, - "msg": firstNonEmptyAuth(env.Msg, "success"), - "tip": firstNonEmptyAuth(env.Tip, "登录成功"), - "data": user, - }) -} - -// Guest 代理上游游客登录,成功后写入登录 cookie。 -// 对应 POST /api/app/auth/guest;需传 devID,上游要求 X-User-Agent / X-Real-IP(由 BFF 组装)。 -func Guest(c *gin.Context) { - var body struct { - DevID string `json:"devID"` - DeviceID string `json:"deviceId"` - Mac string `json:"mac"` - Channel string `json:"channel"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 1001, - "msg": "参数错误", - "tip": "参数错误", - "data": nil, - }) - return - } - - body.DevID = strings.TrimSpace(body.DevID) - body.DeviceID = strings.TrimSpace(body.DeviceID) - body.Mac = strings.TrimSpace(body.Mac) - body.Channel = authser.ResolveChannel(c, body.Channel) - if body.DevID == "" { - c.JSON(http.StatusOK, gin.H{ - "code": 1001, - "msg": "参数错误", - "tip": "缺少设备标识", - "data": nil, - }) - return - } - if body.DeviceID == "" { - body.DeviceID = body.DevID - } - - env, user, err := authser.GuestLogin(c, authser.GuestLoginReq{ - DevID: body.DevID, - DeviceID: body.DeviceID, - Mac: body.Mac, - Channel: body.Channel, - }) - if err != nil { - log.Error("guest login upstream failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "guest login failed", - "tip": "游客登录失败,请稍后重试", - "data": nil, - }) - return - } - - if env == nil || env.Code != 200 || user == nil { - tip := "游客登录失败" - msg := "" - code := 500 - if env != nil { - code = env.Code - msg = env.Msg - if t := strings.TrimSpace(env.Tip); t != "" { - tip = t - } else if m := strings.TrimSpace(env.Msg); m != "" { - tip = m - } - } - c.JSON(http.StatusOK, gin.H{ - "code": code, - "msg": msg, - "tip": tip, - "data": nil, - }) - return - } - - authser.SetAuthCookies(c, user) - c.JSON(http.StatusOK, gin.H{ - "code": 200, - "msg": firstNonEmptyAuth(env.Msg, "success"), - "tip": firstNonEmptyAuth(env.Tip, "游客登录成功"), - "data": user, - }) -} - -// Logout 清除登录 cookie,并标记跳过紧接着的一次自动游客登录。 -func Logout(c *gin.Context) { - authser.ClearAuthCookies(c) - authser.MarkGuestSkipAuto(c) - c.JSON(http.StatusOK, gin.H{ - "code": 200, - "msg": "success", - "tip": "已退出登录", - "data": nil, - }) -} - -func firstNonEmptyAuth(values ...string) string { - for _, v := range values { - if strings.TrimSpace(v) != "" { - return v - } - } - return "" -} diff --git a/app/api/web/chapterLike.go b/app/api/web/chapterLike.go deleted file mode 100644 index a65358c..0000000 --- a/app/api/web/chapterLike.go +++ /dev/null @@ -1,120 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/playletser" - - "github.com/gin-gonic/gin" -) - -// ChapterLikeSet BFF:代理 POST /api/app/playlet/chapter/like/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_like_set -func ChapterLikeSet(c *gin.Context) { - var body struct { - ChapterID int64 `json:"chapterId"` - IsLike *bool `json:"isLike"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ChapterID <= 0 || body.IsLike == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "chapterId/isLike required", - "tip": "缺少剧集 ID 或点赞状态", - "data": nil, - }) - return - } - - env, err := playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{ - ChapterID: body.ChapterID, - IsLike: *body.IsLike, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} - -// ChapterCollectSet BFF:代理 POST /api/app/playlet/chapter/collect/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_collect_set -func ChapterCollectSet(c *gin.Context) { - var body struct { - ChapterID int64 `json:"chapterId"` - IsCollect *bool `json:"isCollect"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ChapterID <= 0 || body.IsCollect == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "chapterId/isCollect required", - "tip": "缺少剧集 ID 或收藏状态", - "data": nil, - }) - return - } - - env, err := playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{ - ChapterID: body.ChapterID, - IsCollect: *body.IsCollect, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/chapterUnlock.go b/app/api/web/chapterUnlock.go deleted file mode 100644 index 7082bc7..0000000 --- a/app/api/web/chapterUnlock.go +++ /dev/null @@ -1,64 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/playletser" - - "github.com/gin-gonic/gin" -) - -// ChapterUnlock BFF:代理 POST /api/app/playlet/chapter/unlock。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_unlock -func ChapterUnlock(c *gin.Context) { - var body struct { - ChapterID int64 `json:"chapterId"` - PlayletID int64 `json:"playletId"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ChapterID <= 0 || body.PlayletID <= 0 { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "chapterId/playletId required", - "tip": "缺少剧集或短剧 ID", - "data": nil, - }) - return - } - - env, err := playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{ - ChapterID: body.ChapterID, - PlayletID: body.PlayletID, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "解锁失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "解锁失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/chapterWatch.go b/app/api/web/chapterWatch.go deleted file mode 100644 index 4ec026a..0000000 --- a/app/api/web/chapterWatch.go +++ /dev/null @@ -1,75 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/playletser" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -// ChapterWatchReport BFF:代理 POST /api/app/playlet/chapter/watch/report。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_chapter_watch_report -func ChapterWatchReport(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{ - "code": 401, - "msg": "unauthorized", - "tip": "请先登录", - "data": nil, - }) - return - } - - var body struct { - ChapterID int64 `json:"chapterId"` - PlayletID int64 `json:"playletId"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ChapterID <= 0 { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "chapterId required", - "tip": "缺少剧集 ID", - "data": nil, - }) - return - } - - env, err := playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{ - ChapterID: body.ChapterID, - PlayletID: body.PlayletID, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "上报失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "上报失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/comment.go b/app/api/web/comment.go index 6f3c044..a62c99e 100644 --- a/app/api/web/comment.go +++ b/app/api/web/comment.go @@ -3,7 +3,6 @@ package web import ( "net/http" "strconv" - "strings" "videoweb/app/service/commentser" "github.com/gin-gonic/gin" @@ -42,114 +41,3 @@ func CommentList(c *gin.Context) { } // CommentSend BFF:代理 POST /api/app/comment/send。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/post_api_app_comment_send -func CommentSend(c *gin.Context) { - var req commentser.CommentSendReq - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - env, err := commentser.Send(c, req) - if err != nil { - tip := "发布失败" - msg := err.Error() - switch { - case strings.Contains(msg, "empty text"): - tip = "请输入评论内容" - case strings.Contains(msg, "too long"): - tip = "评论不能超过 200 字" - case strings.Contains(msg, "invalid objectId"): - tip = "缺少评论对象" - } - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": msg, - "tip": tip, - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "发布失败", - "data": nil, - }) - return - } - tip := env.Tip - if tip == "" { - tip = env.Msg - } - // 上游参数错误兜底提示(例如 objectType 不被支持) - if env.Code == 1001 && (tip == "" || tip == "参数错误") { - tip = "评论参数错误,请稍后重试" - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": tip, - "data": env.Data, - }) -} - -// CommentLikeSet BFF:代理 POST /api/app/comment/like/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/评论APP/post_api_app_comment_like_set -func CommentLikeSet(c *gin.Context) { - var body struct { - CommentID int64 `json:"commentId"` - IsLike *bool `json:"isLike"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.CommentID <= 0 || body.IsLike == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "commentId/isLike required", - "tip": "缺少评论 ID 或点赞状态", - "data": nil, - }) - return - } - env, err := commentser.LikeSet(c, commentser.CommentLikeSetReq{ - CommentID: body.CommentID, - IsLike: *body.IsLike, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/favorites.go b/app/api/web/favorites.go deleted file mode 100644 index d01bb57..0000000 --- a/app/api/web/favorites.go +++ /dev/null @@ -1,338 +0,0 @@ -package web - -import ( - "fmt" - "strconv" - "strings" - "videoweb/app/service/authser" - "videoweb/app/service/playletser" - "videoweb/app/service/videoser" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -type favoriteWork struct { - ID int64 - Title string - Genre string - DesktopMeta string - Episodes string - Badge string - Image string - Kind string - URL string - DramaID int64 - Episode int - VideoID int64 - TotalEpisode int -} - -// Favorites 我的收藏:短剧 / 剧集(视频) / 章节收藏。 -// 文档: -// http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_collect_list -// http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_collect_list -func Favorites(c *gin.Context) { - filter, page, validRoute := favoritesRoute(c) - if !validRoute { - NotFound(c) - return - } - - data := gin.H{"title": "我的收藏 - 黄果短剧", "navActive": "favorites", "accountTab": "favorites"} - responses := gin.H{} - loadAccountSummary(c, data, responses) - - status := 0 - switch filter { - case "short", "chapter", "series": - case "following": - status = 1 - case "finished": - status = 2 - default: - filter = "all" - } - data["filter"] = filter - - pageSize := 12 - query := playletser.CollectListQuery{PageNum: page, PageSize: pageSize, WatchStatus: status} - - if useragent.TokenFromRequest(c.Request) == "" { - responses["playlet/collect/list"] = gin.H{"error": "暂无登录会话"} - responses["video/collect/list"] = gin.H{"error": "暂无登录会话"} - data["favoritesResponse"] = responses["playlet/collect/list"] - data["playletCollectResponse"] = responses["playlet/collect/list"] - data["videoCollectResponse"] = responses["video/collect/list"] - data["favoritesDebug"] = responses - renderFavorites(c, data) - return - } - - if filter == "chapter" { - list, code, err := playletser.FetchChapterCollectList(c, query) - if retry, _ := authser.OnSessionExpired(c, code); retry { - list, _, err = playletser.FetchChapterCollectList(c, query) - } - if err != nil { - log.Error("fetch chapter favorites page failed", log.E(err)) - responses["playlet/chapter/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()} - data["favoritesUnavailable"] = true - } else if list != nil { - responses["playlet/chapter/collect/list"] = gin.H{"code": 200, "data": list} - data["paging"] = makeFavoritesPage(query.PageNum, query.PageSize, list.TotalCount, filter) - data["favorites"] = chapterFavoriteWorks(list.List) - } else { - responses["playlet/chapter/collect/list"] = gin.H{"code": code, "data": list} - } - data["favoritesResponse"] = responses["playlet/chapter/collect/list"] - data["playletCollectResponse"] = responses["playlet/chapter/collect/list"] - data["favoritesDebug"] = responses - renderFavorites(c, data) - return - } - - if filter == "series" { - loadVideoFavorites(c, data, responses, page, pageSize) - data["favoritesResponse"] = responses["video/collect/list"] - data["playletCollectResponse"] = gin.H{} - data["videoCollectResponse"] = responses["video/collect/list"] - data["favoritesDebug"] = responses - renderFavorites(c, data) - return - } - - var works []favoriteWork - var totalCount int64 - playletFailed := false - videoFailed := false - - if filter == "all" || filter == "short" || filter == "following" || filter == "finished" { - list, code, err := playletser.FetchCollectList(c, query) - if retry, _ := authser.OnSessionExpired(c, code); retry { - list, code, err = playletser.FetchCollectList(c, query) - } - if err == nil && list != nil { - responses["playlet/collect/list"] = gin.H{"code": 200, "data": list} - works = append(works, playletFavoriteWorks(list.List)...) - totalCount += list.TotalCount - } else if err != nil { - responses["playlet/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()} - log.Error("fetch favorites page failed", log.E(err)) - playletFailed = true - } else { - responses["playlet/collect/list"] = gin.H{"code": code, "data": list} - } - } - - if filter == "all" { - videoQuery := videoser.CollectListQuery{PageNum: page, PageSize: pageSize} - videoList, videoCode, videoErr := videoser.FetchCollectList(c, videoQuery) - if retry, _ := authser.OnSessionExpired(c, videoCode); retry { - videoList, videoCode, videoErr = videoser.FetchCollectList(c, videoQuery) - } - if videoErr == nil && videoList != nil { - responses["video/collect/list"] = gin.H{"code": 200, "data": videoList} - works = append(works, videoFavoriteWorks(videoList.List)...) - totalCount += videoList.TotalCount - } else if videoErr != nil { - responses["video/collect/list"] = gin.H{"error": "加载失败", "msg": videoErr.Error()} - log.Error("fetch video favorites page failed", log.E(videoErr)) - videoFailed = true - } else { - responses["video/collect/list"] = gin.H{"code": videoCode, "data": videoList} - } - } - - if playletFailed && (filter != "all" || videoFailed) { - data["favoritesUnavailable"] = true - } - if filter == "all" && playletFailed && videoFailed { - data["favoritesUnavailable"] = true - } - - data["paging"] = makeFavoritesPage(page, pageSize, totalCount, filter) - data["favorites"] = works - data["favoritesResponse"] = responses["playlet/collect/list"] - data["playletCollectResponse"] = responses["playlet/collect/list"] - data["videoCollectResponse"] = responses["video/collect/list"] - data["favoritesDebug"] = responses - renderFavorites(c, data) -} - -// renderFavorites 收藏为空时附带「大家都在看」后渲染。 -func renderFavorites(c *gin.Context, data gin.H) { - attachFavoritesRecommended(c, data) - HTMLOK(c, "favorites.html", data) -} - -// attachFavoritesRecommended 列表为空时拉取最热短剧(与搜索无结果一致)。 -// 上游:GET /api/app/playlet/all/list sortType=2 -func attachFavoritesRecommended(c *gin.Context, data gin.H) { - if works, ok := data["favorites"].([]favoriteWork); ok && len(works) > 0 { - return - } - watchingQ := playletser.AllListQuery{ - PageNum: 1, - PageSize: 6, - SortType: 2, // 最热 - UpdateStatus: 0, - } - watching := playletser.FetchAllListOrEmpty(c, watchingQ) - if watching == nil || len(watching.List) == 0 { - return - } - list := watching.List - if len(list) > 6 { - list = list[:6] - } - data["recommended"] = list -} - -func loadVideoFavorites(c *gin.Context, data gin.H, responses gin.H, page, pageSize int) { - videoQuery := videoser.CollectListQuery{PageNum: page, PageSize: pageSize} - list, code, err := videoser.FetchCollectList(c, videoQuery) - if retry, _ := authser.OnSessionExpired(c, code); retry { - list, code, err = videoser.FetchCollectList(c, videoQuery) - } - if err != nil { - log.Error("fetch video favorites page failed", log.E(err)) - responses["video/collect/list"] = gin.H{"error": "加载失败", "msg": err.Error()} - data["favoritesUnavailable"] = true - return - } - if list == nil { - responses["video/collect/list"] = gin.H{"code": code, "data": list} - return - } - responses["video/collect/list"] = gin.H{"code": 200, "data": list} - data["paging"] = makeFavoritesPage(page, pageSize, list.TotalCount, "series") - data["favorites"] = videoFavoriteWorks(list.List) -} - -func favoritesRoute(c *gin.Context) (filter string, page int, valid bool) { - if c.Request.URL.Query().Has("filter") { - return "", 0, false - } - filter = strings.TrimSpace(c.Param("filter")) - switch filter { - case "": - filter = "all" - case "short", "chapter", "series", "following", "finished": - default: - return "", 0, false - } - - page, valid = accountPageNum(c) - if !valid { - return "", 0, false - } - return filter, page, true -} - -func favoritesPagePath(filter string, page int) string { - path := "/favorites" - if filter != "" && filter != "all" { - path += "/" + filter - } - if page > 1 { - path += "/page/" + strconv.Itoa(page) - } - return path -} - -func makeFavoritesPage(page, size int, count int64, filter string) accountPage { - total := int((count + int64(size) - 1) / int64(size)) - pagination := accountPage{Current: page, Total: total, Count: count} - if page > 1 { - pagination.PrevURL = favoritesPagePath(filter, page-1) - } - if page < total { - pagination.NextURL = favoritesPagePath(filter, page+1) - } - return pagination -} - -func playletFavoriteWorks(items []playletser.PlayletCollectData) []favoriteWork { - works := make([]favoriteWork, 0, len(items)) - for _, item := range items { - title := strings.TrimSpace(item.PlayName) - if title == "" { - title = strings.TrimSpace(item.Title) - } - if title == "" { - continue - } - work := favoriteWork{ - ID: item.ID, Title: title, Genre: strings.Join(item.ThemeNames, " "), - Image: profileCover(item.Cover), Kind: "short", - URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID), - DramaID: item.ID, Episode: item.LastEpisode, VideoID: item.LastChapterID, - TotalEpisode: item.TotalEpisode, - } - if work.Episode <= 0 { - work.Episode = 1 - } - if item.TotalEpisode > 0 { - work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode) - work.DesktopMeta = work.Episodes - } - if item.IsFeatured { - work.Badge = "爆剧" - } - works = append(works, work) - } - return works -} - -func chapterFavoriteWorks(items []playletser.PlayletChapterCollectData) []favoriteWork { - works := make([]favoriteWork, 0, len(items)) - for _, item := range items { - title := strings.TrimSpace(item.PlayletTitle) - if title == "" { - continue - } - work := favoriteWork{ - ID: item.ChapterID, Title: title, Image: profileCover(item.PlayletCover), Kind: "chapter", - URL: playletser.ChapterPlayPath(item.PlayletSlug, item.PlayletID, item.ChapterID), DesktopMeta: item.ChapterTitle, - DramaID: item.PlayletID, Episode: item.CurrentEpisode, VideoID: item.ChapterID, - TotalEpisode: item.TotalEpisode, - } - if work.Episode <= 0 { - work.Episode = 1 - } - if item.CurrentEpisode > 0 { - work.Genre = fmt.Sprintf("第 %d 集", item.CurrentEpisode) - } - if item.TotalEpisode > 0 { - work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode) - } - works = append(works, work) - } - return works -} - -func videoFavoriteWorks(items []videoser.VideoCollectData) []favoriteWork { - works := make([]favoriteWork, 0, len(items)) - for _, item := range items { - title := strings.TrimSpace(item.Title) - if title == "" { - continue - } - work := favoriteWork{ - ID: item.ID, Title: title, Genre: strings.Join(item.Keyworld, " "), - Image: profileCover(item.CoverImg), Kind: "series", - URL: videoser.VideoPlayURL(item.Slug, item.ID), - } - if item.VideoTime > 0 { - work.Episodes = formatVideoDuration(item.VideoTime) - work.DesktopMeta = work.Episodes - } else { - work.Episodes = "剧集" - work.DesktopMeta = "剧集" - } - works = append(works, work) - } - return works -} diff --git a/app/api/web/feedback.go b/app/api/web/feedback.go index d67942d..a37f4bc 100644 --- a/app/api/web/feedback.go +++ b/app/api/web/feedback.go @@ -4,7 +4,6 @@ import ( "net/http" "path/filepath" "strings" - "videoweb/app/service/authser" "videoweb/app/service/feedbackser" "github.com/gin-gonic/gin" @@ -113,7 +112,7 @@ func UploadImage(c *gin.Context) { if name == "" || name == "." || name == "/" { name = "image.jpg" } - resp, err := authser.UploadImage(c, name, f) + resp, err := feedbackser.UploadImage(c, name, f) if err != nil { c.JSON(http.StatusOK, gin.H{ "code": 500, diff --git a/app/api/web/history.go b/app/api/web/history.go deleted file mode 100644 index 626dc76..0000000 --- a/app/api/web/history.go +++ /dev/null @@ -1,375 +0,0 @@ -package web - -import ( - "fmt" - "net/http" - "sort" - "strings" - "time" - "videoweb/app/service/authser" - "videoweb/app/service/playletser" - "videoweb/app/service/videoser" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -type historyEntry struct { - ID int64 - Title string - Current int - Total int - EpisodeText string - Genre string - Time string - Image string - Progress string - Kind string - URL string - sortAt time.Time -} - -type historyGroup struct { - Title string - Entries []historyEntry -} - -// History 观看历史页:短剧 GET /api/app/playlet/history/list + 剧集 GET /api/app/video/history/list。 -// 文档: -// http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_history_list -// http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_history_list -func History(c *gin.Context) { - page, validPage := accountPageNum(c) - if !validPage { - NotFound(c) - return - } - - data := gin.H{"title": "观看历史 - 黄果短剧", "navActive": "history", "accountTab": "history"} - responses := gin.H{} - loadAccountSummary(c, data, responses) - - pageSize := 20 - var entries []historyEntry - var totalCount int64 - - if useragent.TokenFromRequest(c.Request) == "" { - responses["playlet/history/list"] = gin.H{"error": "暂无登录会话"} - responses["video/history/list"] = gin.H{"error": "暂无登录会话"} - } else { - playletQuery := playletser.HistoryListQuery{PageNum: page, PageSize: pageSize} - playletList, playletCode, playletErr := playletser.FetchHistoryList(c, playletQuery) - if retry, _ := authser.OnSessionExpired(c, playletCode); retry { - playletList, playletCode, playletErr = playletser.FetchHistoryList(c, playletQuery) - } - if playletErr == nil && playletList != nil { - responses["playlet/history/list"] = gin.H{"code": 200, "data": playletList} - entries = append(entries, playletHistoryEntries(playletList.List)...) - totalCount += playletList.TotalCount - } else if playletErr != nil { - responses["playlet/history/list"] = gin.H{"error": "加载失败", "msg": playletErr.Error()} - log.Error("fetch playlet history page failed", log.E(playletErr)) - } else { - responses["playlet/history/list"] = gin.H{"code": playletCode, "data": playletList} - } - - videoQuery := videoser.HistoryListQuery{PageNum: page, PageSize: pageSize} - videoList, videoCode, videoErr := videoser.FetchHistoryList(c, videoQuery) - if retry, _ := authser.OnSessionExpired(c, videoCode); retry { - videoList, videoCode, videoErr = videoser.FetchHistoryList(c, videoQuery) - } - if videoErr == nil && videoList != nil { - responses["video/history/list"] = gin.H{"code": 200, "data": videoList} - entries = append(entries, videoHistoryEntries(videoList.List)...) - totalCount += videoList.TotalCount - } else if videoErr != nil { - responses["video/history/list"] = gin.H{"error": "加载失败", "msg": videoErr.Error()} - log.Error("fetch video history page failed", log.E(videoErr)) - } else { - responses["video/history/list"] = gin.H{"code": videoCode, "data": videoList} - } - - if playletErr != nil && videoErr != nil { - data["historyUnavailable"] = true - } - } - - data["paging"] = makeAccountPage("/history", page, pageSize, totalCount) - data["groups"] = groupHistoryEntries(entries) - data["historyResponse"] = responses["playlet/history/list"] - data["playletHistoryResponse"] = responses["playlet/history/list"] - data["videoHistoryResponse"] = responses["video/history/list"] - data["historyDebug"] = responses - HTMLOK(c, "history.html", data) -} - -// HistoryClear BFF:代理 POST /api/app/playlet/history/clear。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_clear -func HistoryClear(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - env, err := playletser.HistoryClear(c) - if err != nil { - log.Error("clear playlet history failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "清空失败,请稍后重试", "data": nil}) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "清空失败,请稍后重试", "data": nil}) - return - } - if env.Code != 200 { - tip := strings.TrimSpace(env.Tip) - if tip == "" { - tip = strings.TrimSpace(env.Msg) - } - if tip == "" { - tip = "清空失败,请稍后重试" - } - c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data}) - return - } - c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data}) -} - -// VideoHistoryClear BFF:代理 POST /api/app/video/history/clear。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_clear -func VideoHistoryClear(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - env, err := videoser.HistoryClear(c) - if err != nil { - log.Error("clear video history failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "清空失败,请稍后重试", "data": nil}) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "清空失败,请稍后重试", "data": nil}) - return - } - if env.Code != 200 { - tip := strings.TrimSpace(env.Tip) - if tip == "" { - tip = strings.TrimSpace(env.Msg) - } - if tip == "" { - tip = "清空失败,请稍后重试" - } - c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data}) - return - } - c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data}) -} - -// HistoryDelete BFF:代理 POST /api/app/playlet/history/delete。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_delete -func HistoryDelete(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - var body playletser.PlayletHistoryDeleteReq - if err := c.ShouldBindJSON(&body); err != nil || body.PlayletID <= 0 { - c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "playletId required", "tip": "参数错误", "data": nil}) - return - } - env, err := playletser.HistoryDelete(c, body) - if err != nil { - log.Error("delete playlet history failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "删除失败,请稍后重试", "data": nil}) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "删除失败,请稍后重试", "data": nil}) - return - } - if env.Code != 200 { - tip := strings.TrimSpace(env.Tip) - if tip == "" { - tip = strings.TrimSpace(env.Msg) - } - if tip == "" { - tip = "删除失败,请稍后重试" - } - c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data}) - return - } - c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data}) -} - -// VideoHistoryDelete BFF:代理 POST /api/app/video/history/delete。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_delete -func VideoHistoryDelete(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - var body videoser.VideoHistoryDeleteReq - if err := c.ShouldBindJSON(&body); err != nil || body.VideoID <= 0 { - c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "videoId required", "tip": "参数错误", "data": nil}) - return - } - env, err := videoser.HistoryDelete(c, body) - if err != nil { - log.Error("delete video history failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "删除失败,请稍后重试", "data": nil}) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "empty response", "tip": "删除失败,请稍后重试", "data": nil}) - return - } - if env.Code != 200 { - tip := strings.TrimSpace(env.Tip) - if tip == "" { - tip = strings.TrimSpace(env.Msg) - } - if tip == "" { - tip = "删除失败,请稍后重试" - } - c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data}) - return - } - c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "ok", "tip": "", "data": env.Data}) -} - -func playletHistoryEntries(items []playletser.PlayletHistoryData) []historyEntry { - out := make([]historyEntry, 0, len(items)) - for _, item := range items { - title := strings.TrimSpace(item.PlayName) - if title == "" { - title = strings.TrimSpace(item.Title) - } - if title == "" { - continue - } - progress := 0 - if item.TotalEpisode > 0 { - progress = item.WatchedCount * 100 / item.TotalEpisode - } - if progress < 0 { - progress = 0 - } - if progress > 100 { - progress = 100 - } - out = append(out, historyEntry{ - ID: item.ID, - Title: title, - Current: item.LastEpisode, - Total: item.TotalEpisode, - Genre: strings.Join(item.ThemeNames, " "), - Time: playletser.FormatHistoryListTime(item.HistoryAt), - Image: profileCover(item.Cover), - Progress: fmt.Sprintf("%d%%", progress), - Kind: "short", - URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID), - sortAt: historySortAt(item.HistoryAt), - }) - } - return out -} - -func videoHistoryEntries(items []videoser.VideoHistoryData) []historyEntry { - out := make([]historyEntry, 0, len(items)) - for _, item := range items { - title := strings.TrimSpace(item.Title) - if title == "" { - continue - } - out = append(out, historyEntry{ - ID: item.ID, - Title: title, - EpisodeText: videoHistoryEpisodeText(item), - Genre: strings.Join(item.Keyworld, " "), - Time: playletser.FormatHistoryListTime(item.HistoryAt), - Image: profileCover(item.CoverImg), - Progress: "", - Kind: "series", - URL: videoser.VideoPlayURL(item.Slug, item.ID), - sortAt: historySortAt(item.HistoryAt), - }) - } - return out -} - -func videoHistoryEpisodeText(item videoser.VideoHistoryData) string { - if item.VideoTime > 0 { - return formatVideoDuration(item.VideoTime) - } - return "剧集" -} - -func formatVideoDuration(sec int) string { - if sec <= 0 { - return "" - } - h := sec / 3600 - m := (sec % 3600) / 60 - s := sec % 60 - if h > 0 { - return fmt.Sprintf("%d:%02d:%02d", h, m, s) - } - return fmt.Sprintf("%d:%02d", m, s) -} - -func historySortAt(raw string) time.Time { - t, ok := playletser.ParseHistoryTime(raw) - if !ok { - return time.Time{} - } - return t -} - -func groupHistoryEntries(entries []historyEntry) []historyGroup { - if len(entries) == 0 { - return nil - } - sort.SliceStable(entries, func(i, j int) bool { - if entries[i].sortAt.Equal(entries[j].sortAt) { - return entries[i].ID > entries[j].ID - } - return entries[i].sortAt.After(entries[j].sortAt) - }) - order := []string{"今天", "昨天", "更早"} - buckets := map[string][]historyEntry{ - "今天": {}, - "昨天": {}, - "更早": {}, - } - for _, entry := range entries { - bucket := historyBucketFromTime(entry.sortAt) - buckets[bucket] = append(buckets[bucket], entry) - } - groups := make([]historyGroup, 0, 3) - for _, title := range order { - if len(buckets[title]) == 0 { - continue - } - groups = append(groups, historyGroup{Title: title, Entries: buckets[title]}) - } - return groups -} - -func historyBucketFromTime(t time.Time) string { - if t.IsZero() { - return "更早" - } - now := time.Now() - startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) - dayDiff := int(startToday.Sub(startThat).Hours() / 24) - switch { - case dayDiff <= 0: - return "今天" - case dayDiff == 1: - return "昨天" - default: - return "更早" - } -} diff --git a/app/api/web/index.go b/app/api/web/index.go index f9fb0a2..4cc7598 100644 --- a/app/api/web/index.go +++ b/app/api/web/index.go @@ -9,7 +9,6 @@ import ( // Index 首页 SSR:拉取 GET /api/app/playlet/home,按接口字段注入模板。 // 接口文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_home -// 继续观看:GET /api/app/playlet/history/list?pageNum=1&pageSize=6 // 热播榜:短剧 hot/rank/list;剧集 video/all/list sortType=最热 func Index(c *gin.Context) { raw := playletser.FetchHomeOrEmpty(c) @@ -17,12 +16,6 @@ func Index(c *gin.Context) { if raw != nil && raw.SeoInfo.Title != "" { title = raw.SeoInfo.Title } - history := playletser.FetchHistoryListOrEmpty(c, playletser.HistoryListQuery{PageNum: 1, PageSize: 6}) - continueWatch := history.List - if len(continueWatch) > 6 { - continueWatch = continueWatch[:6] - } - hotPlaylets := playletser.FetchHotRankListOrEmpty(c, playletser.HotRankQuery{ PageNum: 1, PageSize: 10, @@ -52,7 +45,6 @@ func Index(c *gin.Context) { "homeConfigData": playletser.ActiveHomeConfigs(raw.HomeConfigData), // GET /api/app/playlet/tag/options → list(已上架标签) "tagOptions": playletser.FetchTagOptionsOrEmpty(c), - "continueWatch": continueWatch, "hotRankPlaylets": hotPlaylets, "hotRankVideos": hotVideos, }) diff --git a/app/api/web/login.go b/app/api/web/login.go deleted file mode 100644 index 4736777..0000000 --- a/app/api/web/login.go +++ /dev/null @@ -1,11 +0,0 @@ -package web - -import "github.com/gin-gonic/gin" - -// LoginPage H5 登录页:渲染 template/pages/login.html。 -// PC 端由页面内脚本回首页并打开 loginPop;H5 直接展示本页。 -func LoginPage(c *gin.Context) { - HTMLOK(c, "login.html", gin.H{ - "title": "登录 - 黄果短剧", - }) -} diff --git a/app/api/web/play.go b/app/api/web/play.go index b5cdeef..9798e42 100644 --- a/app/api/web/play.go +++ b/app/api/web/play.go @@ -1,9 +1,7 @@ package web import ( - "strconv" "strings" - "videoweb/app/service/authser" "videoweb/app/service/commentser" "videoweb/app/service/playletser" "videoweb/app/service/videoser" @@ -92,13 +90,9 @@ func Play(c *gin.Context) { SortOrder: commentser.SortHot, }) - unlockCost := int64(0) - balance := int64(0) canWatch := false episode := 0 if chapter != nil { - unlockCost = chapter.UnlockCost() - balance = chapter.Balance canWatch = chapter.AllowWatch() episode = chapter.CurrentEpisode } @@ -117,9 +111,6 @@ func Play(c *gin.Context) { if episode <= 0 { episode = 1 } - if unlockCost <= 0 { - unlockCost = int64(playletser.FirstUnlockCost(playlet.PlayletChapterInfo)) - } previewURL := strings.TrimSpace(playlet.Preview) mainURL := "" @@ -139,9 +130,6 @@ func Play(c *gin.Context) { "mainURL": mainURL, "previewURL": previewURL, "hasPreview": previewURL != "", - "unlockCost": unlockCost, - "balance": balance, - "balanceText": authser.FormatRecharge(balance), "nextChapterID": playletser.NextChapterID(playlet, chapterID), "advitise": playlet.Advitise, "related": playlet.RelatedRecommendData, @@ -157,7 +145,6 @@ func Play(c *gin.Context) { "commentSize": comments.PageSize, "commentObjectID": commentObjectID, "commentObjectType": commentObjectType, - "trackerDrama": dramaTrackingData(playlet, chapter, chapterID, episode), "playResponse": gin.H{ "playlet": playlet, "chapter": chapter, @@ -174,76 +161,9 @@ func Play(c *gin.Context) { }, } data["canonicalPath"] = playletser.PlayPath(playlet.Slug, playlet.ID) - attachRechargeModal(c, data) HTMLOK(c, "play.html", data) } -func dramaTrackingData(playlet *playletser.PlayletInfoResp, chapter *playletser.PlayletChapterInfoResp, chapterID int64, episode int) gin.H { - name := strings.TrimSpace(playlet.PlayName) - if name == "" { - name = strings.TrimSpace(playlet.Title) - } - totalEpisode := playlet.TotalEpisode - if totalEpisode <= 0 { - totalEpisode = len(playlet.PlayletChapterInfo) - } - if totalEpisode <= 0 { - totalEpisode = 1 - } - data := gin.H{ - "MediaID": "", - "DramaID": playlet.ID, - "DramaTitle": name, - "Episode": max(1, episode), - "TotalEpisode": totalEpisode, - "VideoID": chapterID, - "DramaTypeID": "default", - "DramaTypeName": "默认分类", - "DramaTagKey": "default", - "DramaTagName": "默认标签", - "DramaDuration": 0, - "RecommendTraceID": "", - } - if chapter != nil { - if chapter.ID > 0 { - data["VideoID"] = chapter.ID - } - if chapter.MediaTime > 0 { - data["DramaDuration"] = chapter.MediaTime - } - } - for i, id := range playlet.ThemeIDs { - if id <= 0 || i >= len(playlet.ThemeNames) { - continue - } - name := strings.TrimSpace(playlet.ThemeNames[i]) - if name == "" { - continue - } - data["DramaTypeID"] = strconv.FormatInt(id, 10) - data["DramaTypeName"] = name - break - } - tagIDs := make([]string, 0, len(playlet.TagIDs)) - tagNames := make([]string, 0, len(playlet.TagIDs)) - for i, id := range playlet.TagIDs { - if id <= 0 || i >= len(playlet.TagNames) { - continue - } - name := strings.TrimSpace(playlet.TagNames[i]) - if name == "" { - continue - } - tagIDs = append(tagIDs, strconv.FormatInt(id, 10)) - tagNames = append(tagNames, name) - } - if len(tagIDs) > 0 { - data["DramaTagKey"] = strings.Join(tagIDs, ",") - data["DramaTagName"] = strings.Join(tagNames, ",") - } - return data -} - // SeriesPlay 视频播放页:GET /api/app/video/detail。 // 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_detail func SeriesPlay(c *gin.Context) { @@ -369,8 +289,6 @@ func SeriesPlay(c *gin.Context) { "list": comments.List, "apis": []string{ "GET /api/app/comment/list", - "POST /api/app/comment/send", - "POST /api/app/comment/like/set", }, }, "playDebug": gin.H{ @@ -383,6 +301,5 @@ func SeriesPlay(c *gin.Context) { if strings.TrimSpace(c.Param("guessPage")) != "" { data["canonicalPath"] = videoser.VideoPlayURL(video.Slug, video.ID) } - attachRechargeModal(c, data) HTMLOK(c, "seriesPlay.html", data) } diff --git a/app/api/web/playletLike.go b/app/api/web/playletLike.go deleted file mode 100644 index 593531e..0000000 --- a/app/api/web/playletLike.go +++ /dev/null @@ -1,64 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/playletser" - - "github.com/gin-gonic/gin" -) - -// PlayletLikeSet BFF:代理 POST /api/app/playlet/like/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_like_set -func PlayletLikeSet(c *gin.Context) { - var body struct { - ID int64 `json:"id"` - IsLike *bool `json:"isLike"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ID <= 0 || body.IsLike == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "id/isLike required", - "tip": "缺少短剧 ID 或点赞状态", - "data": nil, - }) - return - } - - env, err := playletser.LikeSet(c, playletser.PlayletLikeSetReq{ - ID: body.ID, - IsLike: *body.IsLike, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/playlet_collect_set.go b/app/api/web/playlet_collect_set.go deleted file mode 100644 index eeb7523..0000000 --- a/app/api/web/playlet_collect_set.go +++ /dev/null @@ -1,64 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/playletser" - - "github.com/gin-gonic/gin" -) - -// PlayletCollectSet BFF:代理 POST /api/app/playlet/collect/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_collect_set -func PlayletCollectSet(c *gin.Context) { - var body struct { - ID int64 `json:"id"` - IsCollect *bool `json:"isCollect"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ID <= 0 || body.IsCollect == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "id/isCollect required", - "tip": "缺少短剧 ID 或收藏状态", - "data": nil, - }) - return - } - - env, err := playletser.CollectSet(c, playletser.PlayletCollectSetReq{ - ID: body.ID, - IsCollect: *body.IsCollect, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/profile.go b/app/api/web/profile.go deleted file mode 100644 index 95b7b48..0000000 --- a/app/api/web/profile.go +++ /dev/null @@ -1,229 +0,0 @@ -package web - -import ( - "encoding/json" - "fmt" - "net/url" - "strings" - "videoweb/app/service/authser" - "videoweb/app/service/playletser" - "videoweb/app/service/usercenterser" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -type profileWork struct { - Title, Meta, Episodes, Badge, Image, Progress, Remaining, URL string -} - -// Profile 从当前会话拉取用户信息、观看历史和收藏,不展示演示账号数据。 -func Profile(c *gin.Context) { - data := gin.H{ - "title": "个人中心 - 黄果短剧", "navActive": "profile", "accountTab": "profile", - } - responses := gin.H{} - loadAccountSummary(c, data, responses) - if useragent.TokenFromRequest(c.Request) != "" { - fetchProfileLists(c, data, responses) - } - if pretty, err := json.MarshalIndent(responses, "", " "); err == nil { - data["responseJSON"] = string(pretty) - } - HTMLOK(c, "profile.html", data) -} - -func loadAccountSummary(c *gin.Context, data, responses gin.H) { - data["userName"] = "未登录" - data["balance"] = "—" - data["historyCount"] = "—" - data["favoriteCount"] = "—" - data["chasingCount"] = "—" - data["boughtChapterCount"] = "—" - user := authser.SyncUserOnEnter(c) - if user == nil { - if useragent.TokenFromRequest(c.Request) != "" { - responses["auth/getInfo"] = gin.H{"error": "加载失败"} - fetchProfileStats(c, data, responses) - return - } - responses["auth/getInfo"] = gin.H{"error": "暂无登录会话"} - return - } - applyProfileInfo(data, responses, user) - if useragent.TokenFromRequest(c.Request) != "" { - fetchProfileStats(c, data, responses) - } -} - -func fetchProfileStats(c *gin.Context, data, responses gin.H) { - stats, code, err := usercenterser.Fetch(c) - if retry, _ := authser.OnSessionExpired(c, code); retry { - stats, code, err = usercenterser.Fetch(c) - } - if err != nil || stats == nil { - responses["user/center/data"] = gin.H{"error": "加载失败"} - if err != nil { - log.Error("fetch user center data failed", log.E(err)) - } - return - } - responses["user/center/data"] = gin.H{"code": code, "data": stats} - data["historyCount"] = fmt.Sprint(stats.WatchCount) - data["favoriteCount"] = fmt.Sprint(stats.CollectCount) - data["chasingCount"] = fmt.Sprint(stats.ChasingCount) - data["boughtChapterCount"] = fmt.Sprint(stats.BoughtChapterCount) -} - -func fetchProfileLists(c *gin.Context, data, responses gin.H) { - query := playletser.HistoryListQuery{PageNum: 1, PageSize: 6} - history, code, err := playletser.FetchHistoryList(c, query) - if retry, _ := authser.OnSessionExpired(c, code); retry { - history, _, err = playletser.FetchHistoryList(c, query) - } - if err == nil && history != nil { - responses["playlet/history/list"] = gin.H{"code": 200, "data": history} - data["history"] = profileHistoryWorks(history.List) - } else if err != nil { - responses["playlet/history/list"] = gin.H{"error": "加载失败"} - log.Error("fetch profile history failed", log.E(err)) - data["historyUnavailable"] = true - } - - collectQuery := playletser.CollectListQuery{PageNum: 1, PageSize: 6, WatchStatus: 0} - favorites, code, err := playletser.FetchCollectList(c, collectQuery) - if retry, _ := authser.OnSessionExpired(c, code); retry { - favorites, _, err = playletser.FetchCollectList(c, collectQuery) - } - if err == nil && favorites != nil { - responses["playlet/collect/list"] = gin.H{"code": 200, "data": favorites} - data["favorites"] = profileFavoriteWorks(favorites.List) - } else if err != nil { - responses["playlet/collect/list"] = gin.H{"error": "加载失败"} - log.Error("fetch profile favorites failed", log.E(err)) - data["favoritesUnavailable"] = true - } -} - -func applyProfileInfo(data, responses gin.H, user *authser.LoginResp) { - if user == nil { - return - } - // 登录响应可能包含 token;调试区域只展示非凭证字段。 - rawUser, _ := json.Marshal(user) - var safeUser map[string]any - _ = json.Unmarshal(rawUser, &safeUser) - delete(safeUser, "token") - responses["auth/getInfo"] = gin.H{"code": 200, "data": safeUser} - name := strings.TrimSpace(user.NickName) - if name == "" { - name = strings.TrimSpace(user.AccountNo) - } - if name == "" { - name = "用户" - } - data["userName"] = name - data["setting"] = authser.BuildSettingVM(user) - data["userID"] = user.UserID - data["userId"] = fmt.Sprint(user.UserID) - data["balance"] = fmt.Sprint(user.Recharge) - data["recharge"] = user.Recharge - data["rechargeText"] = authser.FormatRecharge(user.Recharge) - if avatar := safeProfileImage(user.Avatar); avatar != "" { - data["avatar"] = avatar - } -} - -func safeProfileImage(raw string) string { - raw = strings.TrimSpace(raw) - if strings.HasPrefix(raw, "/static/") { - return raw - } - u, err := url.Parse(raw) - if err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != "" { - return raw - } - return "" -} - -func profileHistoryWorks(items []playletser.PlayletHistoryData) []profileWork { - works := make([]profileWork, 0, len(items)) - for _, item := range items { - if len(works) == 6 { - break - } - name := strings.TrimSpace(item.Title) - if name == "" { - name = strings.TrimSpace(item.PlayName) - } - if name == "" { - continue - } - progress := 0 - if item.TotalEpisode > 0 { - progress = item.WatchedCount * 100 / item.TotalEpisode - } - if progress < 0 { - progress = 0 - } - if progress > 100 { - progress = 100 - } - meta := "观看记录" - if item.LastEpisode > 0 { - meta = fmt.Sprintf("看至第 %d 集", item.LastEpisode) - } - remaining := "" - if item.UnwatchedCount > 0 { - remaining = fmt.Sprintf("剩 %d 集", item.UnwatchedCount) - } - works = append(works, profileWork{Title: name, Meta: meta, Image: profileCover(item.Cover), Progress: fmt.Sprintf("%d%%", progress), Remaining: remaining, URL: profilePlayURL(item.Slug, item.ID, item.LastEpisode)}) - } - return works -} - -func profileFavoriteWorks(items []playletser.PlayletCollectData) []profileWork { - works := make([]profileWork, 0, len(items)) - for _, item := range items { - if len(works) == 6 { - break - } - name := strings.TrimSpace(item.Title) - if name == "" { - name = strings.TrimSpace(item.PlayName) - } - if name == "" { - continue - } - work := profileWork{Title: name, Meta: strings.Join(item.ThemeNames, " "), Image: profileCover(item.Cover), URL: profilePlayURL(item.Slug, item.ID, 1)} - if item.TotalEpisode > 0 { - work.Episodes = fmt.Sprintf("全 %d 集", item.TotalEpisode) - } - if item.IsFeatured { - work.Badge = "爆剧" - } - works = append(works, work) - } - return works -} - -func profileCover(raw string) string { - if safe := safeProfileImage(raw); safe != "" { - return safe - } - return "/static/images/placeholder.png?v=2" -} - -func profilePlayURL(slug string, id int64, episode int) string { - if slug == "" { - if id <= 0 { - return "" - } - slug = fmt.Sprint(id) - } - if episode < 1 { - episode = 1 - } - return "/play/" + url.PathEscape(slug) + "/" + fmt.Sprint(episode) -} diff --git a/app/api/web/purchased.go b/app/api/web/purchased.go deleted file mode 100644 index f19cd5c..0000000 --- a/app/api/web/purchased.go +++ /dev/null @@ -1,87 +0,0 @@ -package web - -import ( - "fmt" - "strings" - "videoweb/app/service/authser" - "videoweb/app/service/playletser" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -type purchasedWork struct { - Title, Image, Info, Date, Progress, URL string -} - -// PurchasedSeries 已购剧集:GET /api/app/playlet/bought/list。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/get_api_app_playlet_bought_list -func PurchasedSeries(c *gin.Context) { - page, validPage := accountPageNum(c) - if !validPage { - NotFound(c) - return - } - - data := gin.H{"title": "已购剧集 - 黄果短剧", "navActive": "purchased", "accountTab": "purchased"} - responses := gin.H{} - loadAccountSummary(c, data, responses) - - size := 20 - if useragent.TokenFromRequest(c.Request) == "" { - responses["playlet/bought/list"] = gin.H{"error": "暂无登录会话"} - data["purchasedResponse"] = responses["playlet/bought/list"] - HTMLOK(c, "purchased.html", data) - return - } - - list, code, err := playletser.FetchBoughtList(c, page, size) - if retry, _ := authser.OnSessionExpired(c, code); retry { - list, _, err = playletser.FetchBoughtList(c, page, size) - } - if err == nil && list != nil { - responses["playlet/bought/list"] = gin.H{"code": 200, "data": list} - data["paging"] = makeAccountPage("/purchased", page, size, list.TotalCount) - works := make([]purchasedWork, 0, len(list.List)) - for _, item := range list.List { - title := strings.TrimSpace(item.PlayName) - if title == "" { - title = strings.TrimSpace(item.Title) - } - if title == "" { - continue - } - info := fmt.Sprintf("已购 %d 集", item.BoughtChapterCount) - if item.TotalEpisode > 0 { - info += fmt.Sprintf(" / 共 %d 集", item.TotalEpisode) - } - info += fmt.Sprintf(" · %d 金币", item.CostCoins) - progress := 0 - if item.TotalEpisode > 0 { - progress = item.WatchedCount * 100 / item.TotalEpisode - } - if progress < 0 { - progress = 0 - } - if progress > 100 { - progress = 100 - } - works = append(works, purchasedWork{ - Title: title, Image: profileCover(item.Cover), Info: info, - Date: playletser.FormatHistoryAt(item.BoughtAt), Progress: fmt.Sprintf("%d%%", progress), - URL: playletser.ChapterPlayPath(item.Slug, item.ID, item.LastChapterID), - }) - } - data["purchased"] = works - } else if err != nil { - responses["playlet/bought/list"] = gin.H{"error": "加载失败", "msg": err.Error()} - log.Error("fetch bought playlets failed", log.E(err)) - data["purchasedUnavailable"] = true - } else { - responses["playlet/bought/list"] = gin.H{"code": code, "data": list} - } - - data["purchasedResponse"] = responses["playlet/bought/list"] - HTMLOK(c, "purchased.html", data) -} diff --git a/app/api/web/recharge.go b/app/api/web/recharge.go deleted file mode 100644 index 1dc5777..0000000 --- a/app/api/web/recharge.go +++ /dev/null @@ -1,279 +0,0 @@ -package web - -import ( - "net/http" - "net/url" - "strconv" - "strings" - "videoweb/app/service/authser" - "videoweb/app/service/rechargeser" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -const rechargePageSize = 5 - -// Recharge 金币充值页:套餐 GET /api/app/recharge/package/list,购买 POST /api/app/coin/buy,流水 GET /api/app/coin/trans/list。 -func Recharge(c *gin.Context) { - data := gin.H{ - "title": "金币充值 - 黄果短剧", "pageHeading": "金币充值", - "backHref": "/profile", "helpHref": "/help", - } - responses := gin.H{} - loadAccountSummary(c, data, responses) - - packages, err := rechargeser.FetchPackageList(c) - if err != nil { - log.Error("fetch recharge packages failed", log.E(err)) - data["packageUnavailable"] = true - packages = &rechargeser.RechargePackageListResp{} - responses["recharge/package/list"] = gin.H{"error": "加载失败", "msg": err.Error()} - } else { - responses["recharge/package/list"] = gin.H{"code": 200, "data": packages} - } - vm := rechargeser.BuildDepositPageVM(packages, "coin") - data["packages"] = vm.CoinList - data["paymentMethods"] = vm.PayMethods - data["rechargePackageResponse"] = packages - - balance := authser.RechargeFromCookies(c) - if _, ok := data["recharge"]; ok { - balance = toInt64(data["recharge"]) - } - data["balance"] = balance - data["balanceText"] = authser.FormatRecharge(balance) - - page, _, err := fetchRechargeTransactions(c, 1, rechargePageSize, 0) - if err != nil { - log.Error("fetch recharge transactions failed", log.E(err)) - data["transactionsUnavailable"] = true - page = &rechargeser.CoinTransPage{PageNum: 1, PageSize: rechargePageSize} - responses["coin/trans/list"] = gin.H{"error": "加载失败", "msg": err.Error()} - } else { - responses["coin/trans/list"] = gin.H{"code": 200, "data": page} - } - data["transactions"] = rechargeser.BuildTxListFromCoinTrans(page.List, 0) - data["transactionPage"] = page - data["transactionResponse"] = page - data["rechargeDebug"] = responses - HTMLOK(c, "recharge.html", data) -} - -// RechargeCoinBuy BFF:代理 POST /api/app/coin/buy。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/post_api_app_coin_buy -func RechargeCoinBuy(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - var body rechargeser.BuyPayReq - if err := c.ShouldBindJSON(&body); err != nil || body.PackageID <= 0 { - c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "packageId required", "tip": "请选择充值套餐", "data": nil}) - return - } - body.PayMode = rechargeser.NormalizePayMode(body.PayMode) - if body.PayMode == "" { - c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "payMode required", "tip": "请选择支付方式", "data": nil}) - return - } - if !validCoinPackagePayment(c, body.PackageID, body.PayMode) { - c.JSON(http.StatusOK, gin.H{"code": 400, "msg": "invalid package/payMode", "tip": "套餐或支付方式不可用,请刷新后重试", "data": nil}) - return - } - - env, err := rechargeser.BuyCoin(c, body) - if err != nil { - log.Error("buy coin failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "创建支付订单失败,请稍后重试", "data": nil}) - return - } - if env != nil { - if retry, needLogin := authser.OnSessionExpired(c, env.Code); needLogin { - c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": "登录已过期,请重新登录", "data": nil}) - return - } else if retry { - env, err = rechargeser.BuyCoin(c, body) - if err != nil { - log.Error("buy coin retry failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "创建支付订单失败,请稍后重试", "data": nil}) - return - } - } - } - if env == nil || env.Code != 200 { - code, tip, msg := 500, "创建支付订单失败", "buy failed" - if env != nil { - code = env.Code - msg = env.Msg - tip = firstNonEmptyAuth(env.Tip, env.Msg, tip) - } - c.JSON(http.StatusOK, gin.H{"code": code, "msg": msg, "tip": tip, "data": nil}) - return - } - safeURL := safePaymentURL(env.Data.PayURL) - if safeURL == "" { - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "invalid payUrl", "tip": "支付地址无效,请稍后重试", "data": nil}) - return - } - env.Data.PayURL = safeURL - c.JSON(http.StatusOK, gin.H{ - "code": 200, - "msg": env.Msg, - "tip": firstNonEmptyAuth(env.Tip, "订单创建成功"), - "data": env.Data, - }) -} - -// RechargeTransactions BFF:代理 GET /api/app/coin/trans/list。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/get_api_app_coin_trans_list -// query:page / pageSize / type(0全部 1充值 2消费 3奖励) -func RechargeTransactions(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - page := queryInt(c, "page", 1) - pageSize := queryInt(c, "pageSize", rechargePageSize) - typ := queryIntAllowZero(c, "type", 0) - if typ < 0 || typ > 3 { - typ = 0 - } - result, code, err := fetchRechargeTransactions(c, page, pageSize, typ) - if err != nil { - log.Error("fetch recharge transactions failed", log.E(err)) - tip := "交易明细加载失败" - if code > 0 { - c.JSON(http.StatusOK, gin.H{"code": code, "msg": err.Error(), "tip": tip, "data": nil}) - return - } - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": tip, "data": nil}) - return - } - if result == nil { - result = &rechargeser.CoinTransPage{PageNum: page, PageSize: pageSize} - } - c.JSON(http.StatusOK, gin.H{ - "code": 200, - "msg": "ok", - "tip": "", - "data": gin.H{ - "list": rechargeser.BuildTxListFromCoinTrans(result.List, 0), - "rawList": result.List, - "pageNum": result.PageNum, - "pageSize": result.PageSize, - "totalCount": result.TotalCount, - }, - }) -} - -// RechargeCoinTransExport BFF:代理 GET /api/app/coin/trans/export。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/金币/get_api_app_coin_trans_export -// query:type(0全部 1充值 2消费 3奖励),返回 CSV。 -func RechargeCoinTransExport(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录", "data": nil}) - return - } - typ := queryIntAllowZero(c, "type", 0) - if typ < 0 || typ > 3 { - typ = 0 - } - result, err := rechargeser.FetchCoinTransExport(c, typ) - if err != nil { - log.Error("export coin trans failed", log.E(err)) - c.JSON(http.StatusOK, gin.H{"code": 500, "msg": err.Error(), "tip": "导出失败,请稍后重试", "data": nil}) - return - } - ct := strings.TrimSpace(result.ContentType) - if ct == "" || strings.Contains(strings.ToLower(ct), "json") { - ct = "text/csv; charset=utf-8" - } - disposition := strings.TrimSpace(result.ContentDisposition) - if disposition == "" { - disposition = `attachment; filename="coin-trans.csv"` - } - c.Header("Content-Type", ct) - c.Header("Content-Disposition", disposition) - c.Header("Cache-Control", "no-store") - c.Data(http.StatusOK, ct, result.Body) -} - -func fetchRechargeTransactions(c *gin.Context, page, pageSize, typ int) (*rechargeser.CoinTransPage, int, error) { - result, code, err := rechargeser.FetchCoinTransPage(c, page, pageSize, typ) - if retry, _ := authser.OnSessionExpired(c, code); retry { - result, code, err = rechargeser.FetchCoinTransPage(c, page, pageSize, typ) - } - return result, code, err -} - -// attachRechargeModal 注入充值弹窗数据:GET /api/app/recharge/package/list → coinList。 -func attachRechargeModal(c *gin.Context, data gin.H) { - if data == nil { - return - } - packages := rechargeser.FetchPackageListOrEmpty(c) - vm := rechargeser.BuildDepositPageVM(packages, "coin") - data["rechargePackages"] = vm.CoinList - data["rechargePayMethods"] = vm.PayMethods - data["rechargePackageResponse"] = packages - if _, ok := data["unlockCost"]; !ok { - data["unlockCost"] = int64(0) - } - if _, ok := data["balance"]; !ok { - data["balance"] = authser.RechargeFromCookies(c) - } - if _, ok := data["balanceText"]; !ok { - data["balanceText"] = authser.FormatRecharge(toInt64(data["balance"])) - } -} - -func validCoinPackagePayment(c *gin.Context, packageID int64, payMode string) bool { - data, err := rechargeser.FetchPackageList(c) - if err != nil || data == nil { - return false - } - for _, item := range data.CoinList { - if item.ID != packageID { - continue - } - for _, method := range item.PayMethods { - if rechargeser.NormalizePayMode(method.Type) == payMode { - return true - } - } - return false - } - return false -} - -func safePaymentURL(raw string) string { - raw = strings.TrimSpace(raw) - u, err := url.Parse(raw) - if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { - return "" - } - return raw -} - -func queryInt(c *gin.Context, name string, fallback int) int { - v, err := strconv.Atoi(strings.TrimSpace(c.Query(name))) - if err != nil || v <= 0 { - return fallback - } - return v -} - -// queryIntAllowZero 允许 0(如 type=0 全部)。 -func queryIntAllowZero(c *gin.Context, name string, fallback int) int { - raw := strings.TrimSpace(c.Query(name)) - if raw == "" { - return fallback - } - v, err := strconv.Atoi(raw) - if err != nil { - return fallback - } - return v -} diff --git a/app/api/web/register.go b/app/api/web/register.go deleted file mode 100644 index ead157b..0000000 --- a/app/api/web/register.go +++ /dev/null @@ -1,11 +0,0 @@ -package web - -import "github.com/gin-gonic/gin" - -// RegisterPage H5 注册页:渲染 template/pages/register.html。 -// PC 端由页面内脚本回首页并打开 registerPop;H5 直接展示本页。 -func RegisterPage(c *gin.Context) { - HTMLOK(c, "register.html", gin.H{ - "title": "注册账号 - 黄果短剧", - }) -} diff --git a/app/api/web/render.go b/app/api/web/render.go index f39059a..9ccf5af 100644 --- a/app/api/web/render.go +++ b/app/api/web/render.go @@ -4,7 +4,6 @@ import ( "net/http" "strconv" "strings" - "videoweb/app/service/authser" "videoweb/app/service/configser" "videoweb/app/service/playletser" "videoweb/app/service/seoser" @@ -14,68 +13,12 @@ import ( "github.com/gin-gonic/gin" ) -// HTML 渲染页面并注入登录态(以 cookie 为准)。 -// 无 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。 +// HTML 渲染页面并注入公共导航、配置与 SEO 数据。 func HTML(c *gin.Context, code int, name string, data gin.H) { if data == nil { data = gin.H{} } - // 所有服务端页面先完成渠道首次归因;已有会话也不会漏记落地页渠道。 - if channel := authser.ResolveChannel(c, ""); channel != "" { - data["channel"] = channel - } - authser.SyncUserOnEnter(c) - - loggedIn, userName, avatar, email := authser.AuthFromCookies(c) - registered := authser.IsRegisteredFromCookies(c) - data["isLoggedIn"] = loggedIn - data["isRegistered"] = registered - // 已登录(含游客)展示用户菜单;未登录才展示「登录」按钮。 - if _, ok := data["guestHeader"]; !ok { - data["guestHeader"] = !loggedIn - } - if _, ok := data["isVip"]; !ok { - data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c) - } - isVip, _ := data["isVip"].(bool) - data["isVip"] = isVip - - if loggedIn { - if _, provided := data["userName"]; !provided && userName != "" { - data["userName"] = userName - } - if _, provided := data["avatar"]; !provided && avatar != "" { - data["avatar"] = avatar - } - if email != "" { - data["email"] = email - } - if _, ok := data["userId"]; !ok { - if uid := authser.UserIDFromCookies(c); uid > 0 { - data["userId"] = authser.FormatUserID(uid) - } - } - if _, ok := data["recharge"]; !ok { - recharge := authser.RechargeFromCookies(c) - data["recharge"] = recharge - data["rechargeText"] = authser.FormatRecharge(recharge) - } else if _, ok := data["rechargeText"]; !ok { - data["rechargeText"] = authser.FormatRecharge(toInt64(data["recharge"])) - } - if _, ok := data["vipType"]; !ok { - data["vipType"] = authser.VipTypeFromCookies(c) - } - } else { - delete(data, "userName") - delete(data, "avatar") - delete(data, "email") - delete(data, "userId") - delete(data, "recharge") - delete(data, "rechargeText") - delete(data, "vipType") - } - // 导航分类:GET /api/app/translation/category/list → categoryList(包含分类 seoInfo) if _, ok := data["categoryList"]; !ok { data["categoryList"] = translationser.FetchHeaderCategoryListOrDefault(c) @@ -119,9 +62,6 @@ func HTML(c *gin.Context, code int, name string, data gin.H) { } } - data["trackerAppId"] = configser.ResolveTrackerAppID() - attachTrackerPage(c.Request.URL.Path, code, data) - attachPageSEO(c, code, data) c.HTML(code, name, data) } diff --git a/app/api/web/setting.go b/app/api/web/setting.go deleted file mode 100644 index 9572866..0000000 --- a/app/api/web/setting.go +++ /dev/null @@ -1,85 +0,0 @@ -package web - -import ( - "net/http" - "strings" - "videoweb/app/service/authser" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -// Setting 展示账号设置,字段来自最新的用户信息接口。 -func Setting(c *gin.Context) { - data := gin.H{"title": "账号设置 - 黄果短剧", "navActive": "setting", "accountTab": "setting"} - loadAccountSummary(c, data, gin.H{}) - HTMLOK(c, "setting.html", data) -} - -// UpdateProfile 代理账号资料修改。可选字段必须显式传入,避免覆盖未修改内容。 -func UpdateProfile(c *gin.Context) { - if useragent.TokenFromRequest(c.Request) == "" { - c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "tip": "请先登录"}) - return - } - var body authser.UpdateProfileReq - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "参数错误"}) - return - } - if body.NickName != nil { - v := strings.TrimSpace(*body.NickName) - if v == "" || len([]rune(v)) > 40 { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "昵称须为 1 至 40 个字符"}) - return - } - body.NickName = &v - } - if body.Introduction != nil { - v := strings.TrimSpace(*body.Introduction) - if len([]rune(v)) > 300 { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "简介不能超过 300 个字符"}) - return - } - body.Introduction = &v - } - if body.Avatar != nil { - v := strings.TrimSpace(*body.Avatar) - if safeProfileImage(v) == "" || strings.HasPrefix(v, "/static/") { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "头像地址无效"}) - return - } - body.Avatar = &v - } - if (body.OldPassword == nil) != (body.NewPassword == nil) { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "请输入旧密码和新密码"}) - return - } - if body.OldPassword != nil && (*body.OldPassword == "" || *body.NewPassword == "") { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "密码不能为空"}) - return - } - if body.NickName == nil && body.Introduction == nil && body.Avatar == nil && body.OldPassword == nil && body.PrivacyMode == nil { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "tip": "没有需要保存的修改"}) - return - } - env, err := authser.UpdateProfile(c, body) - if err != nil { - log.Error("update profile failed", log.E(err)) - c.JSON(http.StatusBadGateway, gin.H{"code": 500, "tip": "保存失败,请稍后重试"}) - return - } - if env == nil || env.Code != 200 { - if env == nil { - c.JSON(http.StatusBadGateway, gin.H{"code": 500, "tip": "保存失败,请稍后重试"}) - return - } - c.JSON(http.StatusOK, gin.H{"code": env.Code, "tip": firstNonEmptyAuth(env.Tip, env.Msg, "保存失败")}) - return - } - if infoEnv, user, err := authser.GetInfo(c); err == nil && infoEnv != nil && infoEnv.Code == 200 && user != nil { - authser.SetAuthCookies(c, user) - } - c.JSON(http.StatusOK, gin.H{"code": 200, "tip": firstNonEmptyAuth(env.Tip, "保存成功")}) -} diff --git a/app/api/web/tracker_page.go b/app/api/web/tracker_page.go deleted file mode 100644 index c3c422b..0000000 --- a/app/api/web/tracker_page.go +++ /dev/null @@ -1,80 +0,0 @@ -package web - -import ( - "net/http" - "strings" -) - -// attachTrackerPage 为所有 HTML 页面补齐稳定的 pageKey/pageName,避免事件落入 unknown。 -func attachTrackerPage(path string, code int, data map[string]any) { - if data == nil { - return - } - key, name := trackerPageFromRequest(path, code) - if current, _ := data["pageKey"].(string); strings.TrimSpace(current) == "" { - data["pageKey"] = key - } - if current, _ := data["pageName"].(string); strings.TrimSpace(current) == "" { - data["pageName"] = name - } -} - -func trackerPageFromRequest(path string, code int) (string, string) { - if code >= http.StatusInternalServerError { - return "server_error", "服务异常页面" - } - if code >= http.StatusBadRequest { - return "not_found", "404页面" - } - return trackerPageFromPath(path) -} - -func trackerPageFromPath(path string) (string, string) { - p := strings.TrimSuffix(strings.TrimSpace(path), "/") - if p == "" { - return "home", "首页" - } - rules := []struct { - prefix string - key string - name string - }{ - {"/series-play", "series_play", "AI成人剧集播放页"}, - {"/series", "series", "AI成人剧集"}, - {"/play", "play", "短剧播放页"}, - {"/detail", "detail", "短剧详情"}, - {"/list", "list", "短剧列表"}, - {"/recently-added", "recently_added", "最新短剧"}, - {"/popular-recommend", "popular_recommend", "热门推荐"}, - {"/rank", "rank", "排行榜"}, - {"/topic-detail", "topic_detail", "专题详情"}, - {"/topic", "topic", "专题列表"}, - {"/tags", "tags", "标签详情"}, - {"/more-series-list", "more_series_list", "成人剧集列表"}, - {"/more-list", "more_list", "短剧扩展列表"}, - {"/search-mobile", "search", "搜索结果页"}, - {"/search", "search", "搜索结果页"}, - {"/history", "history", "观看历史"}, - {"/favorites", "favorites", "我的收藏"}, - {"/purchased", "purchased", "已购剧集"}, - {"/profile", "profile", "个人中心"}, - {"/setting", "setting", "设置"}, - {"/recharge", "recharge", "充值中心"}, - {"/download-app", "download_app", "下载APP"}, - {"/feedback", "feedback", "问题反馈"}, - {"/help", "help", "帮助中心"}, - {"/about", "about", "关于我们"}, - {"/terms", "terms", "用户协议"}, - {"/privacy", "privacy", "隐私政策"}, - {"/2257", "compliance_2257", "2257合规声明"}, - {"/dmca", "dmca", "DMCA版权声明"}, - {"/login", "login", "登录"}, - {"/register", "register", "注册"}, - } - for _, rule := range rules { - if p == rule.prefix || strings.HasPrefix(p, rule.prefix+"/") { - return rule.key, rule.name - } - } - return "internal_other", "站内其他页面" -} diff --git a/app/api/web/videoCollect.go b/app/api/web/videoCollect.go deleted file mode 100644 index b911572..0000000 --- a/app/api/web/videoCollect.go +++ /dev/null @@ -1,64 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/videoser" - - "github.com/gin-gonic/gin" -) - -// VideoCollectSet BFF:代理 POST /api/app/video/collect/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_collect_set -func VideoCollectSet(c *gin.Context) { - var body struct { - ID int64 `json:"id"` - IsCollect *bool `json:"isCollect"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ID <= 0 || body.IsCollect == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "id/isCollect required", - "tip": "缺少视频 ID 或收藏状态", - "data": nil, - }) - return - } - - env, err := videoser.CollectSet(c, videoser.VideoCollectSetReq{ - ID: body.ID, - IsCollect: *body.IsCollect, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/api/web/videoLike.go b/app/api/web/videoLike.go deleted file mode 100644 index 50226c0..0000000 --- a/app/api/web/videoLike.go +++ /dev/null @@ -1,64 +0,0 @@ -package web - -import ( - "net/http" - "videoweb/app/service/videoser" - - "github.com/gin-gonic/gin" -) - -// VideoLikeSet BFF:代理 POST /api/app/video/like/set。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_like_set -func VideoLikeSet(c *gin.Context) { - var body struct { - ID int64 `json:"id"` - IsLike *bool `json:"isLike"` - } - if err := c.ShouldBindJSON(&body); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "code": 400, - "msg": "invalid body", - "tip": "参数错误", - "data": nil, - }) - return - } - if body.ID <= 0 || body.IsLike == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 400, - "msg": "id/isLike required", - "tip": "缺少视频 ID 或点赞状态", - "data": nil, - }) - return - } - - env, err := videoser.LikeSet(c, videoser.VideoLikeSetReq{ - ID: body.ID, - IsLike: *body.IsLike, - }) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": err.Error(), - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - if env == nil { - c.JSON(http.StatusOK, gin.H{ - "code": 500, - "msg": "empty response", - "tip": "操作失败,请稍后重试", - "data": nil, - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "code": env.Code, - "msg": env.Msg, - "tip": env.Tip, - "data": env.Data, - }) -} diff --git a/app/appg/appg.go b/app/appg/appg.go index 7a7d43c..7a42961 100644 --- a/app/appg/appg.go +++ b/app/appg/appg.go @@ -1,9 +1,6 @@ package appg import ( - "strings" - "videoweb/pub/pubdef" - "github.com/patrickmn/go-cache" ) @@ -15,13 +12,12 @@ var ( ) type GlobalCfgEx struct { - ServerAddr string `yaml:"serverAddr"` - ListenAddr string `yaml:"listenAddr"` - DHListenAddr string `yaml:"dhListenAddr"` - RunMod string `yaml:"runMod"` - TsAuthKey string `yaml:"tsAuthKey"` - SEO SEOConf `yaml:"seo"` - Tracker TrackerConf `yaml:"tracker"` + ServerAddr string `yaml:"serverAddr"` + ListenAddr string `yaml:"listenAddr"` + DHListenAddr string `yaml:"dhListenAddr"` + RunMod string `yaml:"runMod"` + TsAuthKey string `yaml:"tsAuthKey"` + SEO SEOConf `yaml:"seo"` } // SEOConf 站长平台验证与 SEO 相关配置。 @@ -30,17 +26,3 @@ type SEOConf struct { BaiduHTMLFile string `yaml:"baiduHtmlFile"` // 如 baidu_verify_codeva-xxx.html IndexNowKey string `yaml:"indexNowKey"` // Bing IndexNow 密钥,提供 /{key}.txt } - -// TrackerConf 数据上报 SDK 本地兜底(上游 reportSdkConfig 优先)。 -type TrackerConf struct { - AppID string `yaml:"appId"` - EncryptedConfig string `yaml:"encryptedConfig"` -} - -// TrackerAppID 返回页面 SDK 与 BFF 透传校验共用的可信应用 ID。 -func TrackerAppID() string { - if id := strings.TrimSpace(Conf.Tracker.AppID); id != "" { - return id - } - return pubdef.TrackerAppId() -} diff --git a/app/conf/app.yaml b/app/conf/app.yaml index 47b019f..c9853f3 100644 --- a/app/conf/app.yaml +++ b/app/conf/app.yaml @@ -1,11 +1,5 @@ -serverAddr: "http://172.104.181.99:25319" +serverAddr: "https://webhg2api.mfcg.me" listenAddr: "25121" dhListenAddr: "26813" runMod: "dev" tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN" - -# 数据上报 SDK(web-sdk)。encryptedConfig 优先用上游 config/info.reportSdkConfig; -# 若上游暂未下发,可在此填写密文兜底。 -tracker: - appId: "JHA-1045" - encryptedConfig: "" diff --git a/app/router/gin.go b/app/router/gin.go index 9c55ece..876f290 100644 --- a/app/router/gin.go +++ b/app/router/gin.go @@ -147,24 +147,6 @@ func registerRoutes(r *gin.Engine) { r.GET("/search", web.Search) r.GET("/search/:q", web.Search) r.GET("/search/:q/page/:page", web.Search) - r.GET("/profile", web.Profile) - r.GET("/history", web.History) - r.GET("/history/page/:page", web.History) - r.POST("/api/playlet/history/clear", web.HistoryClear) - r.POST("/api/playlet/history/delete", web.HistoryDelete) - r.POST("/api/video/history/clear", web.VideoHistoryClear) - r.POST("/api/video/history/delete", web.VideoHistoryDelete) - r.GET("/favorites", web.Favorites) - r.GET("/favorites/page/:page", web.Favorites) - r.GET("/favorites/:filter", web.Favorites) - r.GET("/favorites/:filter/page/:page", web.Favorites) - r.GET("/purchased", web.PurchasedSeries) - r.GET("/purchased/page/:page", web.PurchasedSeries) - r.GET("/setting", web.Setting) - r.GET("/settings", func(c *gin.Context) { - c.Redirect(http.StatusMovedPermanently, "/setting") - }) - r.GET("/recharge", web.Recharge) r.GET("/about", web.About) r.GET("/help", web.Help) r.GET("/terms", web.Terms) @@ -219,38 +201,15 @@ func registerRoutes(r *gin.Engine) { r.GET("/play/:slug/:ep/recommendations/page/:guessPage", web.Play) r.GET("/series-play/:slug", web.SeriesPlay) r.GET("/series-play/:slug/recommendations/page/:guessPage", web.SeriesPlay) - r.GET("/login", web.LoginPage) - r.GET("/register", web.RegisterPage) r.GET("/feedback", web.Feedback) - // 认证 BFF:SSR 进页已自动游客;前端登录/注册/补游客会话走以下接口。 - r.POST("/api/auth/guest", web.Guest) - r.POST("/api/auth/login", web.Login) - r.POST("/api/auth/register", web.Register) - r.POST("/api/auth/logout", web.Logout) - r.POST("/api/auth/profile", web.UpdateProfile) - r.POST("/api/recharge/coin/buy", web.RechargeCoinBuy) - r.POST("/api/coin/buy", web.RechargeCoinBuy) - r.GET("/api/recharge/transactions", web.RechargeTransactions) - r.GET("/api/coin/trans/list", web.RechargeTransactions) - r.GET("/api/coin/trans/export", web.RechargeCoinTransExport) - r.GET("/api/recharge/transactions/export", web.RechargeCoinTransExport) + // 意见反馈(含截图上传) r.POST("/api/upload/image", web.UploadImage) r.POST("/api/feedback/add", web.FeedbackAdd) // 播放页 BFF r.GET("/api/playlet/guess-like/list", web.GuessLikeList) r.GET("/api/comment/list", web.CommentList) - r.POST("/api/comment/send", web.CommentSend) - r.POST("/api/comment/like/set", web.CommentLikeSet) - r.POST("/api/playlet/chapter/like/set", web.ChapterLikeSet) - r.POST("/api/playlet/chapter/collect/set", web.ChapterCollectSet) - r.POST("/api/playlet/chapter/watch/report", web.ChapterWatchReport) - r.POST("/api/playlet/chapter/unlock", web.ChapterUnlock) - r.POST("/api/playlet/like/set", web.PlayletLikeSet) - r.POST("/api/playlet/collect/set", web.PlayletCollectSet) - r.POST("/api/video/like/set", web.VideoLikeSet) - r.POST("/api/video/collect/set", web.VideoCollectSet) r.GET("/api/web/search/suggest", web.SearchSuggest) // 搜索引擎抓取、发现与站点所有权验证。 diff --git a/app/service/authser/auth.go b/app/service/authser/auth.go deleted file mode 100644 index 22b5d46..0000000 --- a/app/service/authser/auth.go +++ /dev/null @@ -1,290 +0,0 @@ -package authser - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - "time" - "videoweb/app/service/upstream" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -const ( - registerPath = "/api/app/auth/register" - loginPath = "/api/app/auth/login" - resetPath = "/api/app/auth/resetPassword" - guestPath = "/api/app/auth/guest" - getInfoPath = "/api/app/auth/getInfo" - profilePath = "/api/app/auth/profile" - uploadPath = "/api/app/upload/image" - cookieMaxAge = 7 * 24 * 3600 -) - -// Register 调用上游注册接口。 -func Register(c *gin.Context, req RegisterReq) (*APIEnvelope, *LoginResp, error) { - var env APIEnvelope - if _, err := upstream.PostJSON(c, registerPath, req, &env); err != nil { - return nil, nil, err - } - if env.Code != 200 { - return &env, nil, nil - } - user, err := decodeLoginData(env.Data) - if err != nil { - return &env, nil, err - } - return &env, user, nil -} - -// ResetPassword 通过账号和邮箱校验后重置密码。 -func ResetPassword(c *gin.Context, req ResetPasswordReq) (*APIEnvelope, error) { - var env APIEnvelope - if _, err := upstream.PostJSON(c, resetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// Login 调用上游登录接口。 -func Login(c *gin.Context, req LoginReq) (*APIEnvelope, *LoginResp, error) { - var env APIEnvelope - if _, err := upstream.PostJSON(c, loginPath, req, &env); err != nil { - return nil, nil, err - } - if env.Code != 200 { - return &env, nil, nil - } - user, err := decodeLoginData(env.Data) - if err != nil { - return &env, nil, err - } - return &env, user, nil -} - -// GuestLogin 调用上游游客登录(按设备 ID 创建或找回游客账号)。 -func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, error) { - var env APIEnvelope - if _, err := upstream.PostJSON(c, guestPath, req, &env); err != nil { - return nil, nil, err - } - if env.Code != 200 { - return &env, nil, nil - } - user, err := decodeLoginData(env.Data) - if err != nil { - return &env, nil, err - } - return &env, user, nil -} - -// GetInfo 通过 token 拉取最新用户信息。 -func GetInfo(c *gin.Context) (*APIEnvelope, *LoginResp, error) { - var env APIEnvelope - if err := upstream.GetJSON(c, getInfoPath, &env); err != nil { - return nil, nil, err - } - if env.Code != 200 { - return &env, nil, nil - } - user, err := decodeLoginData(env.Data) - if err != nil { - return &env, nil, err - } - return &env, user, nil -} - -// UpdateProfile 修改资料(昵称 / 头像等)。 -func UpdateProfile(c *gin.Context, req UpdateProfileReq) (*APIEnvelope, error) { - var env APIEnvelope - if _, err := upstream.PostJSON(c, profilePath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// UploadImage 上传图片;头像场景不加水印。 -func UploadImage(c *gin.Context, fileName string, r io.Reader) (*ImageUploadResp, error) { - var resp ImageUploadResp - _, err := upstream.PostMultipart(c, uploadPath, map[string]string{ - "watermark": "0", - }, &upstream.MultipartFile{ - FieldName: "file", - FileName: fileName, - Reader: r, - }, &resp) - if err != nil { - return nil, err - } - return &resp, nil -} - -func decodeLoginData(raw json.RawMessage) (*LoginResp, error) { - if len(raw) == 0 || string(raw) == "null" || string(raw) == `""` { - return nil, fmt.Errorf("empty login data") - } - var user LoginResp - if err := json.Unmarshal(raw, &user); err != nil { - return nil, err - } - return &user, nil -} - -// SetAuthCookies 只持久化 SSR 与前端登录态实际依赖的字段。 -// 个人简介、邮箱、邀请码、展示卡信息由 getInfo 即时提供,不再随每个页面响应重复写入; -// sid 已存在于请求时也无需原样回写。ClearAuthCookies 仍保留这些历史键用于兼容清理。 -func SetAuthCookies(c *gin.Context, user *LoginResp) { - if c == nil || user == nil { - return - } - secure := c.Request != nil && c.Request.TLS != nil - if strings.TrimSpace(user.Token) != "" { - setCookie(c, "token", user.Token, secure) - } - setCookie(c, "userId", strconv.FormatInt(user.UserID, 10), secure) - setCookie(c, "accountNo", user.AccountNo, secure) - setCookie(c, "nickName", user.NickName, secure) - setCookie(c, "avatar", user.Avatar, secure) - setCookie(c, "recharge", strconv.FormatInt(user.Recharge, 10), secure) - setCookie(c, "vipType", strconv.Itoa(user.VipType), secure) - setCookie(c, "currentVipExpire", user.CurrentVipExpire, secure) - setCookie(c, "userType", strconv.Itoa(user.Type), secure) - isReg := "0" - if user.IsRegister || user.Type == 1 { - isReg = "1" - } - setCookie(c, "isRegister", isReg, secure) - // 后续 SSR 服务必须能立即读取本次刚建立/刷新的会话;同时记录为本请求的 - // 权威结果,避免 HTML 渲染阶段再次调用 getInfo 并重复写整套 Cookie。 - patchAuthCookiesToRequest(c, user) - rememberRequestAuthUser(c, user) -} - -func setCookie(c *gin.Context, name, value string, secure bool) { - // Cookie Value 仅允许 ASCII;中文昵称等需 URL 编码。 - // 使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致; - // 避免 QueryEscape 的空格→+,否则 JS decodeURIComponent 会留下字面量 "+"。 - writeCookie(c, &http.Cookie{ - Name: name, - Value: url.PathEscape(value), - Path: "/", - MaxAge: cookieMaxAge, - HttpOnly: false, - Secure: secure, - SameSite: http.SameSiteLaxMode, - Expires: time.Now().Add(7 * 24 * time.Hour), - }) -} - -// writeCookie 对同一响应中同名、同 Path、同 Domain 的 Cookie 做 upsert,避免会话恢复的 -// 「先清除、再重建」产生两套响应头,同时保留其他作用域下可能存在的同名 Cookie。 -func writeCookie(c *gin.Context, cookie *http.Cookie) { - if c == nil || c.Writer == nil || cookie == nil || cookie.Name == "" { - return - } - header := c.Writer.Header() - existing := header.Values("Set-Cookie") - if len(existing) > 0 { - header.Del("Set-Cookie") - for _, raw := range existing { - if sameCookieScope(raw, cookie) { - continue - } - header.Add("Set-Cookie", raw) - } - } - http.SetCookie(c.Writer, cookie) -} - -func sameCookieScope(raw string, target *http.Cookie) bool { - response := &http.Response{Header: http.Header{"Set-Cookie": []string{raw}}} - for _, existing := range response.Cookies() { - if existing.Name == target.Name && existing.Path == target.Path && strings.EqualFold(existing.Domain, target.Domain) { - return true - } - } - return false -} - -func cookieValue(c *gin.Context, name string) string { - ck, err := c.Request.Cookie(name) - if err != nil || ck == nil { - return "" - } - raw := strings.TrimSpace(ck.Value) - if raw == "" { - return "" - } - // QueryUnescape 同时兼容历史 QueryEscape(+)与 PathEscape(%20) - if decoded, err := url.QueryUnescape(raw); err == nil { - return strings.TrimSpace(decoded) - } - return raw -} - -// ClearAuthCookies 清除登录相关 cookie。 -func ClearAuthCookies(c *gin.Context) { - forgetRequestAuthUser(c) - for _, name := range authCookieNames() { - writeCookie(c, &http.Cookie{ - Name: name, - Value: "", - Path: "/", - MaxAge: -1, - HttpOnly: false, - SameSite: http.SameSiteLaxMode, - }) - } -} - -func authCookieNames() []string { - return []string{ - "token", "_token_", "sid", "_s_token_", - "userId", "accountNo", "nickName", "introduction", "avatar", - "email", "inviteCode", "recharge", "vipType", "currentVipExpire", "showCardId", "showCardName", - "userType", "isRegister", - } -} - -// AuthFromCookies 从 cookie 读取登录态,供 SSR 头部 / 个人中心展示。 -func AuthFromCookies(c *gin.Context) (loggedIn bool, userName, avatar, email string) { - token := useragent.TokenFromRequest(c.Request) - if token == "" { - return false, "", "", "" - } - userName = cookieValue(c, "nickName") - if userName == "" { - userName = cookieValue(c, "accountNo") - } - avatar = usableAvatarURL(cookieValue(c, "avatar")) - email = cookieValue(c, "email") - return true, userName, avatar, email -} - -// IsRegisteredFromCookies 是否正式注册账号(非游客)。 -func IsRegisteredFromCookies(c *gin.Context) bool { - if useragent.TokenFromRequest(c.Request) == "" { - return false - } - if cookieValue(c, "isRegister") == "1" { - return true - } - return cookieValue(c, "userType") == "1" -} - -func usableAvatarURL(raw string) string { - raw = strings.TrimSpace(raw) - if raw == "" { - return "" - } - if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") || strings.HasPrefix(raw, "/static/") { - return raw - } - // 无域名的相对路径暂不可用(解密拉取需要完整 URL) - return "" -} diff --git a/app/service/authser/guest_enter.go b/app/service/authser/guest_enter.go deleted file mode 100644 index 589a8c6..0000000 --- a/app/service/authser/guest_enter.go +++ /dev/null @@ -1,314 +0,0 @@ -package authser - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "net/http" - "net/url" - "strconv" - "strings" - "time" - "videoweb/pub/common/log" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -const guestDevIDCookie = "guest_dev_id" -const guestSkipAutoCookie = "guest_skip_auto" - -// EnsureGuestOnEnter 页面进入时:无 token(未自己登录)则走游客登录并写 cookie。 -// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 读到登录态。 -// 手动退出后带 guest_skip_auto 时跳过本次;清掉标记后,下次进页再自动游客。 -func EnsureGuestOnEnter(c *gin.Context) *LoginResp { - if c == nil || c.Request == nil { - return nil - } - if user := freshRequestAuthUser(c); user != nil { - return user - } - if requestAutoAuthSuppressed(c) { - return nil - } - // 已有任意会话(游客或正式账号)则跳过 - if useragent.TokenFromRequest(c.Request) != "" { - return nil - } - // 手动退出:本次进页不自动游客,并清除标记,供下次进页恢复 - if IsGuestSkipAuto(c) { - ClearGuestSkipAuto(c) - dropRequestCookie(c, guestSkipAutoCookie) - suppressRequestAutoAuth(c) - return nil - } - - devID := ensureGuestDevID(c) - env, user, err := GuestLogin(c, GuestLoginReq{ - DevID: devID, - DeviceID: devID, - Channel: guestChannel(c), - }) - if err != nil { - log.Error("ensure guest on enter failed", log.E(err)) - return nil - } - if env == nil || env.Code != 200 || user == nil || strings.TrimSpace(user.Token) == "" { - tip := "" - if env != nil { - tip = firstNonEmptyTip(env.Tip, env.Msg) - } - log.Error("ensure guest on enter rejected", log.E(fmt.Errorf("code=%v tip=%s", envCode(env), tip))) - return nil - } - - SetAuthCookies(c, user) - return user -} - -// SyncUserOnEnter 是 SSR 页面统一的会话同步入口。同一请求已经通过游客登录、 -// getInfo 或其他认证流程取得过最新用户时直接复用,避免再次访问上游和重复写 Cookie。 -func SyncUserOnEnter(c *gin.Context) *LoginResp { - if c == nil || c.Request == nil { - return nil - } - if user := freshRequestAuthUser(c); user != nil { - return user - } - if requestAutoAuthSuppressed(c) { - return nil - } - if useragent.TokenFromRequest(c.Request) == "" { - return EnsureGuestOnEnter(c) - } - return RefreshUserOnEnter(c) -} - -// RefreshUserOnEnter 页面进入时用 GET /api/app/auth/getInfo 刷新用户信息并写回 cookie。 -// 保证刷新后头部昵称、头像、金币等与上游一致(如解锁后余额变化)。 -func RefreshUserOnEnter(c *gin.Context) *LoginResp { - if c == nil || c.Request == nil { - return nil - } - if user := freshRequestAuthUser(c); user != nil { - return user - } - if requestAutoAuthSuppressed(c) { - return nil - } - token := useragent.TokenFromRequest(c.Request) - if token == "" { - return nil - } - - env, user, err := GetInfo(c) - if err != nil { - log.Error("refresh user on enter failed", log.E(err)) - return nil - } - if env != nil && IsSessionExpiredCode(env.Code) { - if retry, _ := OnSessionExpired(c, env.Code); retry { - // 游客重建已经返回完整用户并写好 Cookie,无需立刻再 getInfo 一次。 - if fresh := freshRequestAuthUser(c); fresh != nil { - return fresh - } - env, user, err = GetInfo(c) - if err != nil { - log.Error("refresh user on enter retry failed", log.E(err)) - return nil - } - } else { - return nil - } - } - if env == nil || env.Code != 200 || user == nil { - return nil - } - if strings.TrimSpace(user.Token) == "" { - user.Token = useragent.TokenFromRequest(c.Request) - if user.Token == "" { - user.Token = token - } - } - SetAuthCookies(c, user) - return user -} - -// RefreshGuestSession 游客会话失效时(如上游 6001 用户不存在)重新游客登录。 -// 正式注册账号不处理,避免把用户踢成游客。 -func RefreshGuestSession(c *gin.Context) *LoginResp { - if c == nil || c.Request == nil { - return nil - } - if IsRegisteredFromCookies(c) { - return nil - } - forgetRequestAuthUser(c) - dropRequestCookie(c, "token") - dropRequestCookie(c, "_token_") - return EnsureGuestOnEnter(c) -} - -func dropRequestCookie(c *gin.Context, name string) { - if c == nil || c.Request == nil || name == "" { - return - } - var parts []string - for _, ck := range c.Request.Cookies() { - if ck == nil || ck.Name == name { - continue - } - parts = append(parts, ck.Name+"="+ck.Value) - } - c.Request.Header.Set("Cookie", strings.Join(parts, "; ")) -} - -// MarkGuestSkipAuto 手动退出后标记:跳过紧接着的一次自动游客登录。 -func MarkGuestSkipAuto(c *gin.Context) { - if c == nil { - return - } - suppressRequestAutoAuth(c) - secure := c.Request != nil && c.Request.TLS != nil - writeCookie(c, &http.Cookie{ - Name: guestSkipAutoCookie, - Value: "1", - Path: "/", - MaxAge: 0, // session cookie - HttpOnly: false, - Secure: secure, - SameSite: http.SameSiteLaxMode, - }) -} - -// IsGuestSkipAuto 是否处于「手动退出后跳过自动游客」状态。 -func IsGuestSkipAuto(c *gin.Context) bool { - return cookieValue(c, guestSkipAutoCookie) == "1" -} - -// ClearGuestSkipAuto 清除手动退出跳过标记。 -func ClearGuestSkipAuto(c *gin.Context) { - if c == nil { - return - } - writeCookie(c, &http.Cookie{ - Name: guestSkipAutoCookie, - Value: "", - Path: "/", - MaxAge: -1, - HttpOnly: false, - SameSite: http.SameSiteLaxMode, - }) -} - -func ensureGuestDevID(c *gin.Context) string { - if id := cookieValue(c, guestDevIDCookie); id != "" { - return id - } - id := newGuestDevID() - secure := c.Request != nil && c.Request.TLS != nil - setCookie(c, guestDevIDCookie, id, secure) - patchRequestCookie(c, guestDevIDCookie, id) - return id -} - -func newGuestDevID() string { - b := make([]byte, 8) - if _, err := rand.Read(b); err != nil { - now := time.Now() - return fmt.Sprintf("%016X%d", uint64(now.UnixNano()), now.UnixMilli()) - } - return strings.ToUpper(hex.EncodeToString(b)) + strconv.FormatInt(time.Now().UnixMilli(), 10) -} - -// ResolveChannel 统一解析渠道码并按首次归因落盘。 -// 优先级:已有 channel cookie > 调用方传入值 > dc/ch/pc 查询参数;已有渠道不覆盖。 -func ResolveChannel(c *gin.Context, candidate string) string { - channel := strings.TrimSpace(candidate) - if c == nil || c.Request == nil { - return channel - } - if stored := cookieValue(c, "channel"); stored != "" { - return stored - } - if channel == "" { - for _, key := range []string{"dc", "ch", "pc"} { - if value := strings.TrimSpace(c.Query(key)); value != "" { - channel = value - break - } - } - } - if channel == "" { - return "" - } - - secure := c.Request.TLS != nil - setCookie(c, "channel", channel, secure) - patchRequestCookie(c, "channel", channel) - return channel -} - -// guestChannel 为首屏服务端自动游客登录补渠道;cookie 用于复用前端首次归因结果。 -func guestChannel(c *gin.Context) string { - return ResolveChannel(c, "") -} - -func patchAuthCookiesToRequest(c *gin.Context, user *LoginResp) { - if c == nil || user == nil { - return - } - if strings.TrimSpace(user.Token) != "" { - patchRequestCookie(c, "token", user.Token) - } - patchRequestCookie(c, "userId", strconv.FormatInt(user.UserID, 10)) - patchRequestCookie(c, "accountNo", user.AccountNo) - patchRequestCookie(c, "nickName", user.NickName) - patchRequestCookie(c, "introduction", user.Introduction) - patchRequestCookie(c, "avatar", user.Avatar) - patchRequestCookie(c, "email", user.Email) - patchRequestCookie(c, "inviteCode", user.InviteCode) - patchRequestCookie(c, "recharge", strconv.FormatInt(user.Recharge, 10)) - patchRequestCookie(c, "vipType", strconv.Itoa(user.VipType)) - patchRequestCookie(c, "currentVipExpire", user.CurrentVipExpire) - patchRequestCookie(c, "showCardId", strconv.FormatInt(user.ShowCardID, 10)) - patchRequestCookie(c, "showCardName", user.ShowCardName) - patchRequestCookie(c, "userType", strconv.Itoa(user.Type)) - isReg := "0" - if user.IsRegister || user.Type == 1 { - isReg = "1" - } - patchRequestCookie(c, "isRegister", isReg) -} - -func patchRequestCookie(c *gin.Context, name, value string) { - if c == nil || c.Request == nil || name == "" { - return - } - escaped := url.PathEscape(value) - var parts []string - for _, ck := range c.Request.Cookies() { - if ck == nil || ck.Name == name { - continue - } - parts = append(parts, ck.Name+"="+ck.Value) - } - parts = append(parts, name+"="+escaped) - c.Request.Header.Set("Cookie", strings.Join(parts, "; ")) -} - -func envCode(env *APIEnvelope) int { - if env == nil { - return 0 - } - return env.Code -} - -func firstNonEmptyTip(values ...string) string { - for _, v := range values { - if strings.TrimSpace(v) != "" { - return v - } - } - return "" -} diff --git a/app/service/authser/guest_enter_test.go b/app/service/authser/guest_enter_test.go deleted file mode 100644 index 4dc7bca..0000000 --- a/app/service/authser/guest_enter_test.go +++ /dev/null @@ -1,345 +0,0 @@ -package authser - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "sync/atomic" - "testing" - "videoweb/app/appg" - "videoweb/pub/common/useragent" - - "github.com/gin-gonic/gin" -) - -var authCookieNamesWritten = []string{ - "token", - "userId", - "accountNo", - "nickName", - "avatar", - "recharge", - "vipType", - "currentVipExpire", - "userType", - "isRegister", -} - -func TestSyncUserOnEnterCreatesGuestWithoutRefreshingIt(t *testing.T) { - var guestCalls atomic.Int32 - var getInfoCalls atomic.Int32 - - useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case guestPath: - guestCalls.Add(1) - if r.Method != http.MethodPost { - t.Errorf("guest method = %s, want POST", r.Method) - } - var request GuestLoginReq - if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - t.Errorf("decode guest request: %v", err) - } - if request.DevID == "" || request.DeviceID != request.DevID { - t.Errorf("guest device ids = %#v, want matching non-empty ids", request) - } - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("guest-token", "new-guest")) - case getInfoPath: - getInfoCalls.Add(1) - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("guest-token", "unexpected-refresh")) - default: - t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path) - http.NotFound(w, r) - } - })) - - c, recorder := newAuthTestContext() - user := SyncUserOnEnter(c) - - if user == nil || user.Token != "guest-token" { - t.Fatalf("SyncUserOnEnter user = %#v, want guest-token", user) - } - if got := guestCalls.Load(); got != 1 { - t.Errorf("guest calls = %d, want 1", got) - } - if got := getInfoCalls.Load(); got != 0 { - t.Errorf("getInfo calls = %d, want 0", got) - } - assertRequestToken(t, c, "guest-token") - assertSingleAuthCookieSet(t, recorder) -} - -func TestSyncUserOnEnterRefreshesExistingTokenOnlyOnce(t *testing.T) { - var guestCalls atomic.Int32 - var getInfoCalls atomic.Int32 - authorization := make(chan string, 1) - - useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case guestPath: - guestCalls.Add(1) - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("unexpected-guest-token", "unexpected-guest")) - case getInfoPath: - getInfoCalls.Add(1) - select { - case authorization <- r.Header.Get("Authorization"): - default: - } - // getInfo may omit token; the existing request token must be preserved. - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("", "fresh-user")) - default: - t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path) - http.NotFound(w, r) - } - })) - - c, recorder := newAuthTestContext( - &http.Cookie{Name: "token", Value: "existing-token"}, - &http.Cookie{Name: "sid", Value: "existing-sid"}, - ) - first := SyncUserOnEnter(c) - second := SyncUserOnEnter(c) - - if first == nil || first.Token != "existing-token" || first.NickName != "fresh-user" { - t.Fatalf("first SyncUserOnEnter user = %#v, want refreshed user with existing token", first) - } - if second == nil || second.Token != "existing-token" || second.NickName != "fresh-user" { - t.Fatalf("second SyncUserOnEnter user = %#v, want memoized refreshed user", second) - } - if got := guestCalls.Load(); got != 0 { - t.Errorf("guest calls = %d, want 0", got) - } - if got := getInfoCalls.Load(); got != 1 { - t.Errorf("getInfo calls = %d, want 1", got) - } - select { - case got := <-authorization: - if got != "existing-token" { - t.Errorf("Authorization = %q, want existing-token", got) - } - default: - t.Error("getInfo did not capture Authorization header") - } - assertRequestToken(t, c, "existing-token") - if got := cookieValue(c, "nickName"); got != "fresh-user" { - t.Errorf("request nickName cookie = %q, want fresh-user", got) - } - assertSingleAuthCookieSet(t, recorder) -} - -func TestSyncUserOnEnterReusesGuestCreatedBeforeRender(t *testing.T) { - var guestCalls atomic.Int32 - var getInfoCalls atomic.Int32 - - useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case guestPath: - guestCalls.Add(1) - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("pre-render-token", "pre-render-guest")) - case getInfoPath: - getInfoCalls.Add(1) - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("pre-render-token", "unexpected-refresh")) - default: - t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path) - http.NotFound(w, r) - } - })) - - c, recorder := newAuthTestContext() - guest := EnsureGuestOnEnter(c) - user := SyncUserOnEnter(c) - - if guest == nil || guest.Token != "pre-render-token" { - t.Fatalf("EnsureGuestOnEnter user = %#v, want pre-render guest", guest) - } - if user == nil || user.Token != "pre-render-token" || user.NickName != "pre-render-guest" { - t.Fatalf("SyncUserOnEnter user = %#v, want memoized pre-render guest", user) - } - if got := guestCalls.Load(); got != 1 { - t.Errorf("guest calls = %d, want 1", got) - } - if got := getInfoCalls.Load(); got != 0 { - t.Errorf("getInfo calls = %d, want 0", got) - } - assertRequestToken(t, c, "pre-render-token") - assertSingleAuthCookieSet(t, recorder) -} - -func TestRefreshUserOnEnterReturnsRebuiltGuestWithoutRefreshingItAgain(t *testing.T) { - var guestCalls atomic.Int32 - var getInfoCalls atomic.Int32 - getInfoAuthorization := make(chan string, 1) - guestAuthorization := make(chan string, 1) - - useAuthTestUpstream(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case getInfoPath: - getInfoCalls.Add(1) - select { - case getInfoAuthorization <- r.Header.Get("Authorization"): - default: - } - writeAuthTestEnvelope(w, http.StatusOK, 6001, nil) - case guestPath: - guestCalls.Add(1) - select { - case guestAuthorization <- r.Header.Get("Authorization"): - default: - } - writeAuthTestEnvelope(w, http.StatusOK, 200, testLoginUser("rebuilt-guest-token", "rebuilt-guest")) - default: - t.Errorf("unexpected upstream request: %s %s", r.Method, r.URL.Path) - http.NotFound(w, r) - } - })) - - c, recorder := newAuthTestContext( - &http.Cookie{Name: "token", Value: "expired-guest-token"}, - &http.Cookie{Name: "isRegister", Value: "0"}, - &http.Cookie{Name: "userType", Value: "0"}, - ) - user := RefreshUserOnEnter(c) - // The rebuilt guest must also be reusable by the page-level sync call. - synced := SyncUserOnEnter(c) - - if user == nil || user.Token != "rebuilt-guest-token" || user.NickName != "rebuilt-guest" { - t.Fatalf("RefreshUserOnEnter user = %#v, want rebuilt guest", user) - } - if synced == nil || synced.Token != "rebuilt-guest-token" { - t.Fatalf("SyncUserOnEnter user = %#v, want memoized rebuilt guest", synced) - } - if got := getInfoCalls.Load(); got != 1 { - t.Errorf("getInfo calls = %d, want 1 (no refresh of rebuilt guest)", got) - } - if got := guestCalls.Load(); got != 1 { - t.Errorf("guest calls = %d, want 1", got) - } - select { - case got := <-getInfoAuthorization: - if got != "expired-guest-token" { - t.Errorf("getInfo Authorization = %q, want expired-guest-token", got) - } - default: - t.Error("getInfo did not capture Authorization header") - } - select { - case got := <-guestAuthorization: - if got != "" { - t.Errorf("guest Authorization = %q, want empty after expired token is removed", got) - } - default: - t.Error("guest login did not capture Authorization header") - } - assertRequestToken(t, c, "rebuilt-guest-token") - assertSingleAuthCookieSet(t, recorder) -} - -func TestSetAuthCookiesUpsertsSameNameWithinResponse(t *testing.T) { - c, recorder := newAuthTestContext() - SetAuthCookies(c, testLoginUser("first-token", "first-user")) - SetAuthCookies(c, testLoginUser("latest-token", "latest-user")) - - assertSingleAuthCookieSet(t, recorder) - assertRequestToken(t, c, "latest-token") - if got := cookieValue(c, "nickName"); got != "latest-user" { - t.Errorf("request nickName cookie = %q, want latest-user", got) - } - for _, cookie := range recorder.Result().Cookies() { - if cookie.Name == "token" && cookie.Value != "latest-token" { - t.Errorf("response token cookie = %q, want latest-token", cookie.Value) - } - } -} - -func useAuthTestUpstream(t *testing.T, handler http.Handler) { - t.Helper() - server := httptest.NewServer(handler) - previousServerAddr := appg.Conf.ServerAddr - appg.Conf.ServerAddr = server.URL - t.Cleanup(func() { - appg.Conf.ServerAddr = previousServerAddr - server.Close() - }) -} - -func newAuthTestContext(cookies ...*http.Cookie) (*gin.Context, *httptest.ResponseRecorder) { - recorder := httptest.NewRecorder() - c, _ := gin.CreateTestContext(recorder) - c.Request = httptest.NewRequest(http.MethodGet, "https://site.test/page", nil) - for _, cookie := range cookies { - c.Request.AddCookie(cookie) - } - return c, recorder -} - -func writeAuthTestEnvelope(w http.ResponseWriter, status, code int, data *LoginResp) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(struct { - Code int `json:"code"` - Msg string `json:"msg"` - Data *LoginResp `json:"data"` - }{ - Code: code, - Msg: http.StatusText(status), - Data: data, - }) -} - -func testLoginUser(token, nickName string) *LoginResp { - return &LoginResp{ - Token: token, - UserID: 101, - AccountNo: "account-101", - Email: "user101@example.test", - NickName: nickName, - // This field is intentionally large: it must stay out of Set-Cookie headers. - Introduction: strings.Repeat("简介", 150), - Avatar: "https://cdn.example.test/avatar.png", - InviteCode: "INVITE101", - VipType: 1, - CurrentVipExpire: "2099-01-01T00:00:00Z", - ShowCardID: 7, - ShowCardName: "monthly", - Type: 0, - Recharge: 88, - } -} - -func assertRequestToken(t *testing.T, c *gin.Context, want string) { - t.Helper() - if got := useragent.TokenFromRequest(c.Request); got != want { - t.Errorf("request token = %q, want %q", got, want) - } -} - -func assertSingleAuthCookieSet(t *testing.T, recorder *httptest.ResponseRecorder) { - t.Helper() - counts := make(map[string]int) - for _, cookie := range recorder.Result().Cookies() { - counts[cookie.Name]++ - } - for name, count := range counts { - if count > 1 { - t.Errorf("Set-Cookie %q written %d times, want at most once", name, count) - } - } - for _, name := range authCookieNamesWritten { - if got := counts[name]; got != 1 { - t.Errorf("auth Set-Cookie %q written %d times, want exactly once", name, got) - } - } - for _, name := range []string{"introduction", "email", "inviteCode", "showCardId", "showCardName", "sid"} { - if got := counts[name]; got != 0 { - t.Errorf("non-session Set-Cookie %q written %d times, want none", name, got) - } - } - var headerBytes int - for _, raw := range recorder.Header().Values("Set-Cookie") { - headerBytes += len("Set-Cookie: ") + len(raw) + len("\r\n") - } - if headerBytes >= 4096 { - t.Errorf("Set-Cookie response headers = %d bytes, want below 4096", headerBytes) - } -} diff --git a/app/service/authser/request_state.go b/app/service/authser/request_state.go deleted file mode 100644 index a693ee3..0000000 --- a/app/service/authser/request_state.go +++ /dev/null @@ -1,67 +0,0 @@ -package authser - -import "github.com/gin-gonic/gin" - -const requestAuthStateKey = "authser.request-auth-state" - -// requestAuthState 只在单个 Gin 请求内有效,用来避免多个 SSR 服务重复刷新同一会话。 -// freshUser 表示本请求已经从上游取得并写回过的最新用户;suppressAutoAuth -// 表示本请求明确不应再自动创建游客(例如用户刚退出或正式账号已过期)。 -type requestAuthState struct { - freshUser *LoginResp - suppressAutoAuth bool -} - -func authState(c *gin.Context) *requestAuthState { - if c == nil { - return nil - } - if value, ok := c.Get(requestAuthStateKey); ok { - if state, ok := value.(*requestAuthState); ok && state != nil { - return state - } - } - state := &requestAuthState{} - c.Set(requestAuthStateKey, state) - return state -} - -func freshRequestAuthUser(c *gin.Context) *LoginResp { - state := authState(c) - if state == nil { - return nil - } - return state.freshUser -} - -func rememberRequestAuthUser(c *gin.Context, user *LoginResp) { - state := authState(c) - if state == nil { - return - } - state.freshUser = user - state.suppressAutoAuth = false -} - -func forgetRequestAuthUser(c *gin.Context) { - if state := authState(c); state != nil { - state.freshUser = nil - } -} - -func suppressRequestAutoAuth(c *gin.Context) { - if state := authState(c); state != nil { - state.suppressAutoAuth = true - } -} - -func allowRequestAutoAuth(c *gin.Context) { - if state := authState(c); state != nil { - state.suppressAutoAuth = false - } -} - -func requestAutoAuthSuppressed(c *gin.Context) bool { - state := authState(c) - return state != nil && state.suppressAutoAuth -} diff --git a/app/service/authser/session.go b/app/service/authser/session.go deleted file mode 100644 index b373447..0000000 --- a/app/service/authser/session.go +++ /dev/null @@ -1,58 +0,0 @@ -package authser - -import "github.com/gin-gonic/gin" - -// TokenExpiredCode 上游约定:token 失效,需重新登录。 -const TokenExpiredCode = 1000 - -// IsSessionExpiredCode 是否为会话失效类业务码。 -func IsSessionExpiredCode(code int) bool { - switch code { - case TokenExpiredCode, 1002, 1012, 6001: // InvalidToken / NoToken / InvalidateToken / 用户不存在 - return true - default: - return false - } -} - -// OnSessionExpired 处理上游会话失效。 -// retry=true:游客已重新登录,调用方应重试原请求; -// needLogin=true:注册账号已清 cookie,调用方应跳转登录弹窗/页。 -func OnSessionExpired(c *gin.Context, code int) (retry, needLogin bool) { - if c == nil || !IsSessionExpiredCode(code) { - return false, false - } - if IsRegisteredFromCookies(c) { - ClearAuthCookies(c) - dropRequestCookie(c, "token") - dropRequestCookie(c, "_token_") - // 正式账号失效后,本请求保持退出态,避免后续 HTML 渲染又静默转成游客。 - suppressRequestAutoAuth(c) - return false, true - } - if RefreshGuestSession(c) != nil { - return true, false - } - ClearAuthCookies(c) - dropRequestCookie(c, "token") - dropRequestCookie(c, "_token_") - return false, false -} - -// RecoverContentSession 用于允许游客访问的内容读取接口。 -// 正式账号或游客 token 失效后,清理旧登录态、重新建立游客会话,调用方随后重试一次原请求。 -// 写操作仍应使用 OnSessionExpired,避免把用户操作静默降级为游客操作。 -func RecoverContentSession(c *gin.Context, code int) bool { - if c == nil || !IsSessionExpiredCode(code) { - return false - } - ClearAuthCookies(c) - for _, name := range authCookieNames() { - dropRequestCookie(c, name) - } - dropRequestCookie(c, guestSkipAutoCookie) - ClearGuestSkipAuto(c) - // 内容读取接口明确允许降级为游客,因此覆盖本请求可能已有的自动认证抑制。 - allowRequestAutoAuth(c) - return EnsureGuestOnEnter(c) != nil -} diff --git a/app/service/authser/types.go b/app/service/authser/types.go deleted file mode 100644 index 2a9c93c..0000000 --- a/app/service/authser/types.go +++ /dev/null @@ -1,95 +0,0 @@ -package authser - -import "encoding/json" - -// RegisterReq 对应 swagger authapi.registerReq -type RegisterReq struct { - AccountNo string `json:"accountNo"` - Email string `json:"email,omitempty"` - Password string `json:"password"` - AgentInviteCode string `json:"agentInviteCode,omitempty"` - DevID string `json:"devID,omitempty"` - Channel string `json:"channel,omitempty"` -} - -// LoginReq 对应 swagger authapi.loginReq -type LoginReq struct { - Account string `json:"account"` - Password string `json:"password"` - DevID string `json:"devID,omitempty"` - Channel string `json:"channel,omitempty"` -} - -// ResetPasswordReq 对应 swagger authapi.resetPasswordReq。 -type ResetPasswordReq struct { - AccountNo string `json:"accountNo"` - Email string `json:"email"` - NewPassword string `json:"newPassword"` -} - -// GuestLoginReq 对应 swagger authser.GuestLoginReq -// POST /api/app/auth/guest -type GuestLoginReq struct { - DevID string `json:"devID"` - DeviceID string `json:"deviceId,omitempty"` - Mac string `json:"mac,omitempty"` - Channel string `json:"channel,omitempty"` -} - -// UpdateProfileReq 对应 swagger authapi.updateProfileReq -type UpdateProfileReq struct { - Avatar *string `json:"avatar,omitempty"` - Introduction *string `json:"introduction,omitempty"` - NickName *string `json:"nickName,omitempty"` - OldPassword *string `json:"oldPassword,omitempty"` - NewPassword *string `json:"newPassword,omitempty"` - PrivacyMode *bool `json:"privacyMode,omitempty"` -} - -// LoginResp 对应 swagger authser.LoginResp -// GET /api/app/auth/getInfo、登录/注册/游客成功 data。 -type LoginResp struct { - Token string `json:"token"` - UserID int64 `json:"userId"` - AccountNo string `json:"accountNo"` - Email string `json:"email"` - NickName string `json:"nickName"` - Introduction string `json:"introduction"` // 简介 - Avatar string `json:"avatar"` // 头像 - InviteCode string `json:"inviteCode"` // 邀请码 - VipType int `json:"vipType"` // 0没卡 1月卡 2年卡 - CurrentVipExpire string `json:"currentVipExpire"` // VIP 过期时间,需结合 vipType 判断是否有效 - ShowCardID int64 `json:"showCardId"` // 最新会员卡 id - ShowCardName string `json:"showCardName"` // 最新会员卡名称,如「1个月会员」 - Status int `json:"status"` // 0正常 1封禁 2禁言 - Type int `json:"type"` // 身份类型:0游客 1普通用户 - IsNewUser bool `json:"isNewUser"` // 本次是否新建游客 - IsRegister bool `json:"isRegister"` // 是否已注册 - HasPassword *bool `json:"hasPassword,omitempty"` // 是否已设置密码;上游未返回时由账号状态推断 - LoginType int `json:"loginType"` // 登录方式 - PrivacyMode bool `json:"privacyMode"` // 隐私模式 - Score int64 `json:"score"` // 积分 - Recharge int64 `json:"recharge"` // 金币余额 -} - -// APIEnvelope 上游统一响应壳。Data 在失败时可能是 string/null。 -type APIEnvelope struct { - Code int `json:"code"` - Msg string `json:"msg"` - Tip string `json:"tip"` - Data json.RawMessage `json:"data"` -} - -// ImageUploadData 对应 swagger uploadser.ImageUploadData -type ImageUploadData struct { - ID string `json:"id"` - SourceNameFs string `json:"sourceNameFs"` -} - -// ImageUploadResp 对应 swagger uploadser.ImageUploadResp -type ImageUploadResp struct { - Code int `json:"code"` - Msg string `json:"msg"` - Tip string `json:"tip"` - Data *ImageUploadData `json:"data"` -} diff --git a/app/service/authser/view.go b/app/service/authser/view.go deleted file mode 100644 index ae5ac84..0000000 --- a/app/service/authser/view.go +++ /dev/null @@ -1,97 +0,0 @@ -package authser - -import ( - "fmt" - "strings" -) - -// SettingPageVM 账号设置页展示模型。 -type SettingPageVM struct { - UserID int64 - AccountNo string - HasAccount bool - HasPassword bool - NickName string - Introduction string - Email string - EmailMasked string - EmailBound bool - Avatar string // 可用头像 URL;空则模板用默认图 - InviteCode string - PrivacyMode bool - VipType int - VipExpire string - Recharge int64 - Score int64 - DisplayName string -} - -// BuildSettingVM 由 LoginResp 构建设置页 VM。 -func BuildSettingVM(user *LoginResp) SettingPageVM { - if user == nil { - return SettingPageVM{} - } - nick := strings.TrimSpace(user.NickName) - account := strings.TrimSpace(user.AccountNo) - registered := user.IsRegister || user.Type == 1 - hasAccount := registered && account != "" - hasPassword := hasAccount - if user.HasPassword != nil { - hasPassword = *user.HasPassword - } - email := strings.TrimSpace(user.Email) - intro := strings.TrimSpace(user.Introduction) - display := nick - if display == "" { - display = account - } - if display == "" { - display = "用户" - } - return SettingPageVM{ - UserID: user.UserID, - AccountNo: account, - HasAccount: hasAccount, - HasPassword: hasPassword, - NickName: nick, - Introduction: intro, - Email: email, - EmailMasked: MaskEmail(email), - EmailBound: email != "", - Avatar: usableAvatarURL(user.Avatar), - InviteCode: strings.TrimSpace(user.InviteCode), - PrivacyMode: user.PrivacyMode, - VipType: user.VipType, - VipExpire: strings.TrimSpace(user.CurrentVipExpire), - Recharge: user.Recharge, - Score: user.Score, - DisplayName: display, - } -} - -// MaskEmail 邮箱脱敏,如 ab***@mail.com。 -func MaskEmail(email string) string { - email = strings.TrimSpace(email) - if email == "" { - return "" - } - at := strings.LastIndex(email, "@") - if at <= 0 || at == len(email)-1 { - return email - } - local := email[:at] - domain := email[at:] - runes := []rune(local) - if len(runes) <= 2 { - return string(runes[:1]) + "***" + domain - } - return string(runes[:2]) + "***" + domain -} - -// FormatUserID 展示用 ID。 -func FormatUserID(id int64) string { - if id <= 0 { - return "" - } - return fmt.Sprintf("%d", id) -} diff --git a/app/service/authser/vip.go b/app/service/authser/vip.go deleted file mode 100644 index 70a634a..0000000 --- a/app/service/authser/vip.go +++ /dev/null @@ -1,187 +0,0 @@ -package authser - -import ( - "strconv" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -func parseVipExpireMs(raw string) int64 { - raw = strings.TrimSpace(raw) - if raw == "" { - return 0 - } - if n, err := strconv.ParseInt(raw, 10, 64); err == nil { - if n > 0 && n < 1e12 { - return n * 1000 - } - return n - } - layouts := []string{ - time.RFC3339Nano, - time.RFC3339, - "2006-01-02T15:04:05Z07:00", - "2006-01-02 15:04:05", - } - for _, layout := range layouts { - if t, err := time.Parse(layout, raw); err == nil { - return t.UnixMilli() - } - } - return 0 -} - -// UserIDFromCookies 读取 cookie userId。 -func UserIDFromCookies(c *gin.Context) int64 { - if c == nil { - return 0 - } - n, _ := strconv.ParseInt(strings.TrimSpace(cookieValue(c, "userId")), 10, 64) - if n < 0 { - return 0 - } - return n -} - -// VipTypeFromCookies 读取 cookie vipType。 -func VipTypeFromCookies(c *gin.Context) int { - if c == nil { - return 0 - } - n, _ := strconv.Atoi(strings.TrimSpace(cookieValue(c, "vipType"))) - return n -} - -// VipTypeLabel vipType 文案:0普通 1月卡 2年卡(与 91ly / 编辑资料注释一致)。 -func VipTypeLabel(vipType int) string { - switch vipType { - case 1: - return "月卡会员" - case 2: - return "年卡会员" - default: - return "普通会员" - } -} - -// ShowCardNameFromCookies 读取 cookie showCardName。 -func ShowCardNameFromCookies(c *gin.Context) string { - if c == nil { - return "" - } - return strings.TrimSpace(cookieValue(c, "showCardName")) -} - -// ShowCardIDFromCookies 读取 cookie showCardId。 -func ShowCardIDFromCookies(c *gin.Context) int64 { - if c == nil { - return 0 - } - n, _ := strconv.ParseInt(strings.TrimSpace(cookieValue(c, "showCardId")), 10, 64) - if n < 0 { - return 0 - } - return n -} - -// VipDisplayLabel 优先使用 getInfo.showCardName,否则回退 vipType 文案。 -func VipDisplayLabel(showCardName string, vipType int) string { - if s := strings.TrimSpace(showCardName); s != "" { - return s - } - return VipTypeLabel(vipType) -} - -// VipDisplayLabelFromCookies 从 cookie 组装会员展示名。 -func VipDisplayLabelFromCookies(c *gin.Context) string { - return VipDisplayLabel(ShowCardNameFromCookies(c), VipTypeFromCookies(c)) -} - -// IsActiveVipUser 根据 vipType + currentVipExpire 判断是否有效会员。 -func IsActiveVipUser(vipType int, currentVipExpire string) bool { - if vipType == 0 { - return false - } - expireMs := parseVipExpireMs(currentVipExpire) - if expireMs <= 0 { - return false - } - return expireMs > time.Now().UnixMilli() -} - -// FormatVipExpireText 格式化到期时间展示。 -func FormatVipExpireText(currentVipExpire string) string { - ms := parseVipExpireMs(currentVipExpire) - if ms <= 0 { - return "" - } - return time.UnixMilli(ms).In(time.Local).Format("有效期至 2006/01/02") -} - -// VipExpireTextFromCookies 格式化到期时间展示。 -func VipExpireTextFromCookies(c *gin.Context) string { - if c == nil { - return "" - } - return FormatVipExpireText(cookieValue(c, "currentVipExpire")) -} - -// IsActiveVipFromCookies vipType≠0 且 currentVipExpire 未过期(与 91ly 一致)。 -func IsActiveVipFromCookies(c *gin.Context) bool { - if c == nil { - return false - } - return IsActiveVipUser(VipTypeFromCookies(c), cookieValue(c, "currentVipExpire")) -} - -// RechargeFromCookies 读取 cookie recharge(金币余额)。 -func RechargeFromCookies(c *gin.Context) int64 { - if c == nil { - return 0 - } - n, _ := strconv.ParseInt(strings.TrimSpace(cookieValue(c, "recharge")), 10, 64) - if n < 0 { - return 0 - } - return n -} - -// SetRechargeCookie 更新金币余额 cookie(解锁/充值后同步)。 -func SetRechargeCookie(c *gin.Context, balance int64) { - if c == nil { - return - } - if balance < 0 { - balance = 0 - } - secure := c.Request != nil && c.Request.TLS != nil - setCookie(c, "recharge", strconv.FormatInt(balance, 10), secure) -} - -// FormatRecharge 金币余额千分位展示。 -func FormatRecharge(n int64) string { - if n < 0 { - n = 0 - } - s := strconv.FormatInt(n, 10) - if len(s) <= 3 { - return s - } - var b strings.Builder - rem := len(s) % 3 - if rem > 0 { - b.WriteString(s[:rem]) - if len(s) > rem { - b.WriteByte(',') - } - } - for i := rem; i < len(s); i += 3 { - b.WriteString(s[i : i+3]) - if i+3 < len(s) { - b.WriteByte(',') - } - } - return b.String() -} diff --git a/app/service/commentser/like.go b/app/service/commentser/like.go deleted file mode 100644 index 7f58280..0000000 --- a/app/service/commentser/like.go +++ /dev/null @@ -1,22 +0,0 @@ -package commentser - -import ( - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const likeSetPath = "/api/app/comment/like/set" - -// LikeSet 设置评论点赞/取消点赞。 -func LikeSet(c *gin.Context, req CommentLikeSetReq) (*APIEnvelope[CommentLikeSetResp], error) { - if req.CommentID <= 0 { - return nil, fmt.Errorf("comment like invalid commentId") - } - var env APIEnvelope[CommentLikeSetResp] - if _, err := upstream.PostJSON(c, likeSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/commentser/list.go b/app/service/commentser/list.go index c45c741..05c41f4 100644 --- a/app/service/commentser/list.go +++ b/app/service/commentser/list.go @@ -6,7 +6,6 @@ import ( "strconv" "strings" "time" - "videoweb/app/service/authser" "videoweb/app/service/upstream" "videoweb/pub/common/log" @@ -57,18 +56,11 @@ func FetchList(c *gin.Context, q ListQuery) (*GetCommentListResp, error) { params.Set("pageSize", strconv.Itoa(q.PageSize)) params.Set("sortOrder", strconv.Itoa(q.SortOrder)) - authser.EnsureGuestOnEnter(c) requestPath := listPath + "?" + params.Encode() env, err := fetchListOnce(c, requestPath) if err != nil { return nil, err } - if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) && authser.RecoverContentSession(c, env.Code) { - env, err = fetchListOnce(c, requestPath) - if err != nil { - return nil, err - } - } if env.Code != 200 { return nil, fmt.Errorf("comment list code=%d msg=%s", env.Code, env.Msg) } diff --git a/app/service/commentser/send.go b/app/service/commentser/send.go deleted file mode 100644 index c1f5ddc..0000000 --- a/app/service/commentser/send.go +++ /dev/null @@ -1,43 +0,0 @@ -package commentser - -import ( - "fmt" - "strings" - "unicode/utf8" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const sendPath = "/api/app/comment/send" - -// Send 发布评论(一级 / 二级)。 -func Send(c *gin.Context, req CommentSendReq) (*APIEnvelope[any], error) { - req.Text = strings.TrimSpace(req.Text) - if req.ObjectID <= 0 { - return nil, fmt.Errorf("comment send invalid objectId") - } - req.ObjectType = NormalizeObjectType(req.ObjectType) - if req.Text == "" { - return nil, fmt.Errorf("comment send empty text") - } - if utf8.RuneCountInString(req.Text) > 200 { - return nil, fmt.Errorf("comment send text too long") - } - if req.ParentsID < 0 { - req.ParentsID = 0 - } - if req.ReplyCommentID < 0 { - req.ReplyCommentID = 0 - } - // 二级评论:未显式指定 replyCommentId 时默认回复父评论 - if req.ParentsID > 0 && req.ReplyCommentID <= 0 { - req.ReplyCommentID = req.ParentsID - } - - var env APIEnvelope[any] - if _, err := upstream.PostJSON(c, sendPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/configser/info.go b/app/service/configser/info.go index de6ea43..af32775 100644 --- a/app/service/configser/info.go +++ b/app/service/configser/info.go @@ -47,9 +47,9 @@ func FetchAppConfigVMOrEmpty(c *gin.Context) AppConfigVM { if configCache != nil { return *configCache } - return ApplyTrackerFallback(AppConfigVM{}) + return AppConfigVM{} } - vm := ApplyTrackerFallback(BuildAppConfigVM(info)) + vm := BuildAppConfigVM(info) configCache = &vm configAt = time.Now() return vm diff --git a/app/service/configser/tracker.go b/app/service/configser/tracker.go deleted file mode 100644 index 87c66a3..0000000 --- a/app/service/configser/tracker.go +++ /dev/null @@ -1,25 +0,0 @@ -package configser - -import ( - "strings" - "videoweb/app/appg" -) - -// ResolveTrackerAppID 优先 yaml,否则固定 JHA-1045。 -func ResolveTrackerAppID() string { - return appg.TrackerAppID() -} - -// ResolveReportSDKConfig 上游 reportSdkConfig 优先,本地 yaml 兜底。 -func ResolveReportSDKConfig(fromAPI string) string { - if v := strings.TrimSpace(fromAPI); v != "" { - return v - } - return strings.TrimSpace(appg.Conf.Tracker.EncryptedConfig) -} - -// ApplyTrackerFallback 补齐 AppConfigVM 中的上报密文。 -func ApplyTrackerFallback(vm AppConfigVM) AppConfigVM { - vm.ReportSDKConfig = ResolveReportSDKConfig(vm.ReportSDKConfig) - return vm -} diff --git a/app/service/configser/types.go b/app/service/configser/types.go index 4c542f8..c39e644 100644 --- a/app/service/configser/types.go +++ b/app/service/configser/types.go @@ -52,16 +52,14 @@ type FooterContact struct { // ConfigInfoResp 对应 swagger proto.ConfigInfoResp // GET /api/app/config/info -// reportSdkConfig 与 proto.ConfigInfo 对齐;上游若尚未下发则为空,可由本地 yaml 兜底。 // config 为上游实际下发的完整站点配置(swagger 未完整声明,但含 potaoGroup 等字段)。 type ConfigInfoResp struct { - NewAddressInfo NewAddressInfo `json:"newAddressInfo"` - Config ConfigInfo `json:"config"` - Advitise AdvitiseData `json:"advitise"` - SeoInfo SeoInfo `json:"seoInfo"` - SeoNavInfo SeoNavInfo `json:"seoNavInfo"` - NavPageList []NavPageItem `json:"navPageList"` - ReportSDKConfig string `json:"reportSdkConfig"` + NewAddressInfo NewAddressInfo `json:"newAddressInfo"` + Config ConfigInfo `json:"config"` + Advitise AdvitiseData `json:"advitise"` + SeoInfo SeoInfo `json:"seoInfo"` + SeoNavInfo SeoNavInfo `json:"seoNavInfo"` + NavPageList []NavPageItem `json:"navPageList"` } // ConfigInfo 对应上游 data.config(proto.ConfigInfo 子集,按页面需要取用)。 diff --git a/app/service/configser/view.go b/app/service/configser/view.go index 1bc0380..83d4d29 100644 --- a/app/service/configser/view.go +++ b/app/service/configser/view.go @@ -29,7 +29,6 @@ type AppConfigVM struct { SeoInfo SeoInfo `json:"seoInfo,omitempty"` SeoNavInfo SeoNavInfo `json:"seoNavInfo,omitempty"` NavPageList []NavPageItem `json:"navPageList,omitempty"` - ReportSDKConfig string `json:"reportSdkConfig,omitempty"` Advitise AdvitiseData `json:"advitise"` DetailTopList []Advertisement `json:"detailTopList,omitempty"` NineGridList []Advertisement `json:"nineGridList,omitempty"` @@ -64,7 +63,6 @@ func BuildAppConfigVM(info *ConfigInfoResp) AppConfigVM { TwitterAddr: firstNonEmptyURL(cfg.TwitterAddr, addr.TwitterAddr), GitAddr: sanitizeURL(cfg.GitAddr), FreeVPN: sanitizeURL(cfg.FreeVPN), - ReportSDKConfig: strings.TrimSpace(info.ReportSDKConfig), NavPageList: buildNavPageList(info.NavPageList), Advitise: info.Advitise, DetailTopList: buildAdvertisementList(info.Advitise.DetailTopList), diff --git a/app/service/feedbackser/feedback.go b/app/service/feedbackser/feedback.go index 3448d15..6de8e9e 100644 --- a/app/service/feedbackser/feedback.go +++ b/app/service/feedbackser/feedback.go @@ -1,7 +1,7 @@ package feedbackser import ( - "videoweb/app/service/authser" + "io" "videoweb/app/service/upstream" "github.com/gin-gonic/gin" @@ -11,9 +11,11 @@ const addPath = "/api/app/feedback/add" const contactUsAddPath = "/api/app/contactus/add" +const uploadPath = "/api/app/upload/image" + // Add 调用上游提交意见反馈。 -func Add(c *gin.Context, req FeedbackReq) (*authser.APIEnvelope, error) { - var env authser.APIEnvelope +func Add(c *gin.Context, req FeedbackReq) (*APIEnvelope, error) { + var env APIEnvelope if _, err := upstream.PostJSON(c, addPath, req, &env); err != nil { return nil, err } @@ -21,10 +23,26 @@ func Add(c *gin.Context, req FeedbackReq) (*authser.APIEnvelope, error) { } // AddContactUs 调用上游联系我们接口。 -func AddContactUs(c *gin.Context, req ContactUsAddReq) (*authser.APIEnvelope, error) { - var env authser.APIEnvelope +func AddContactUs(c *gin.Context, req ContactUsAddReq) (*APIEnvelope, error) { + var env APIEnvelope if _, err := upstream.PostJSON(c, contactUsAddPath, req, &env); err != nil { return nil, err } return &env, nil } + +// UploadImage 上传反馈截图;不加水印。 +func UploadImage(c *gin.Context, fileName string, r io.Reader) (*ImageUploadResp, error) { + var resp ImageUploadResp + _, err := upstream.PostMultipart(c, uploadPath, map[string]string{ + "watermark": "0", + }, &upstream.MultipartFile{ + FieldName: "file", + FileName: fileName, + Reader: r, + }, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/app/service/feedbackser/types.go b/app/service/feedbackser/types.go index 8aab388..e62ff91 100644 --- a/app/service/feedbackser/types.go +++ b/app/service/feedbackser/types.go @@ -1,5 +1,29 @@ package feedbackser +import "encoding/json" + +// APIEnvelope 上游通用响应。 +type APIEnvelope struct { + Code int `json:"code"` + Msg string `json:"msg"` + Tip string `json:"tip"` + Data json.RawMessage `json:"data"` +} + +// ImageUploadData 对应 swagger uploadser.ImageUploadData +type ImageUploadData struct { + ID string `json:"id"` + SourceNameFs string `json:"sourceNameFs"` +} + +// ImageUploadResp 对应 swagger uploadser.ImageUploadResp +type ImageUploadResp struct { + Code int `json:"code"` + Msg string `json:"msg"` + Tip string `json:"tip"` + Data *ImageUploadData `json:"data"` +} + // FeedbackReq 对应 swagger proto.FeedbackReq。 // type(接口说明):1-功能建议、2-播放问题、3-账号问题、4-其他 type FeedbackReq struct { diff --git a/app/service/playletser/bought.go b/app/service/playletser/bought.go deleted file mode 100644 index bb7cee3..0000000 --- a/app/service/playletser/bought.go +++ /dev/null @@ -1,56 +0,0 @@ -package playletser - -import ( - "fmt" - "net/url" - "strconv" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const boughtListPath = "/api/app/playlet/bought/list" - -// PlayletBoughtData 对应 proto.PlayletBoughtData;只列出页面所需字段。 -type PlayletBoughtData struct { - ID int64 `json:"id"` - Title string `json:"title"` - PlayName string `json:"playName"` - Slug string `json:"slug"` - Cover string `json:"cover"` - ThemeNames []string `json:"themeNames"` - TotalEpisode int `json:"totalEpisode"` - BoughtChapterCount int `json:"boughtChapterCount"` - CostCoins int64 `json:"costCoins"` - BoughtAt string `json:"boughtAt"` - LastChapterID int64 `json:"lastChapterId"` - LastEpisode int `json:"lastEpisode"` - WatchedCount int `json:"watchedCount"` -} - -type PlayletBoughtListResp struct { - List []PlayletBoughtData `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -func FetchBoughtList(c *gin.Context, pageNum, pageSize int) (*PlayletBoughtListResp, int, error) { - if pageNum < 1 { - pageNum = 1 - } - if pageSize < 1 { - pageSize = 20 - } - params := url.Values{} - params.Set("pageNum", strconv.Itoa(pageNum)) - params.Set("pageSize", strconv.Itoa(pageSize)) - var env APIEnvelope[PlayletBoughtListResp] - if err := upstream.GetJSON(c, boughtListPath+"?"+params.Encode(), &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("playlet bought list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, env.Code, nil -} diff --git a/app/service/playletser/chapter.go b/app/service/playletser/chapter.go index 9ee569d..0a6a8fa 100644 --- a/app/service/playletser/chapter.go +++ b/app/service/playletser/chapter.go @@ -5,7 +5,6 @@ import ( "net/url" "strconv" "strings" - "videoweb/app/service/authser" "videoweb/app/service/upstream" "videoweb/pub/common/log" @@ -49,18 +48,10 @@ func FetchChapterDetail(c *gin.Context, chapterID, playletID int64, playletSlug } else if playletID > 0 { params.Set("playletId", strconv.FormatInt(playletID, 10)) } - authser.EnsureGuestOnEnter(c) var env APIEnvelope[PlayletChapterInfoResp] if err := upstream.GetJSON(c, chapterDetailPath+"?"+params.Encode(), &env); err != nil { return nil, err } - if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) { - if authser.RecoverContentSession(c, env.Code) { - if err := upstream.GetJSON(c, chapterDetailPath+"?"+params.Encode(), &env); err != nil { - return nil, err - } - } - } if env.Code != 200 { return nil, fmt.Errorf("chapter detail code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } @@ -91,18 +82,10 @@ func FetchChapterList(c *gin.Context, playletID int64, playletSlug string) (*Pla } else { return nil, fmt.Errorf("chapter list missing playletSlug or playletId") } - authser.EnsureGuestOnEnter(c) var env APIEnvelope[PlayletChapterListResp] if err := upstream.GetJSON(c, chapterListPath+"?"+params.Encode(), &env); err != nil { return nil, err } - if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) { - if authser.RecoverContentSession(c, env.Code) { - if err := upstream.GetJSON(c, chapterListPath+"?"+params.Encode(), &env); err != nil { - return nil, err - } - } - } if env.Code != 200 { return nil, fmt.Errorf("playlet chapter list code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } diff --git a/app/service/playletser/chapter_collect.go b/app/service/playletser/chapter_collect.go deleted file mode 100644 index 28681ef..0000000 --- a/app/service/playletser/chapter_collect.go +++ /dev/null @@ -1,84 +0,0 @@ -package playletser - -import ( - "fmt" - "net/url" - "strconv" - "videoweb/app/service/authser" - "videoweb/app/service/upstream" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -const chapterCollectListPath = "/api/app/playlet/chapter/collect/list" - -// PlayletChapterCollectData 对应 swagger proto.PlayletChapterCollectData -type PlayletChapterCollectData struct { - ChapterID int64 `json:"chapterId"` - ChapterTitle string `json:"chapterTitle"` - PlayletID int64 `json:"playletId"` - PlayletSlug string `json:"playletSlug"` - PlayletTitle string `json:"playletTitle"` - PlayletCover string `json:"playletCover"` - CurrentEpisode int `json:"currentEpisode"` - TotalEpisode int `json:"totalEpisode"` - UpdateStatus int `json:"updateStatus"` - CountBrowse int64 `json:"countBrowse"` - CountCollect int64 `json:"countCollect"` - CountLike int64 `json:"countLike"` - IsCollect int `json:"isCollect"` - CollectedAt string `json:"collectedAt"` - MediaTime int `json:"mediaTime"` - PayType int `json:"payType"` - SingleCoins int `json:"singleCoins"` - VideoURL string `json:"videoUrl"` -} - -// PlayletChapterCollectListResp 对应 swagger proto.PlayletChapterCollectListResp -type PlayletChapterCollectListResp struct { - List []PlayletChapterCollectData `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -// FetchChapterCollectList 拉取当前用户剧集收藏列表。 -// 第二个返回值为上游业务 code(网络错误时为 0)。 -func FetchChapterCollectList(c *gin.Context, q CollectListQuery) (*PlayletChapterCollectListResp, int, error) { - q.Normalize() - params := url.Values{} - params.Set("pageNum", strconv.Itoa(q.PageNum)) - params.Set("pageSize", strconv.Itoa(q.PageSize)) - - var env APIEnvelope[PlayletChapterCollectListResp] - if err := upstream.GetJSON(c, chapterCollectListPath+"?"+params.Encode(), &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("playlet chapter collect list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, env.Code, nil -} - -// FetchChapterCollectListOrEmpty 失败时返回空列表;游客会话失效时尝试重登后重试一次。 -func FetchChapterCollectListOrEmpty(c *gin.Context, q CollectListQuery) *PlayletChapterCollectListResp { - data, code, err := FetchChapterCollectList(c, q) - if err == nil { - if data == nil { - return &PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - if retry, _ := authser.OnSessionExpired(c, code); retry { - data, _, err = FetchChapterCollectList(c, q) - if err == nil { - if data == nil { - return &PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - } - log.Error("fetch playlet chapter collect list failed", log.E(err)) - return &PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} -} diff --git a/app/service/playletser/chapter_like.go b/app/service/playletser/chapter_like.go deleted file mode 100644 index 3d20ee5..0000000 --- a/app/service/playletser/chapter_like.go +++ /dev/null @@ -1,63 +0,0 @@ -package playletser - -import ( - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const ( - chapterLikeSetPath = "/api/app/playlet/chapter/like/set" - chapterCollectSetPath = "/api/app/playlet/chapter/collect/set" -) - -// PlayletChapterLikeSetReq 对应 swagger proto.PlayletChapterLikeSetReq -type PlayletChapterLikeSetReq struct { - ChapterID int64 `json:"chapterId"` - IsLike bool `json:"isLike"` -} - -// PlayletChapterLikeSetResp 对应 swagger proto.PlayletChapterLikeSetResp -type PlayletChapterLikeSetResp struct { - ChapterID int64 `json:"chapterId"` - CountLike int64 `json:"countLike"` - IsLike int `json:"isLike"` -} - -// ChapterLikeSet 设置剧集点赞/取消点赞。 -func ChapterLikeSet(c *gin.Context, req PlayletChapterLikeSetReq) (*APIEnvelope[PlayletChapterLikeSetResp], error) { - if req.ChapterID <= 0 { - return nil, fmt.Errorf("chapter like invalid chapterId") - } - var env APIEnvelope[PlayletChapterLikeSetResp] - if _, err := upstream.PostJSON(c, chapterLikeSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// PlayletChapterCollectSetReq 对应 swagger proto.PlayletChapterCollectSetReq -type PlayletChapterCollectSetReq struct { - ChapterID int64 `json:"chapterId"` - IsCollect bool `json:"isCollect"` -} - -// PlayletChapterCollectSetResp 对应 swagger proto.PlayletChapterCollectSetResp -type PlayletChapterCollectSetResp struct { - ChapterID int64 `json:"chapterId"` - CountCollect int64 `json:"countCollect"` - IsCollect int `json:"isCollect"` -} - -// ChapterCollectSet 设置剧集收藏/取消收藏。 -func ChapterCollectSet(c *gin.Context, req PlayletChapterCollectSetReq) (*APIEnvelope[PlayletChapterCollectSetResp], error) { - if req.ChapterID <= 0 { - return nil, fmt.Errorf("chapter collect invalid chapterId") - } - var env APIEnvelope[PlayletChapterCollectSetResp] - if _, err := upstream.PostJSON(c, chapterCollectSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/playletser/chapter_unlock.go b/app/service/playletser/chapter_unlock.go deleted file mode 100644 index 6f62c67..0000000 --- a/app/service/playletser/chapter_unlock.go +++ /dev/null @@ -1,40 +0,0 @@ -package playletser - -import ( - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const chapterUnlockPath = "/api/app/playlet/chapter/unlock" - -// PlayletChapterUnlockReq 对应 swagger proto.PlayletChapterUnlockReq -// POST /api/app/playlet/chapter/unlock -type PlayletChapterUnlockReq struct { - ChapterID int64 `json:"chapterId"` - PlayletID int64 `json:"playletId"` -} - -// PlayletChapterUnlockResp 对应 swagger proto.PlayletChapterUnlockResp -type PlayletChapterUnlockResp struct { - Balance int64 `json:"balance"` - CanWatch bool `json:"canWatch"` - ChangeCoin int64 `json:"changeCoin"` - ChapterID int64 `json:"chapterId"` - CostCoins int64 `json:"costCoins"` - PlayletID int64 `json:"playletId"` - Unlocked bool `json:"unlocked"` -} - -// ChapterUnlock 使用金币解锁剧集(幂等)。 -func ChapterUnlock(c *gin.Context, req PlayletChapterUnlockReq) (*APIEnvelope[PlayletChapterUnlockResp], error) { - if req.ChapterID <= 0 || req.PlayletID <= 0 { - return nil, fmt.Errorf("chapter unlock invalid chapterId/playletId") - } - var env APIEnvelope[PlayletChapterUnlockResp] - if _, err := upstream.PostJSON(c, chapterUnlockPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/playletser/chapter_watch.go b/app/service/playletser/chapter_watch.go deleted file mode 100644 index 2fd6c1f..0000000 --- a/app/service/playletser/chapter_watch.go +++ /dev/null @@ -1,39 +0,0 @@ -package playletser - -import ( - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const chapterWatchReportPath = "/api/app/playlet/chapter/watch/report" - -// PlayletChapterWatchReportReq 对应 swagger proto.PlayletChapterWatchReportReq -type PlayletChapterWatchReportReq struct { - ChapterID int64 `json:"chapterId"` - PlayletID int64 `json:"playletId,omitempty"` -} - -// PlayletChapterWatchReportResp 对应 swagger proto.PlayletChapterWatchReportResp -type PlayletChapterWatchReportResp struct { - ChapterID int64 `json:"chapterId"` - PlayletID int64 `json:"playletId"` - CurrentEpisode int `json:"currentEpisode"` - LastChapterID int64 `json:"lastChapterId"` - LastEpisode int `json:"lastEpisode"` - WatchedCount int `json:"watchedCount"` - UnwatchedCount int `json:"unwatchedCount"` -} - -// ChapterWatchReport 上报短剧剧集观看。 -func ChapterWatchReport(c *gin.Context, req PlayletChapterWatchReportReq) (*APIEnvelope[PlayletChapterWatchReportResp], error) { - if req.ChapterID <= 0 { - return nil, fmt.Errorf("chapter watch report invalid chapterId") - } - var env APIEnvelope[PlayletChapterWatchReportResp] - if _, err := upstream.PostJSON(c, chapterWatchReportPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/playletser/collect.go b/app/service/playletser/collect.go deleted file mode 100644 index cdc17b0..0000000 --- a/app/service/playletser/collect.go +++ /dev/null @@ -1,161 +0,0 @@ -package playletser - -import ( - "fmt" - "net/url" - "strconv" - "strings" - "videoweb/app/service/authser" - "videoweb/app/service/upstream" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -const ( - collectSetPath = "/api/app/playlet/collect/set" - collectListPath = "/api/app/playlet/collect/list" - defaultCollectPageSize = 40 - - CollectTypePlaylet = "playlet" - CollectTypeChapter = "chapter" -) - -// PlayletCollectSetReq 对应 swagger proto.PlayletCollectSetReq -type PlayletCollectSetReq struct { - ID int64 `json:"id"` - IsCollect bool `json:"isCollect"` -} - -// PlayletCollectSetResp 对应 swagger proto.PlayletCollectSetResp -type PlayletCollectSetResp struct { - ID int64 `json:"id"` - CountCollect int64 `json:"countCollect"` - IsCollect int `json:"isCollect"` -} - -// PlayletCollectData 对应 swagger proto.PlayletCollectData -type PlayletCollectData struct { - ID int64 `json:"id"` - Title string `json:"title"` - PlayName string `json:"playName"` - Slug string `json:"slug"` - Cover string `json:"cover"` - Desc string `json:"desc"` - TagIDs []int64 `json:"tagIds"` - TagNames []string `json:"tagNames"` - TagSlugs []string `json:"tagSlugs"` - ThemeIDs []int64 `json:"themeIds"` - ThemeNames []string `json:"themeNames"` - ThemeSlugs []string `json:"themeSlugs"` - TotalEpisode int `json:"totalEpisode"` - UpdateStatus int `json:"updateStatus"` - CountBrowse int64 `json:"countBrowse"` - CountCollect int64 `json:"countCollect"` - CountLike int64 `json:"countLike"` - IsFeatured bool `json:"isFeatured"` - IsNew bool `json:"isNew"` - IsRecommend bool `json:"isRecommend"` - IsCollect int `json:"isCollect"` - CollectedAt string `json:"collectedAt"` - WatchedCount int `json:"watchedCount"` - UnwatchedCount int `json:"unwatchedCount"` - LastChapterID int64 `json:"lastChapterId"` - LastEpisode int `json:"lastEpisode"` -} - -// PlayletCollectListResp 对应 swagger proto.PlayletCollectListResp -type PlayletCollectListResp struct { - List []PlayletCollectData `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -// CollectListQuery 收藏列表分页参数。 -type CollectListQuery struct { - PageNum int - PageSize int - WatchStatus int // 0 全部、1 追剧中、2 已看完;仅短剧收藏列表使用 - Type string // playlet | chapter -} - -// NormalizeCollectType 归一化收藏类型。 -func NormalizeCollectType(raw string) string { - switch strings.TrimSpace(strings.ToLower(raw)) { - case CollectTypeChapter, "2", "episode", "ep": - return CollectTypeChapter - default: - return CollectTypePlaylet - } -} - -// Normalize 校正分页默认值。 -func (q *CollectListQuery) Normalize() { - if q.PageNum <= 0 { - q.PageNum = 1 - } - if q.PageSize <= 0 { - q.PageSize = defaultCollectPageSize - } - if q.PageSize > 50 { - q.PageSize = 50 - } - if q.WatchStatus < 0 || q.WatchStatus > 2 { - q.WatchStatus = 0 - } - q.Type = NormalizeCollectType(q.Type) -} - -// CollectSet 设置短剧收藏/取消收藏。 -func CollectSet(c *gin.Context, req PlayletCollectSetReq) (*APIEnvelope[PlayletCollectSetResp], error) { - if req.ID <= 0 { - return nil, fmt.Errorf("playlet collect invalid id") - } - var env APIEnvelope[PlayletCollectSetResp] - if _, err := upstream.PostJSON(c, collectSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// FetchCollectList 拉取当前用户短剧收藏列表。 -// 第二个返回值为上游业务 code(网络错误时为 0)。 -func FetchCollectList(c *gin.Context, q CollectListQuery) (*PlayletCollectListResp, int, error) { - q.Normalize() - params := url.Values{} - params.Set("pageNum", strconv.Itoa(q.PageNum)) - params.Set("pageSize", strconv.Itoa(q.PageSize)) - params.Set("watchStatus", strconv.Itoa(q.WatchStatus)) - - var env APIEnvelope[PlayletCollectListResp] - if err := upstream.GetJSON(c, collectListPath+"?"+params.Encode(), &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("playlet collect list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, env.Code, nil -} - -// FetchCollectListOrEmpty 失败时返回空列表;游客会话失效时尝试重登后重试一次。 -func FetchCollectListOrEmpty(c *gin.Context, q CollectListQuery) *PlayletCollectListResp { - data, code, err := FetchCollectList(c, q) - if err == nil { - if data == nil { - return &PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - if retry, _ := authser.OnSessionExpired(c, code); retry { - data, _, err = FetchCollectList(c, q) - if err == nil { - if data == nil { - return &PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - } - log.Error("fetch playlet collect list failed", log.E(err)) - return &PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} -} diff --git a/app/service/playletser/collect_view.go b/app/service/playletser/collect_view.go deleted file mode 100644 index e5c5dc1..0000000 --- a/app/service/playletser/collect_view.go +++ /dev/null @@ -1,89 +0,0 @@ -package playletser - -import ( - "fmt" - "net/url" - "strconv" - "strings" -) - -func collectPageURL(pageNum int, collectType string) string { - collectType = NormalizeCollectType(collectType) - path := "/collect" - if collectType != CollectTypePlaylet { - path += "/" + url.PathEscape(collectType) - } - if pageNum > 1 { - path += "/page/" + strconv.Itoa(pageNum) - } - return path -} - -func CollectCanonicalPath(q CollectListQuery) string { - q.Normalize() - return collectPageURL(q.PageNum, q.Type) -} - -// BuildCollectPagination 收藏列表分页。 -func BuildCollectPagination(q CollectListQuery, totalCount int64) PaginationVM { - q.Normalize() - return BuildPagination(func(p int) string { - return collectPageURL(p, q.Type) - }, q.PageNum, q.PageSize, totalCount) -} - -// ParseCollectListQuery 从 query 解析分页与类型(playlet 短剧 / chapter 剧集)。 -func ParseCollectListQuery(pageStr, typeStr string) CollectListQuery { - q := CollectListQuery{ - PageSize: defaultCollectPageSize, - Type: NormalizeCollectType(typeStr), - } - if n, err := strconv.Atoi(strings.TrimSpace(pageStr)); err == nil { - q.PageNum = n - } - q.Normalize() - return q -} - -// collectProgressText 进度文案:优先最近观看集,其次未看/已看完。 -func collectProgressText(item PlayletCollectData) string { - if item.LastEpisode > 0 { - return fmt.Sprintf("第%d集", item.LastEpisode) - } - if item.TotalEpisode > 0 && item.UnwatchedCount <= 0 { - return "已看完" - } - if item.UnwatchedCount > 0 { - return fmt.Sprintf("%d集未看", item.UnwatchedCount) - } - return "" -} - -// collectTagText 收藏卡片标签:仅展示 TagNames 第一项,无标签时不显示。 -func collectTagText(item PlayletCollectData) string { - return firstName(item.TagNames) -} - -// chapterCollectEpisodeLabel 剧集收藏角标:总集数。 -func chapterCollectEpisodeLabel(item PlayletChapterCollectData) string { - return episodeText(item.TotalEpisode) -} - -// chapterCollectDesc 剧集收藏副标题。 -func chapterCollectDesc(item PlayletChapterCollectData) string { - if t := strings.TrimSpace(item.ChapterTitle); t != "" { - return t - } - if item.CurrentEpisode > 0 { - return fmt.Sprintf("第%d集", item.CurrentEpisode) - } - return "" -} - -// chapterCollectMeta 剧集收藏右侧文案:第 N 集。 -func chapterCollectMeta(item PlayletChapterCollectData) string { - if item.CurrentEpisode > 0 { - return fmt.Sprintf("第%d集", item.CurrentEpisode) - } - return updateStatusText(item.UpdateStatus) -} diff --git a/app/service/playletser/detail.go b/app/service/playletser/detail.go index 5434b9b..ffa5b4e 100644 --- a/app/service/playletser/detail.go +++ b/app/service/playletser/detail.go @@ -5,7 +5,6 @@ import ( "net/url" "strconv" "strings" - "videoweb/app/service/authser" "videoweb/app/service/upstream" "videoweb/pub/common/log" @@ -45,19 +44,10 @@ func FetchDetailByKey(c *gin.Context, key string) (*PlayletInfoResp, error) { } func fetchDetail(c *gin.Context, queryKey, value string) (*PlayletInfoResp, error) { - authser.EnsureGuestOnEnter(c) env, err := fetchDetailOnce(c, queryKey, value) if err != nil { return nil, err } - if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) { - if authser.RecoverContentSession(c, env.Code) { - env, err = fetchDetailOnce(c, queryKey, value) - if err != nil { - return nil, err - } - } - } if env.Code != 200 { return nil, fmt.Errorf("playlet detail code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } diff --git a/app/service/playletser/history.go b/app/service/playletser/history.go deleted file mode 100644 index 318ac08..0000000 --- a/app/service/playletser/history.go +++ /dev/null @@ -1,166 +0,0 @@ -package playletser - -import ( - "fmt" - "net/url" - "strconv" - "videoweb/app/service/authser" - "videoweb/app/service/upstream" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -const ( - historyListPath = "/api/app/playlet/history/list" - historyClearPath = "/api/app/playlet/history/clear" - historyDeletePath = "/api/app/playlet/history/delete" - defaultHistoryPageSize = 40 -) - -// PlayletHistoryData 对应 swagger proto.PlayletHistoryData -// GET /api/app/playlet/history/list -type PlayletHistoryData struct { - ID int64 `json:"id"` - Status int `json:"status"` - IsFeatured bool `json:"isFeatured"` - IsNew bool `json:"isNew"` - IsRecommend bool `json:"isRecommend"` - ShowOnBanner bool `json:"showOnBanner"` - Banner string `json:"banner"` - OnlineAt string `json:"onlineAt"` - MediaCreatedAt string `json:"mediaCreatedAt"` - MediaUpdatedAt string `json:"mediaUpdatedAt"` - ThemeIDs []int64 `json:"themeIds"` - ThemeNames []string `json:"themeNames"` - ThemeSlugs []string `json:"themeSlugs"` - Title string `json:"title"` - PlayName string `json:"playName"` - Slug string `json:"slug"` - Desc string `json:"desc"` - Anchor string `json:"anchor"` - Cover string `json:"cover"` - TagIDs []int64 `json:"tagIds"` - TagNames []string `json:"tagNames"` - TagSlugs []string `json:"tagSlugs"` - TotalEpisode int `json:"totalEpisode"` - UpdateStatus int `json:"updateStatus"` - CountBrowse int64 `json:"countBrowse"` - CountCollect int64 `json:"countCollect"` - CountLike int64 `json:"countLike"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - WatchedCount int `json:"watchedCount"` - UnwatchedCount int `json:"unwatchedCount"` - LastChapterID int64 `json:"lastChapterId"` - LastEpisode int `json:"lastEpisode"` - IsFinished bool `json:"isFinished"` - HistoryAt string `json:"historyAt"` -} - -// PlayletHistoryListResp 对应 swagger proto.PlayletHistoryListResp -type PlayletHistoryListResp struct { - List []PlayletHistoryData `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -// PlayletHistoryClearResp 对应 swagger proto.PlayletHistoryClearResp -type PlayletHistoryClearResp struct { - DeletedCount int `json:"deletedCount"` -} - -// PlayletHistoryDeleteReq 对应 swagger proto.PlayletHistoryDeleteReq -type PlayletHistoryDeleteReq struct { - PlayletID int64 `json:"playletId"` -} - -// PlayletHistoryDeleteResp 对应 swagger proto.PlayletHistoryDeleteResp -type PlayletHistoryDeleteResp struct { - DeletedCount int `json:"deletedCount"` -} - -// HistoryListQuery 观看历史分页参数。 -type HistoryListQuery struct { - PageNum int - PageSize int -} - -// Normalize 校正分页默认值。 -func (q *HistoryListQuery) Normalize() { - if q.PageNum <= 0 { - q.PageNum = 1 - } - if q.PageSize <= 0 { - q.PageSize = defaultHistoryPageSize - } - if q.PageSize > 100 { - q.PageSize = 100 - } -} - -// FetchHistoryList 分页查询当前用户观看历史。 -// 第二个返回值为上游业务 code(网络错误时为 0)。 -func FetchHistoryList(c *gin.Context, q HistoryListQuery) (*PlayletHistoryListResp, int, error) { - q.Normalize() - params := url.Values{} - params.Set("pageNum", strconv.Itoa(q.PageNum)) - params.Set("pageSize", strconv.Itoa(q.PageSize)) - - var env APIEnvelope[PlayletHistoryListResp] - if err := upstream.GetJSON(c, historyListPath+"?"+params.Encode(), &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("playlet history list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, env.Code, nil -} - -// FetchHistoryListOrEmpty 失败时返回空列表;游客会话失效时尝试重登后重试一次。 -func FetchHistoryListOrEmpty(c *gin.Context, q HistoryListQuery) *PlayletHistoryListResp { - data, code, err := FetchHistoryList(c, q) - if err == nil { - if data == nil { - return &PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - if retry, _ := authser.OnSessionExpired(c, code); retry { - data, _, err = FetchHistoryList(c, q) - if err == nil { - if data == nil { - return &PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - } - log.Error("fetch playlet history list failed", log.E(err)) - return &PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize} -} - -// HistoryClear 清空当前用户短剧观看历史。 -// POST /api/app/playlet/history/clear,无请求体。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_clear -func HistoryClear(c *gin.Context) (*APIEnvelope[PlayletHistoryClearResp], error) { - var env APIEnvelope[PlayletHistoryClearResp] - if _, err := upstream.PostJSON(c, historyClearPath, nil, &env); err != nil { - return nil, err - } - return &env, nil -} - -// HistoryDelete 删除单条短剧观看历史。 -// POST /api/app/playlet/history/delete -// 文档:http://172.104.181.99:25319/swagger/index.html#/app短剧相关接口/post_api_app_playlet_history_delete -func HistoryDelete(c *gin.Context, req PlayletHistoryDeleteReq) (*APIEnvelope[PlayletHistoryDeleteResp], error) { - if req.PlayletID <= 0 { - return nil, fmt.Errorf("playlet history delete invalid playletId") - } - var env APIEnvelope[PlayletHistoryDeleteResp] - if _, err := upstream.PostJSON(c, historyDeletePath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/playletser/history_view.go b/app/service/playletser/history_view.go deleted file mode 100644 index 3b98629..0000000 --- a/app/service/playletser/history_view.go +++ /dev/null @@ -1,229 +0,0 @@ -package playletser - -import ( - "fmt" - "strconv" - "strings" - "time" -) - -func historyPageURL(pageNum int) string { - if pageNum <= 1 { - return "/history" - } - return "/history/page/" + strconv.Itoa(pageNum) -} - -func HistoryCanonicalPath(q HistoryListQuery) string { - q.Normalize() - return historyPageURL(q.PageNum) -} - -// BuildHistoryPagination 观看历史分页。 -func BuildHistoryPagination(q HistoryListQuery, totalCount int64) PaginationVM { - q.Normalize() - return BuildPagination(func(p int) string { - return historyPageURL(p) - }, q.PageNum, q.PageSize, totalCount) -} - -// ParseHistoryListQuery 从 query 解析分页。 -func ParseHistoryListQuery(pageStr string) HistoryListQuery { - q := HistoryListQuery{PageSize: defaultHistoryPageSize} - if n, err := strconv.Atoi(strings.TrimSpace(pageStr)); err == nil { - q.PageNum = n - } - q.Normalize() - return q -} - -func historyFinished(item PlayletHistoryData) bool { - if item.IsFinished { - return true - } - return item.TotalEpisode > 0 && item.UnwatchedCount <= 0 && item.WatchedCount > 0 -} - -// historyStatus 卡片筛选状态:finished / unfinished。 -func historyStatus(item PlayletHistoryData) string { - if historyFinished(item) { - return "finished" - } - return "unfinished" -} - -// historyProgressText PC 卡片未看文案,使用接口 unwatchedCount / isFinished。 -func historyProgressText(item PlayletHistoryData) string { - if historyFinished(item) { - return "已看完" - } - if item.UnwatchedCount > 0 { - return fmt.Sprintf("%d集未看", item.UnwatchedCount) - } - if item.LastEpisode > 0 { - return fmt.Sprintf("看到第%d集", item.LastEpisode) - } - return "未看完" -} - -// historyWatchLine H5 进度行。 -func historyWatchLine(item PlayletHistoryData) string { - if historyFinished(item) { - if item.LastEpisode > 0 { - return fmt.Sprintf("第%d集 · 已看完", item.LastEpisode) - } - return "已看完" - } - ep := "未开始" - if item.LastEpisode > 0 { - ep = fmt.Sprintf("第%d集", item.LastEpisode) - } - if item.UnwatchedCount > 0 { - return ep + fmt.Sprintf(" · 还有%d集未看", item.UnwatchedCount) - } - return ep + " · 未看完" -} - -// historyTagText 历史卡片标签:仅展示 TagNames 第一项,无标签时不显示。 -func historyTagText(item PlayletHistoryData) string { - return firstName(item.TagNames) -} - -// historyPlayURL 继续播放:优先最近观看剧集。 -func historyPlayURL(item PlayletHistoryData) string { - return chapterPlayURL(item.Slug, item.ID, item.LastChapterID) -} - -// historyContinueMeta 首页「继续观看」文案:看至第 N 集 / 剩 M 集。 -func historyContinueMeta(item PlayletHistoryData) string { - if item.LastEpisode <= 0 { - return "观看记录" - } - meta := fmt.Sprintf("看至第 %d 集", item.LastEpisode) - if item.UnwatchedCount > 0 { - meta += fmt.Sprintf(" 剩 %d 集", item.UnwatchedCount) - } - return meta -} - -// historyProgressPct 观看进度百分比,供卡片 --p 使用。 -func historyProgressPct(item PlayletHistoryData) string { - progress := 0 - if item.TotalEpisode > 0 { - progress = item.WatchedCount * 100 / item.TotalEpisode - } - if progress < 0 { - progress = 0 - } - if progress > 100 { - progress = 100 - } - return fmt.Sprintf("%d%%", progress) -} - -// FormatHistoryAt 最近观看时间展示。 -func FormatHistoryAt(raw string) string { - return formatHistoryAt(raw) -} - -// FormatHistoryListTime 观看历史列表时间:今天/昨天带时刻,更早仅日期。 -func FormatHistoryListTime(raw string) string { - raw = strings.TrimSpace(raw) - if raw == "" { - return "" - } - t, ok := parseHistoryTime(raw) - if !ok { - return raw - } - now := time.Now() - startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) - dayDiff := int(startToday.Sub(startThat).Hours() / 24) - hm := t.Format("15:04") - switch { - case dayDiff <= 0: - return "今天 " + hm - case dayDiff == 1: - return "昨天 " + hm - default: - return fmt.Sprintf("%d 月 %d 日", int(t.Month()), t.Day()) - } -} - -// HistoryDayBucket 列表分组标题:今天 / 昨天 / 更早。 -func HistoryDayBucket(raw string) string { - raw = strings.TrimSpace(raw) - t, ok := parseHistoryTime(raw) - if !ok { - return "更早" - } - now := time.Now() - startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) - dayDiff := int(startToday.Sub(startThat).Hours() / 24) - switch { - case dayDiff <= 0: - return "今天" - case dayDiff == 1: - return "昨天" - default: - return "更早" - } -} - -func formatHistoryAt(raw string) string { - raw = strings.TrimSpace(raw) - if raw == "" { - return "" - } - t, ok := parseHistoryTime(raw) - if !ok { - return raw - } - now := time.Now() - startToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - startThat := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()) - dayDiff := int(startToday.Sub(startThat).Hours() / 24) - hm := t.Format("15:04") - switch { - case dayDiff <= 0: - return "今天 " + hm - case dayDiff == 1: - return "昨天 " + hm - case dayDiff == 2: - return "前天 " + hm - case dayDiff < 7: - return fmt.Sprintf("%d天前 %s", dayDiff, hm) - case dayDiff < 14: - return "一周前 " + hm - default: - return fmt.Sprintf("%d 月 %d 日", int(t.Month()), t.Day()) - } -} - -func parseHistoryTime(raw string) (time.Time, bool) { - layouts := []string{ - time.RFC3339Nano, - time.RFC3339, - "2006-01-02 15:04:05", - "2006-01-02T15:04:05", - "2006-01-02T15:04:05Z07:00", - "2006-01-02 15:04:05.000", - "2006-01-02 15:04:05.999999999", - } - for _, layout := range layouts { - if t, err := time.ParseInLocation(layout, raw, time.Local); err == nil { - return t, true - } - if t, err := time.Parse(layout, raw); err == nil { - return t.In(time.Local), true - } - } - return time.Time{}, false -} - -// ParseHistoryTime 解析观看历史时间字符串。 -func ParseHistoryTime(raw string) (time.Time, bool) { - return parseHistoryTime(strings.TrimSpace(raw)) -} diff --git a/app/service/playletser/like.go b/app/service/playletser/like.go deleted file mode 100644 index bf8e462..0000000 --- a/app/service/playletser/like.go +++ /dev/null @@ -1,35 +0,0 @@ -package playletser - -import ( - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const likeSetPath = "/api/app/playlet/like/set" - -// PlayletLikeSetReq 对应 swagger proto.PlayletLikeSetReq -type PlayletLikeSetReq struct { - ID int64 `json:"id"` - IsLike bool `json:"isLike"` -} - -// PlayletLikeSetResp 对应 swagger proto.PlayletLikeSetResp -type PlayletLikeSetResp struct { - ID int64 `json:"id"` - CountLike int64 `json:"countLike"` - IsLike int `json:"isLike"` -} - -// LikeSet 设置短剧点赞/取消点赞。 -func LikeSet(c *gin.Context, req PlayletLikeSetReq) (*APIEnvelope[PlayletLikeSetResp], error) { - if req.ID <= 0 { - return nil, fmt.Errorf("playlet like invalid id") - } - var env APIEnvelope[PlayletLikeSetResp] - if _, err := upstream.PostJSON(c, likeSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/playletser/search.go b/app/service/playletser/search.go index af6af65..4cd76e6 100644 --- a/app/service/playletser/search.go +++ b/app/service/playletser/search.go @@ -5,7 +5,6 @@ import ( "net/url" "strconv" "strings" - "videoweb/app/service/authser" "videoweb/app/service/upstream" "videoweb/pub/common/log" @@ -46,19 +45,10 @@ func FetchSearch(c *gin.Context, q SearchQuery) (*PlayletSearchResp, error) { if q.Keyword == "" { return nil, fmt.Errorf("playlet search keyword is empty") } - authser.EnsureGuestOnEnter(c) env, err := fetchSearchOnce(c, q) if err != nil { return nil, err } - if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) { - if authser.RecoverContentSession(c, env.Code) { - env, err = fetchSearchOnce(c, q) - if err != nil { - return nil, err - } - } - } if env.Code != 200 { return nil, fmt.Errorf("playlet search code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } @@ -98,19 +88,10 @@ func FetchSearchSuggest(c *gin.Context, keyword string) (*PlayletSearchSuggestRe if keyword == "" { return nil, fmt.Errorf("playlet search suggest keyword is empty") } - authser.EnsureGuestOnEnter(c) env, err := fetchSearchSuggestOnce(c, keyword) if err != nil { return nil, err } - if env.Code != 200 && authser.IsSessionExpiredCode(env.Code) { - if authser.RecoverContentSession(c, env.Code) { - env, err = fetchSearchSuggestOnce(c, keyword) - if err != nil { - return nil, err - } - } - } if env.Code != 200 { return nil, fmt.Errorf("playlet search suggest code=%d msg=%s tip=%s", env.Code, env.Msg, env.Tip) } diff --git a/app/service/playletser/tplfuncs.go b/app/service/playletser/tplfuncs.go index 5483905..820d374 100644 --- a/app/service/playletser/tplfuncs.go +++ b/app/service/playletser/tplfuncs.go @@ -12,83 +12,70 @@ import ( // TemplateFuncs 模板辅助:仅做展示格式化 / URL,不把接口字段映射成另一套名字。 func TemplateFuncs() template.FuncMap { return template.FuncMap{ - "detailURL": detailURL, - "playURL": playURL, - "chapterPlayURL": chapterPlayURL, - "categoryURL": categoryURL, - "categoryDetailURL": categoryDetailURL, - "themeListURL": themeListURLByKey, - "tagOptionsURL": tagListURLByKey, - "allListURL": AllListURL, - "moreListURL": MoreListURL, - "moreSeriesListURL": MoreSeriesListURL, - "navMegaIcon": NavMegaIcon, - "topicListURL": TopicListURL, - "topicDetailURL": TopicDetailURL, - "topicCoverURLs": TopicCoverURLs, - "tagPageURL": TagListURL, - "hotRankPageURL": HotRankPageURL, - "searchResultsURL": SearchResultsURL, - "coverURL": coverURL, - "formatHeat": formatHeat, - "formatCollect": formatCollectHeat, - "formatLikeCount": formatLikeCount, - "formatMediaTime": formatMediaTime, - "playletDisplayName": playletDisplayName, - "rankNo": RankNo, - "episodeLabel": episodeText, - "displayTag": displayTag, - "firstTagName": firstTagName, - "themeTagText": themeTagText, - "tagListURL": tagListURL, - "cardTag": cardTag, - "cardTags": cardTags, - "namedTags": namedTags, - "namedThemeTags": namedThemeTags, - "firstName": firstName, - "rankClass": rankClass, - "add": addInt, - "mod": modInt, - "contains": strings.Contains, - "hasPrefix": strings.HasPrefix, - "footerLinkURL": footerLinkURL, - "footerLinkIsExternal": footerLinkIsExternal, - "externalLinkAttrs": externalLinkAttrs, - "footerContactURL": footerContactURL, - "footerContactName": footerContactName, - "footerContactValue": footerContactValue, - "navDisplayURL": navDisplayURL, - "urlquery": url.QueryEscape, - "pathescape": url.PathEscape, - "metaLine": metaLine, - "adSlice": AdSlice, - "cssCover": cssCover, - "cssCoverPlaceholder": cssCoverPlaceholder, - "updateStatusText": updateStatusText, - "browseText": browseText, - "usableAnchor": usableAnchor, - "playletYearText": playletYearText, - "playletDateText": playletDateText, - "isPlayletAllFree": isPlayletAllFree, - "collectProgressText": collectProgressText, - "collectTagText": collectTagText, - "chapterCollectEpisodeLabel": chapterCollectEpisodeLabel, - "chapterCollectDesc": chapterCollectDesc, - "chapterCollectMeta": chapterCollectMeta, - "historyProgressText": historyProgressText, - "historyWatchLine": historyWatchLine, - "historyTagText": historyTagText, - "historyPlayURL": historyPlayURL, - "historyContinueMeta": historyContinueMeta, - "historyProgressPct": historyProgressPct, - "historyStatus": historyStatus, - "formatHistoryAt": formatHistoryAt, - "formatShortDateTime": formatShortDateTime, - "absInt64": absInt64Tpl, - "absURL": absURLTpl, - "canonicalOr": canonicalOr, - "joinStrings": joinStrings, - "dict": dict, + "detailURL": detailURL, + "playURL": playURL, + "chapterPlayURL": chapterPlayURL, + "categoryURL": categoryURL, + "categoryDetailURL": categoryDetailURL, + "themeListURL": themeListURLByKey, + "tagOptionsURL": tagListURLByKey, + "allListURL": AllListURL, + "moreListURL": MoreListURL, + "moreSeriesListURL": MoreSeriesListURL, + "navMegaIcon": NavMegaIcon, + "topicListURL": TopicListURL, + "topicDetailURL": TopicDetailURL, + "topicCoverURLs": TopicCoverURLs, + "tagPageURL": TagListURL, + "hotRankPageURL": HotRankPageURL, + "searchResultsURL": SearchResultsURL, + "coverURL": coverURL, + "formatHeat": formatHeat, + "formatCollect": formatCollectHeat, + "formatLikeCount": formatLikeCount, + "formatMediaTime": formatMediaTime, + "playletDisplayName": playletDisplayName, + "rankNo": RankNo, + "episodeLabel": episodeText, + "displayTag": displayTag, + "firstTagName": firstTagName, + "themeTagText": themeTagText, + "tagListURL": tagListURL, + "cardTag": cardTag, + "cardTags": cardTags, + "namedTags": namedTags, + "namedThemeTags": namedThemeTags, + "firstName": firstName, + "rankClass": rankClass, + "add": addInt, + "mod": modInt, + "contains": strings.Contains, + "hasPrefix": strings.HasPrefix, + "footerLinkURL": footerLinkURL, + "footerLinkIsExternal": footerLinkIsExternal, + "externalLinkAttrs": externalLinkAttrs, + "footerContactURL": footerContactURL, + "footerContactName": footerContactName, + "footerContactValue": footerContactValue, + "navDisplayURL": navDisplayURL, + "urlquery": url.QueryEscape, + "pathescape": url.PathEscape, + "metaLine": metaLine, + "adSlice": AdSlice, + "cssCover": cssCover, + "cssCoverPlaceholder": cssCoverPlaceholder, + "updateStatusText": updateStatusText, + "browseText": browseText, + "usableAnchor": usableAnchor, + "playletYearText": playletYearText, + "playletDateText": playletDateText, + "isPlayletAllFree": isPlayletAllFree, + "formatShortDateTime": formatShortDateTime, + "absInt64": absInt64Tpl, + "absURL": absURLTpl, + "canonicalOr": canonicalOr, + "joinStrings": joinStrings, + "dict": dict, } } diff --git a/app/service/rechargeser/list.go b/app/service/rechargeser/list.go deleted file mode 100644 index c81695e..0000000 --- a/app/service/rechargeser/list.go +++ /dev/null @@ -1,281 +0,0 @@ -package rechargeser - -import ( - "encoding/json" - "fmt" - "net/url" - "strconv" - "strings" - "videoweb/app/service/authser" - "videoweb/app/service/upstream" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -const ( - packageListPath = "/api/app/recharge/package/list" - coinBuyPath = "/api/app/coin/buy" - vipPayPath = "/api/app/vip/pay" - vipOrdersPath = "/api/app/vip/orders" - coinOrdersPath = "/api/app/coin/orders" - coinTransPath = "/api/app/coin/trans/list" - coinTransExportPath = "/api/app/coin/trans/export" -) - -// FetchPackageList 拉取购买套餐列表(会员 + 金币)。 -func FetchPackageList(c *gin.Context) (*RechargePackageListResp, error) { - var env APIEnvelope[RechargePackageListResp] - if err := upstream.GetJSON(c, packageListPath, &env); err != nil { - return nil, err - } - if env.Code != 200 { - return nil, fmt.Errorf("recharge package list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, nil -} - -// FetchPackageListOrEmpty 失败时返回空列表,保证 SSR 可渲染。 -func FetchPackageListOrEmpty(c *gin.Context) *RechargePackageListResp { - data, err := FetchPackageList(c) - if err != nil { - log.Error("fetch recharge package list failed", log.E(err)) - return &RechargePackageListResp{} - } - if data == nil { - return &RechargePackageListResp{} - } - return data -} - -// BuyCoin 购买金币套餐:POST /api/app/coin/buy -func BuyCoin(c *gin.Context, req BuyPayReq) (*APIEnvelope[PayOrderResp], error) { - var env APIEnvelope[PayOrderResp] - if _, err := upstream.PostJSON(c, coinBuyPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// VipPay 购买会员套餐:POST /api/app/vip/pay -func VipPay(c *gin.Context, req BuyPayReq) (*APIEnvelope[PayOrderResp], error) { - var env APIEnvelope[PayOrderResp] - if _, err := upstream.PostJSON(c, vipPayPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// NormalizePayMode 规范化支付方式。 -func NormalizePayMode(raw string) string { - v := strings.ToLower(strings.TrimSpace(raw)) - switch v { - case "wx", "weixin": - return "wechat" - case "unionpay": - return "union" - default: - return v - } -} - -// FetchVipOrders 查询 VIP 充值记录。 -func FetchVipOrders(c *gin.Context, page, pageSize int) ([]VipOrder, error) { - return fetchOrders(c, vipOrdersPath, page, pageSize) -} - -// FetchCoinOrders 查询金币购买记录。 -func FetchCoinOrders(c *gin.Context, page, pageSize int) ([]VipOrder, error) { - return fetchOrders(c, coinOrdersPath, page, pageSize) -} - -// FetchRecentOrdersOrEmpty 合并 VIP + 金币订单,失败返回空。 -func FetchRecentOrdersOrEmpty(c *gin.Context, limit int) []VipOrder { - if limit <= 0 { - limit = 20 - } - pageSize := limit - vipOrders, err1 := FetchVipOrders(c, 1, pageSize) - coinOrders, err2 := FetchCoinOrders(c, 1, pageSize) - if err1 != nil { - log.Error("fetch vip orders failed", log.E(err1)) - } - if err2 != nil { - log.Error("fetch coin orders failed", log.E(err2)) - } - out := make([]VipOrder, 0, len(vipOrders)+len(coinOrders)) - out = append(out, vipOrders...) - out = append(out, coinOrders...) - return out -} - -// FetchCoinTransList 查询金币余额明细:GET /api/app/coin/trans/list -// 第二个返回值为上游业务 code(网络/解码失败时为 0)。 -func FetchCoinTransList(c *gin.Context, page, pageSize int) ([]CoinTransListItem, int, error) { - data, code, err := FetchCoinTransPage(c, page, pageSize, 0) - if data == nil { - return nil, code, err - } - return data.List, code, err -} - -// FetchCoinTransPage 查询金币余额明细并保留分页信息。 -// typ: 0全部、1充值、2消费、3奖励。 -func FetchCoinTransPage(c *gin.Context, page, pageSize, typ int) (*CoinTransPage, int, error) { - if page <= 0 { - page = 1 - } - if pageSize <= 0 { - pageSize = 10 - } - if pageSize > 100 { - pageSize = 100 - } - if typ < 0 || typ > 3 { - typ = 0 - } - query := url.Values{} - query.Set("page", strconv.Itoa(page)) - query.Set("pageSize", strconv.Itoa(pageSize)) - query.Set("type", strconv.Itoa(typ)) - full := coinTransPath + "?" + query.Encode() - var env APIEnvelopeRaw - if err := upstream.GetJSON(c, full, &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("%s code=%d msg=%s", coinTransPath, env.Code, env.Msg) - } - return decodeCoinTransPage(env.Data, page, pageSize), env.Code, nil -} - -// FetchCoinTransExport 导出金币流水 CSV:GET /api/app/coin/trans/export?type= -// typ: 0全部、1充值、2消费、3奖励。 -func FetchCoinTransExport(c *gin.Context, typ int) (*upstream.BinaryResult, error) { - if typ < 0 || typ > 3 { - typ = 0 - } - query := url.Values{} - query.Set("type", strconv.Itoa(typ)) - full := coinTransExportPath + "?" + query.Encode() - result, err := upstream.GetBinary(c, full) - if err != nil { - return nil, err - } - if result == nil { - return nil, fmt.Errorf("%s empty response", coinTransExportPath) - } - if result.StatusCode < 200 || result.StatusCode >= 300 { - return result, fmt.Errorf("%s status=%d body=%s", coinTransExportPath, result.StatusCode, truncateBytes(result.Body, 200)) - } - // 上游偶发返回 JSON 错误包装 - ct := strings.ToLower(result.ContentType) - if strings.Contains(ct, "json") { - var env APIEnvelopeRaw - if json.Unmarshal(result.Body, &env) == nil && env.Code != 0 && env.Code != 200 { - return result, fmt.Errorf("%s code=%d msg=%s", coinTransExportPath, env.Code, env.Msg) - } - } - return result, nil -} - -func truncateBytes(b []byte, n int) string { - s := string(b) - if len(s) <= n { - return s - } - return s[:n] + "..." -} - -// FetchCoinTransListOrEmpty 失败返回空列表;游客会话失效时会尝试重登后重试一次。 -func FetchCoinTransListOrEmpty(c *gin.Context, page, pageSize int) []CoinTransListItem { - list, code, err := FetchCoinTransList(c, page, pageSize) - if err == nil { - return list - } - if retry, _ := authser.OnSessionExpired(c, code); retry { - list, _, err = FetchCoinTransList(c, page, pageSize) - if err == nil { - return list - } - } - log.Error("fetch coin trans list failed", log.E(err)) - return nil -} - -func decodeCoinTransList(raw json.RawMessage) []CoinTransListItem { - return decodeCoinTransPage(raw, 1, 10).List -} - -func decodeCoinTransPage(raw json.RawMessage, page, pageSize int) *CoinTransPage { - empty := &CoinTransPage{PageNum: page, PageSize: pageSize} - if len(raw) == 0 || string(raw) == "null" { - return empty - } - var arr []CoinTransListItem - if err := json.Unmarshal(raw, &arr); err == nil { - empty.List = arr - empty.TotalCount = int64(len(arr)) - return empty - } - var obj CoinTransPage - if err := json.Unmarshal(raw, &obj); err != nil { - return empty - } - if obj.PageNum <= 0 { - obj.PageNum = page - } - if obj.PageSize <= 0 { - obj.PageSize = pageSize - } - return &obj -} - -func fetchOrders(c *gin.Context, path string, page, pageSize int) ([]VipOrder, error) { - if page <= 0 { - page = 1 - } - if pageSize <= 0 { - pageSize = 10 - } - full := fmt.Sprintf("%s?page=%d&pageSize=%d", path, page, pageSize) - var env APIEnvelopeRaw - if err := upstream.GetJSON(c, full, &env); err != nil { - return nil, err - } - if env.Code != 200 { - return nil, fmt.Errorf("%s code=%d msg=%s", path, env.Code, env.Msg) - } - return decodeOrderList(env.Data), nil -} - -func decodeOrderList(raw json.RawMessage) []VipOrder { - if len(raw) == 0 || string(raw) == "null" { - return nil - } - var arr []VipOrder - if err := json.Unmarshal(raw, &arr); err == nil { - return arr - } - var obj struct { - List []VipOrder `json:"list"` - Orders []VipOrder `json:"orders"` - Records []VipOrder `json:"records"` - Items []VipOrder `json:"items"` - } - if err := json.Unmarshal(raw, &obj); err != nil { - return nil - } - switch { - case len(obj.List) > 0: - return obj.List - case len(obj.Orders) > 0: - return obj.Orders - case len(obj.Records) > 0: - return obj.Records - case len(obj.Items) > 0: - return obj.Items - default: - return nil - } -} diff --git a/app/service/rechargeser/types.go b/app/service/rechargeser/types.go deleted file mode 100644 index 1c35892..0000000 --- a/app/service/rechargeser/types.go +++ /dev/null @@ -1,125 +0,0 @@ -package rechargeser - -import "encoding/json" - -// PackageType 对应 rechargepackagemod.PackageType -const ( - PackageTypeVip = 1 // 会员畅享 - PackageTypeCoin = 2 // 金币充值 -) - -// PayMethodInfo 对应 swagger proto.PayMethodInfo -type PayMethodInfo struct { - Type string `json:"type"` // alipay / wechat / union - TypeName string `json:"typeName"` // 支付宝 / 微信 / 银联 -} - -// RechargePackageItem 对应 swagger proto.RechargePackageItem -type RechargePackageItem struct { - ID int64 `json:"id"` - Name string `json:"name"` - SubTitle string `json:"subTitle"` - PackageType int `json:"packageType"` - Price int64 `json:"price"` // 售价,分 - OriginalPrice int64 `json:"originalPrice"` // 原价,分 - BaseCoin int64 `json:"baseCoin"` - GiftCoin int64 `json:"giftCoin"` - TotalCoin int64 `json:"totalCoin"` - DurationDays int `json:"durationDays"` - DurationHours int `json:"durationHours"` - Sort int `json:"sort"` - PayMethods []PayMethodInfo `json:"payMethods"` -} - -// RechargePackageListResp 对应 swagger proto.RechargePackageListResp -// GET /api/app/recharge/package/list -type RechargePackageListResp struct { - VipList []RechargePackageItem `json:"vipList"` - CoinList []RechargePackageItem `json:"coinList"` -} - -// BuyPayReq 对应 coinapi.buyCoinReq / rechargeapi.submitOrderReq -type BuyPayReq struct { - PackageID int64 `json:"packageId"` - PayMode string `json:"payMode"` // alipay / wechat / union ... -} - -// PayOrderResp 对应 coinser.BuyCoinResp / rechargeser.SubmitOrderResp -type PayOrderResp struct { - IsOpenNewBrowser bool `json:"isOpenNewBrowser"` - PayURL string `json:"payUrl"` - PayURLMode string `json:"payUrlMode"` - TradeNo string `json:"tradeNo"` -} - -// VipOrder 对应 swagger vipordermod.VipOrder(会员/金币订单共用结构) -type VipOrder struct { - ID int64 `json:"id"` - UserID int64 `json:"userId"` - OrderType int `json:"orderType"` // 1会员 2金币 - Status int `json:"status"` // 1待支付 2成功 3失败 - Amount int64 `json:"amount"` // 分 - Coin int64 `json:"coin"` - CardID int64 `json:"cardId"` - CardName string `json:"cardName"` - CoinCfgID int64 `json:"coinCfgId"` - PayMode string `json:"payMode"` - TradeNo string `json:"tradeNo"` - CreatedAt string `json:"createdAt"` - FinishedAt string `json:"finishedAt"` -} - -// CoinTransListItem 对应 swagger proto.CoinTransListItemResp -// GET /api/app/coin/trans/list -type CoinTransListItem struct { - ID int64 `json:"id"` - Title string `json:"title"` - Remark string `json:"remark"` - TranType int `json:"tranType"` - TranTypeName string `json:"tranTypeName"` - FlowDir int `json:"flowDir"` // 1收入 -1支出 - FlowDirName string `json:"flowDirName"` - Amount int64 `json:"amount"` - ChangeAmount int64 `json:"changeAmount"` - BalanceBefore int64 `json:"balanceBefore"` - BalanceAfter int64 `json:"balanceAfter"` - OrderNo string `json:"orderNo"` - RelatedID int64 `json:"relatedId"` - CreatedAt string `json:"createdAt"` -} - -// CoinTransPage 对应 response.BasePageResp 与金币流水列表组合。 -type CoinTransPage struct { - List []CoinTransListItem `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -const ( - OrderTypeVip = 1 - OrderTypeCoin = 2 - - OrderStatusPending = 1 - OrderStatusSuccess = 2 - OrderStatusFailed = 3 - - FlowDirIn = 1 - FlowDirOut = -1 -) - -// APIEnvelope 上游统一响应壳 -type APIEnvelope[T any] struct { - Code int `json:"code"` - Msg string `json:"msg"` - Tip string `json:"tip"` - Data T `json:"data"` -} - -// APIEnvelopeRaw Data 形态不固定时使用。 -type APIEnvelopeRaw struct { - Code int `json:"code"` - Msg string `json:"msg"` - Tip string `json:"tip"` - Data json.RawMessage `json:"data"` -} diff --git a/app/service/rechargeser/view.go b/app/service/rechargeser/view.go deleted file mode 100644 index b0eab1c..0000000 --- a/app/service/rechargeser/view.go +++ /dev/null @@ -1,496 +0,0 @@ -package rechargeser - -import ( - "encoding/json" - "fmt" - "sort" - "strconv" - "strings" -) - -// PayMethodVM 支付方式展示。 -type PayMethodVM struct { - Type string - TypeName string - Active bool -} - -// PackageCardVM 充值页套餐卡片(字段来自 package/list)。 -type PackageCardVM struct { - ID int64 - Name string - Hint string // 优先 subTitle - BaseCoinText string - GiftCoinText string - TotalCoinText string - PriceText string // 售价(元),不含 ¥ - OriginalPriceText string // 原价(元),仅当原价>售价时有值 - PriceFen int64 - Active bool - PayMethods []PayMethodVM - PayMethodsJSON string // 供 data-pay-methods 使用 -} - -// DepositPageVM 充值页模板数据。 -type DepositPageVM struct { - VipList []PackageCardVM - CoinList []PackageCardVM - PayMethods []PayMethodVM // 当前选中套餐的支付方式(接口 payMethods) - // TxList 交易明细:直接透传 /api/app/coin/trans/list 返回的字段 - //(不再做 Name/TimeText/AmountText 的自定义字段映射)。 - TxList []CoinTransListItem -} - -// TxItemVM 交易明细行(历史遗留:旧逻辑会把 CoinTransListItem 映射到此结构)。 -// 当前“交易明细调试”需求:DepositPageVM 直接透传 CoinTransListItem, -// 但旧函数可能仍被其它页面复用,因此保留结构体定义。 -type TxItemVM struct { - Name string `json:"name"` - Kind string `json:"kind"` - Description string `json:"description"` - Category string `json:"category"` - TimeText string `json:"time"` - MobileTime string `json:"mobileTime"` - AmountText string `json:"amount"` - AmountClass string `json:"amountClass"` // deposit-tx-amount--plus / --minus - Credit bool `json:"credit"` - StatusText string `json:"status"` -} - -var defaultPayMethods = []PayMethodInfo{ - {Type: "alipay", TypeName: "支付宝"}, - {Type: "wechat", TypeName: "微信"}, - {Type: "union", TypeName: "银联"}, -} - -// BuildDepositPageVM 按接口 vipList / coinList 组装展示数据(支付方式不造假)。 -// preferTab: "coin" 优先选中金币套餐,其它/空则优先会员。 -func BuildDepositPageVM(raw *RechargePackageListResp, preferTab string) DepositPageVM { - if raw == nil { - return DepositPageVM{} - } - vip := buildCards(raw.VipList, true) - coin := buildCards(raw.CoinList, false) - activatePreferredPackage(vip, coin, preferTab) - - var activePays []PayMethodVM - for _, item := range coin { - if item.Active { - activePays = item.PayMethods - break - } - } - if activePays == nil { - for _, item := range vip { - if item.Active { - activePays = item.PayMethods - break - } - } - } - if len(activePays) > 0 { - activePays[0].Active = true - } - return DepositPageVM{ - VipList: vip, - CoinList: coin, - PayMethods: activePays, - } -} - -// activatePreferredPackage 默认选中:优先有支付方式的套餐,否则选第一项。 -// preferTab=coin 时优先金币,否则优先会员。 -func activatePreferredPackage(vip, coin []PackageCardVM, preferTab string) { - pick := func(list []PackageCardVM) bool { - for i := range list { - if len(list[i].PayMethods) > 0 { - list[i].Active = true - return true - } - } - if len(list) > 0 { - list[0].Active = true - return true - } - return false - } - preferCoin := strings.EqualFold(strings.TrimSpace(preferTab), "coin") - if preferCoin { - if pick(coin) { - return - } - pick(vip) - return - } - if pick(vip) { - return - } - pick(coin) -} - -// AttachTxList 附加交易明细(金币流水)。 -func (vm *DepositPageVM) AttachTxList(list []CoinTransListItem) { - if vm == nil { - return - } - vm.TxList = list -} - -// BuildTxListFromCoinTrans 将金币流水转为展示列表。 -func BuildTxListFromCoinTrans(list []CoinTransListItem, limit int) []TxItemVM { - items := append([]CoinTransListItem(nil), list...) - if limit > 0 && len(items) > limit { - items = items[:limit] - } - out := make([]TxItemVM, 0, len(items)) - for _, item := range items { - name := strings.TrimSpace(item.Title) - if name == "" { - name = strings.TrimSpace(item.TranTypeName) - } - if name == "" { - name = strings.TrimSpace(item.Remark) - } - if name == "" { - name = "金币变动" - } - change := item.ChangeAmount - if change == 0 { - change = item.Amount - if item.FlowDir == FlowDirOut { - change = -change - } - } - amountClass := "deposit-tx-amount--plus" - amountText := "+" + formatCount(absInt64(change)) + " 金币" - if change < 0 || item.FlowDir == FlowDirOut { - amountClass = "deposit-tx-amount--minus" - amountText = "-" + formatCount(absInt64(change)) + " 金币" - } - out = append(out, TxItemVM{ - Name: name, - Kind: transactionKind(item), - Description: name, - Category: transactionCategory(item), - TimeText: formatTransTime(item.CreatedAt), - MobileTime: formatMobileTransTime(item.CreatedAt), - AmountText: amountText, - AmountClass: amountClass, - Credit: change >= 0 && item.FlowDir != FlowDirOut, - StatusText: strings.TrimSpace(item.FlowDirName), - }) - } - return out -} - -func transactionKind(item CoinTransListItem) string { - if name := strings.TrimSpace(item.TranTypeName); name != "" { - return name - } - if item.FlowDir == FlowDirOut { - return "消费" - } - return "收入" -} - -func transactionCategory(item CoinTransListItem) string { - switch item.TranType { - case 1: - return "topup" - case 2: - return "reward" - case 4: - return "spend" - default: - if item.FlowDir == FlowDirOut { - return "spend" - } - return "reward" - } -} - -func formatMobileTransTime(raw string) string { - formatted := formatTransTime(raw) - if len(formatted) >= 16 { - return formatted[5:16] - } - return formatted -} - -func absInt64(n int64) int64 { - if n < 0 { - return -n - } - return n -} - -func formatTransTime(raw string) string { - raw = strings.TrimSpace(raw) - if raw == "" || strings.HasPrefix(raw, "0001") { - return "" - } - raw = strings.ReplaceAll(raw, "T", " ") - if i := strings.Index(raw, "."); i > 0 { - raw = raw[:i] - } - if i := strings.IndexAny(raw, "Z+"); i > 10 { - raw = strings.TrimSpace(raw[:i]) - } - if len(raw) > 19 { - raw = raw[:19] - } - return raw -} - -// BuildTxList 将订单转为展示列表(按时间倒序)。保留供订单场景复用。 -func BuildTxList(orders []VipOrder, limit int) []TxItemVM { - items := append([]VipOrder(nil), orders...) - sort.SliceStable(items, func(i, j int) bool { - return orderTimeKey(items[i]) > orderTimeKey(items[j]) - }) - if limit > 0 && len(items) > limit { - items = items[:limit] - } - out := make([]TxItemVM, 0, len(items)) - for _, o := range items { - name := strings.TrimSpace(o.CardName) - if name == "" { - if o.OrderType == OrderTypeCoin || o.CoinCfgID > 0 { - name = "金币充值" - } else { - name = "会员充值" - } - } - status := orderStatusText(o.Status) - if status != "" { - name = name + " · " + status - } - amountClass := "deposit-tx-amount--minus" - amountText := "-¥" + FormatYuan(o.Amount) - if o.OrderType == OrderTypeCoin || o.Coin > 0 { - if o.Status == OrderStatusSuccess && o.Coin > 0 { - amountClass = "deposit-tx-amount--plus" - amountText = "+" + formatCount(o.Coin) + " 金币" - } - } - out = append(out, TxItemVM{ - Name: name, - TimeText: formatOrderTime(o), - AmountText: amountText, - AmountClass: amountClass, - StatusText: status, - }) - } - return out -} - -func orderTimeKey(o VipOrder) string { - if t := strings.TrimSpace(o.FinishedAt); t != "" && !strings.HasPrefix(t, "0001") { - return t - } - return strings.TrimSpace(o.CreatedAt) -} - -func formatOrderTime(o VipOrder) string { - raw := orderTimeKey(o) - if raw == "" || strings.HasPrefix(raw, "0001") { - return "" - } - // 常见 RFC3339 / 带 T 的时间,截成可读 - raw = strings.ReplaceAll(raw, "T", " ") - if i := strings.Index(raw, "."); i > 0 { - raw = raw[:i] - } - if i := strings.IndexAny(raw, "Z+"); i > 10 { - raw = strings.TrimSpace(raw[:i]) - } - if len(raw) > 19 { - raw = raw[:19] - } - return raw -} - -func orderStatusText(status int) string { - switch status { - case OrderStatusPending: - return "待支付" - case OrderStatusSuccess: - return "已完成" - case OrderStatusFailed: - return "失败" - default: - return "" - } -} - -func buildCards(list []RechargePackageItem, isVIP bool) []PackageCardVM { - items := append([]RechargePackageItem(nil), list...) - // 仅按接口 sort 排序;sort 相同保持接口原顺序 - sort.SliceStable(items, func(i, j int) bool { - return items[i].Sort < items[j].Sort - }) - out := make([]PackageCardVM, 0, len(items)) - for _, item := range items { - name := strings.TrimSpace(item.Name) - if name == "" { - if isVIP { - name = "会员套餐" - } else { - name = "金币套餐" - } - } - pays := buildPayMethodVMs(item.PayMethods, false) - card := PackageCardVM{ - ID: item.ID, - Name: name, - Hint: packageHint(item, isVIP), - BaseCoinText: formatCount(item.BaseCoin), - GiftCoinText: formatCount(item.GiftCoin), - TotalCoinText: formatCount(packageTotalCoin(item)), - PriceText: FormatYuan(item.Price), - PriceFen: item.Price, - PayMethods: pays, - PayMethodsJSON: marshalPayMethodsJSON(pays), - } - if item.OriginalPrice > item.Price && item.OriginalPrice > 0 { - card.OriginalPriceText = FormatYuan(item.OriginalPrice) - } - out = append(out, card) - } - return out -} - -func packageTotalCoin(item RechargePackageItem) int64 { - if item.TotalCoin > 0 { - return item.TotalCoin - } - return item.BaseCoin + item.GiftCoin -} - -func buildPayMethodVMs(list []PayMethodInfo, withFallback bool) []PayMethodVM { - src := list - if len(src) == 0 && withFallback { - src = defaultPayMethods - } - out := make([]PayMethodVM, 0, len(src)) - seen := map[string]struct{}{} - for _, item := range src { - typ := strings.ToLower(strings.TrimSpace(item.Type)) - name := strings.TrimSpace(item.TypeName) - if typ == "" && name == "" { - continue - } - if typ == "" { - typ = strings.ToLower(name) - } - if name == "" { - name = defaultPayTypeName(typ) - } - if _, ok := seen[typ]; ok { - continue - } - seen[typ] = struct{}{} - out = append(out, PayMethodVM{Type: typ, TypeName: name}) - } - if len(out) == 0 && withFallback { - return buildPayMethodVMs(defaultPayMethods, false) - } - return out -} - -func defaultPayTypeName(typ string) string { - switch typ { - case "alipay": - return "支付宝" - case "wechat", "wx", "weixin": - return "微信" - case "union", "unionpay": - return "银联" - case "dcep", "ecny": - return "数字人民币" - case "usdt": - return "USDT" - default: - if typ == "" { - return "支付" - } - return typ - } -} - -func marshalPayMethodsJSON(list []PayMethodVM) string { - type row struct { - Type string `json:"type"` - TypeName string `json:"typeName"` - } - rows := make([]row, 0, len(list)) - for _, item := range list { - rows = append(rows, row{Type: item.Type, TypeName: item.TypeName}) - } - b, err := json.Marshal(rows) - if err != nil || len(b) == 0 { - return "[]" - } - return string(b) -} - -func packageHint(item RechargePackageItem, isVIP bool) string { - // 优先接口 subTitle - if s := strings.TrimSpace(item.SubTitle); s != "" { - return s - } - if isVIP { - if item.DurationDays > 0 { - return fmt.Sprintf("%d天会员", item.DurationDays) - } - if item.DurationHours > 0 { - return fmt.Sprintf("%d小时会员", item.DurationHours) - } - return "" - } - base := item.BaseCoin - gift := item.GiftCoin - total := item.TotalCoin - if total <= 0 { - total = base + gift - } - if gift > 0 && base > 0 { - return fmt.Sprintf("基础 %s + 赠送 %s", formatCount(base), formatCount(gift)) - } - if total > 0 { - return formatCount(total) + " 金币" - } - return "" -} - -// FormatYuan 分转元展示(整数不带小数)。 -func FormatYuan(fen int64) string { - if fen < 0 { - fen = 0 - } - if fen%100 == 0 { - return strconv.FormatInt(fen/100, 10) - } - return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", float64(fen)/100), "0"), ".") -} - -func formatCount(n int64) string { - if n < 0 { - n = 0 - } - s := strconv.FormatInt(n, 10) - if len(s) <= 3 { - return s - } - var b strings.Builder - rem := len(s) % 3 - if rem == 0 { - rem = 3 - } - b.WriteString(s[:rem]) - for i := rem; i < len(s); i += 3 { - b.WriteByte(',') - b.WriteString(s[i : i+3]) - } - return b.String() -} diff --git a/app/service/translationser/list.go b/app/service/translationser/list.go index dd1527d..ebe4676 100644 --- a/app/service/translationser/list.go +++ b/app/service/translationser/list.go @@ -2,6 +2,7 @@ package translationser import ( "fmt" + "strings" "sync" "time" "videoweb/app/service/upstream" @@ -67,11 +68,23 @@ func filterHeaderCategories(info *AppTranslationInfo) []TranslationCategory { if cat.Name == "" && cat.URLValue == "" { continue } + // 网页版已移除账号体系,后台若仍配置个人中心/已购入口则不展示。 + if isAccountNavPath(cat.URLValue) { + continue + } out = append(out, cat) } return out } +func isAccountNavPath(raw string) bool { + switch strings.TrimRight(strings.TrimSpace(raw), "/") { + case "/user-center", "user-center", "/profile", "profile", "/purchased-series", "purchased-series", "/purchased", "purchased": + return true + } + return false +} + // DefaultCategoryList 接口失败时的兜底导航(字段与接口一致)。 func DefaultCategoryList() []TranslationCategory { return []TranslationCategory{ diff --git a/app/service/translationser/view.go b/app/service/translationser/view.go index 50d82af..f8bb907 100644 --- a/app/service/translationser/view.go +++ b/app/service/translationser/view.go @@ -290,10 +290,6 @@ func rewriteNavPath(path string) string { return "/popular-recommend" case "/tag", "/tag-list": return "/tags" - case "/user-center": - return "/profile" - case "/purchased-series": - return "/purchased" case "/search-results": return "/search" case "/more": @@ -455,8 +451,6 @@ func navActiveKey(href, name string, download bool) string { return "aiSeries" case "/home-road": return "homeRoad" - case "/profile": - return "userCenter" case "/download-app": return "downloadApp" default: diff --git a/app/service/usercenterser/center.go b/app/service/usercenterser/center.go deleted file mode 100644 index fdb4713..0000000 --- a/app/service/usercenterser/center.go +++ /dev/null @@ -1,42 +0,0 @@ -package usercenterser - -import ( - "encoding/json" - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const centerDataPath = "/api/app/user/center/data" - -// Data 对应 Swagger proto.UserCenterDataResp。 -type Data struct { - WatchCount int64 `json:"watchCount"` - CollectCount int64 `json:"collectCount"` - ChasingCount int64 `json:"chasingCount"` - BoughtChapterCount int64 `json:"boughtChapterCount"` -} - -// Fetch 获取当前用户的个人中心统计。code 为上游业务码,供会话失效重试。 -func Fetch(c *gin.Context) (*Data, int, error) { - var env struct { - Code int `json:"code"` - Msg string `json:"msg"` - Data json.RawMessage `json:"data"` - } - if err := upstream.GetJSON(c, centerDataPath, &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("user center data code=%d msg=%s", env.Code, env.Msg) - } - var data Data - if len(env.Data) == 0 || string(env.Data) == "null" { - return nil, env.Code, fmt.Errorf("user center data is empty") - } - if err := json.Unmarshal(env.Data, &data); err != nil { - return nil, env.Code, fmt.Errorf("decode user center data: %w", err) - } - return &data, env.Code, nil -} diff --git a/app/service/videoser/collect.go b/app/service/videoser/collect.go deleted file mode 100644 index c591f7e..0000000 --- a/app/service/videoser/collect.go +++ /dev/null @@ -1,130 +0,0 @@ -package videoser - -import ( - "fmt" - "net/url" - "strconv" - "videoweb/app/service/authser" - "videoweb/app/service/upstream" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -const ( - collectSetPath = "/api/app/video/collect/set" - collectListPath = "/api/app/video/collect/list" - defaultCollectPageSize = 40 -) - -// VideoCollectSetReq 对应 swagger proto.VideoCollectSetReq。 -// POST /api/app/video/collect/set -type VideoCollectSetReq struct { - ID int64 `json:"id"` - IsCollect bool `json:"isCollect"` -} - -// VideoCollectSetResp 对应 swagger proto.VideoCollectSetResp。 -type VideoCollectSetResp struct { - ID int64 `json:"id"` - CountCollect int64 `json:"countCollect"` - IsCollect int `json:"isCollect"` // 1-已收藏 2-未收藏 -} - -// VideoCollectData 对应 swagger proto.VideoCollectData。 -// GET /api/app/video/collect/list -type VideoCollectData struct { - ID int64 `json:"id"` - Title string `json:"title"` - Slug string `json:"slug"` - CoverImg string `json:"coverImg"` - Keyworld []string `json:"keyworld"` - Tags []int64 `json:"tags"` - VideoTime int `json:"videoTime"` - WatchTimes int64 `json:"watchTimes"` - CountLike int64 `json:"countLike"` - CountCollect int64 `json:"countCollect"` - CountComment int64 `json:"countComment"` - IsCollect int `json:"isCollect"` - CollectedAt string `json:"collectedAt"` -} - -// VideoCollectListResp 对应 swagger proto.VideoCollectListResp。 -type VideoCollectListResp struct { - List []VideoCollectData `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -// CollectListQuery 视频收藏列表分页。 -type CollectListQuery struct { - PageNum int - PageSize int -} - -// Normalize 校正分页默认值。 -func (q *CollectListQuery) Normalize() { - if q.PageNum <= 0 { - q.PageNum = 1 - } - if q.PageSize <= 0 { - q.PageSize = defaultCollectPageSize - } - if q.PageSize > 100 { - q.PageSize = 100 - } -} - -// CollectSet 设置视频收藏/取消收藏。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_collect_set -func CollectSet(c *gin.Context, req VideoCollectSetReq) (*APIEnvelope[VideoCollectSetResp], error) { - if req.ID <= 0 { - return nil, fmt.Errorf("video collect invalid id") - } - var env APIEnvelope[VideoCollectSetResp] - if _, err := upstream.PostJSON(c, collectSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} - -// FetchCollectList 拉取视频收藏列表:GET /api/app/video/collect/list。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_collect_list -func FetchCollectList(c *gin.Context, q CollectListQuery) (*VideoCollectListResp, int, error) { - q.Normalize() - params := url.Values{} - params.Set("pageNum", strconv.Itoa(q.PageNum)) - params.Set("pageSize", strconv.Itoa(q.PageSize)) - - var env APIEnvelope[VideoCollectListResp] - if err := upstream.GetJSON(c, collectListPath+"?"+params.Encode(), &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("video collect list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, env.Code, nil -} - -// FetchCollectListOrEmpty 失败返回空列表;游客会话失效时尝试重登后重试一次。 -func FetchCollectListOrEmpty(c *gin.Context, q CollectListQuery) *VideoCollectListResp { - data, code, err := FetchCollectList(c, q) - if err == nil { - if data == nil { - return &VideoCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - if retry, _ := authser.OnSessionExpired(c, code); retry { - data, _, err = FetchCollectList(c, q) - if err == nil { - if data == nil { - return &VideoCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - } - log.Error("fetch video collect list failed", log.E(err)) - return &VideoCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize} -} diff --git a/app/service/videoser/history.go b/app/service/videoser/history.go deleted file mode 100644 index 33c1c44..0000000 --- a/app/service/videoser/history.go +++ /dev/null @@ -1,141 +0,0 @@ -package videoser - -import ( - "fmt" - "net/url" - "strconv" - "videoweb/app/service/authser" - "videoweb/app/service/upstream" - "videoweb/pub/common/log" - - "github.com/gin-gonic/gin" -) - -const ( - videoHistoryListPath = "/api/app/video/history/list" - videoHistoryClearPath = "/api/app/video/history/clear" - videoHistoryDeletePath = "/api/app/video/history/delete" - defaultHistoryPageSize = 40 -) - -// VideoHistoryData 对应 swagger proto.VideoHistoryData。 -// GET /api/app/video/history/list -type VideoHistoryData struct { - ID int64 `json:"id"` - Title string `json:"title"` - Slug string `json:"slug"` - CoverImg string `json:"coverImg"` - Keyworld []string `json:"keyworld"` - Tags []int64 `json:"tags"` - VideoTime int `json:"videoTime"` - WatchTimes int64 `json:"watchTimes"` - CountLike int64 `json:"countLike"` - CountCollect int64 `json:"countCollect"` - CountComment int64 `json:"countComment"` - IsCollect int `json:"isCollect"` - HistoryAt string `json:"historyAt"` -} - -// VideoHistoryListResp 对应 swagger proto.VideoHistoryListResp。 -type VideoHistoryListResp struct { - List []VideoHistoryData `json:"list"` - PageNum int `json:"pageNum"` - PageSize int `json:"pageSize"` - TotalCount int64 `json:"totalCount"` -} - -// VideoHistoryClearResp 对应 swagger proto.VideoHistoryClearResp。 -type VideoHistoryClearResp struct { - DeletedCount int `json:"deletedCount"` -} - -// VideoHistoryDeleteReq 对应 swagger proto.VideoHistoryDeleteReq。 -type VideoHistoryDeleteReq struct { - VideoID int64 `json:"videoId"` -} - -// VideoHistoryDeleteResp 对应 swagger proto.VideoHistoryDeleteResp。 -type VideoHistoryDeleteResp struct { - DeletedCount int `json:"deletedCount"` -} - -// HistoryListQuery 视频观看历史分页。 -type HistoryListQuery struct { - PageNum int - PageSize int -} - -// Normalize 校正分页默认值。 -func (q *HistoryListQuery) Normalize() { - if q.PageNum <= 0 { - q.PageNum = 1 - } - if q.PageSize <= 0 { - q.PageSize = defaultHistoryPageSize - } - if q.PageSize > 100 { - q.PageSize = 100 - } -} - -// FetchHistoryList 分页查询视频观看历史:GET /api/app/video/history/list。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/get_api_app_video_history_list -func FetchHistoryList(c *gin.Context, q HistoryListQuery) (*VideoHistoryListResp, int, error) { - q.Normalize() - params := url.Values{} - params.Set("pageNum", strconv.Itoa(q.PageNum)) - params.Set("pageSize", strconv.Itoa(q.PageSize)) - - var env APIEnvelope[VideoHistoryListResp] - if err := upstream.GetJSON(c, videoHistoryListPath+"?"+params.Encode(), &env); err != nil { - return nil, 0, err - } - if env.Code != 200 { - return nil, env.Code, fmt.Errorf("video history list code=%d msg=%s", env.Code, env.Msg) - } - return &env.Data, env.Code, nil -} - -// FetchHistoryListOrEmpty 失败返回空列表;游客会话失效时尝试重登后重试一次。 -func FetchHistoryListOrEmpty(c *gin.Context, q HistoryListQuery) *VideoHistoryListResp { - data, code, err := FetchHistoryList(c, q) - if err == nil { - if data == nil { - return &VideoHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - if retry, _ := authser.OnSessionExpired(c, code); retry { - data, _, err = FetchHistoryList(c, q) - if err == nil { - if data == nil { - return &VideoHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize} - } - return data - } - } - log.Error("fetch video history list failed", log.E(err)) - return &VideoHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize} -} - -// HistoryClear 清空视频观看历史:POST /api/app/video/history/clear。 -func HistoryClear(c *gin.Context) (*APIEnvelope[VideoHistoryClearResp], error) { - var env APIEnvelope[VideoHistoryClearResp] - if _, err := upstream.PostJSON(c, videoHistoryClearPath, nil, &env); err != nil { - return nil, err - } - return &env, nil -} - -// HistoryDelete 删除单条视频观看历史:POST /api/app/video/history/delete。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_history_delete -func HistoryDelete(c *gin.Context, req VideoHistoryDeleteReq) (*APIEnvelope[VideoHistoryDeleteResp], error) { - if req.VideoID <= 0 { - return nil, fmt.Errorf("video history delete invalid videoId") - } - var env APIEnvelope[VideoHistoryDeleteResp] - if _, err := upstream.PostJSON(c, videoHistoryDeletePath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/service/videoser/like.go b/app/service/videoser/like.go deleted file mode 100644 index 866374d..0000000 --- a/app/service/videoser/like.go +++ /dev/null @@ -1,37 +0,0 @@ -package videoser - -import ( - "fmt" - "videoweb/app/service/upstream" - - "github.com/gin-gonic/gin" -) - -const likeSetPath = "/api/app/video/like/set" - -// VideoLikeSetReq 对应 swagger proto.VideoLikeSetReq。 -// POST /api/app/video/like/set -type VideoLikeSetReq struct { - ID int64 `json:"id"` - IsLike bool `json:"isLike"` -} - -// VideoLikeSetResp 对应 swagger proto.VideoLikeSetResp。 -type VideoLikeSetResp struct { - ID int64 `json:"id"` - CountLike int64 `json:"countLike"` - IsLike int `json:"isLike"` // 1-已点赞 2-未点赞 -} - -// LikeSet 设置视频点赞/取消点赞。 -// 文档:http://172.104.181.99:25319/swagger/index.html#/视频相关接口/post_api_app_video_like_set -func LikeSet(c *gin.Context, req VideoLikeSetReq) (*APIEnvelope[VideoLikeSetResp], error) { - if req.ID <= 0 { - return nil, fmt.Errorf("video like invalid id") - } - var env APIEnvelope[VideoLikeSetResp] - if _, err := upstream.PostJSON(c, likeSetPath, req, &env); err != nil { - return nil, err - } - return &env, nil -} diff --git a/app/static/common/web-sdk-v1.2.0.js b/app/static/common/web-sdk-v1.2.0.js deleted file mode 100644 index 250d471..0000000 --- a/app/static/common/web-sdk-v1.2.0.js +++ /dev/null @@ -1 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).WebSDK=t()}(this,function(){"use strict";function e(e){var t,n,r,o,i,a,s,c,u,l,d,_,p,f,h,g,m,y,v,w,k,b,C,S,P=function(e){var t,n,r,o,i,a;for(t=(null==e?"":e)+"",n=[],r=0;t.length>r;r++)128>(o=t.charCodeAt(r))?n.push(o):2048>o?n.push(192|o>>6,128|63&o):55296>o||o>56319||r+1>=t.length||56320>(i=t.charCodeAt(r+1))||i>57343?n.push(224|o>>12,128|o>>6&63,128|63&o):(n.push(240|(a=i-56320+(o-55296<<10)+65536)>>18,128|a>>12&63,128|a>>6&63,128|63&a),r++);return n}((null==e?"":e)+""),E=8*P.length;for(P.push(128);P.length%64!=56;)P.push(0);for(t=0;8>t;t++)P.push(E>>>8*t&255);for(n=function(e,t){return e<>>32-t},r=function(e,t){return((e>>>0)+(t>>>0)&4294967295)>>>0},o=function(e,t,n,o){return r(r(e,t),r(n,o))},i=[7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22,5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20,4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23,6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21],a=Array(64),s=0;64>s;s++)a[s]=Math.floor(4294967296*Math.abs(Math.sin(s+1)))>>>0;for(c=1732584193,u=4023233417,l=2562383102,d=271733878,_=0;P.length>_;_+=64){for(p=Array(16),f=0;16>f;f++)p[f]=(P[h=_+4*f]|P[h+1]<<8|P[h+2]<<16|P[h+3]<<24)>>>0;for(g=c,m=u,y=l,v=d,w=0;64>w;w++)16>w?(k=m&y|~m&v,b=w):32>w?(k=v&m|~v&y,b=(5*w+1)%16):48>w?(k=m^y^v,b=(3*w+5)%16):(k=y^(m|~v),b=7*w%16),C=v,v=y,y=m,m=r(m,n(o(g,k,a[w],p[b]),i[w])),g=C;c=r(c,g),u=r(u,m),l=r(l,y),d=r(d,v)}return S=function(e){for(var t="",n=0;4>n;n++)t+=(256+(e>>>8*n&255)).toString(16).slice(1);return t},S(c)+S(u)+S(l)+S(d)}function t(e,t){return(e>>>t|e<<32-t)>>>0}function n(e,t,n){return(e&t^~e&n)>>>0}function r(e,t,n){return(e&t^e&n^t&n)>>>0}function o(e){return(t(e,2)^t(e,13)^t(e,22))>>>0}function i(e){return(t(e,6)^t(e,11)^t(e,25))>>>0}function a(e){return(t(e,7)^t(e,18)^e>>>3)>>>0}function s(e){return(t(e,17)^t(e,19)^e>>>10)>>>0}function c(e){for(var t=(e>>>0).toString(16);8>t.length;)t="0"+t;return t}function u(e,t){return((255&e[t])<<24|(255&e[t+1])<<16|(255&e[t+2])<<8|255&e[t+3])>>>0}function l(e){for(var t=function(e){var t,c,l,d,_,p,f,h,g,m,y,v,w,k,b,C,S,P,E,T,x,D=e.slice(),M=0,I=8*D.length>>>0;for(D.length>=536870912&&(M=Math.floor(D.length/536870912),0==(I=D.length%536870912*8>>>0)&&D.length>0&&M--),D.push(128);D.length%64!=56;)D.push(0);for(D.push(M>>>24&255,M>>>16&255,M>>>8&255,255&M,I>>>24&255,I>>>16&255,I>>>8&255,255&I),t=1779033703,c=3144134277,l=1013904242,d=2773480762,_=1359893119,p=2600822924,f=528734635,h=1541459225,g=0;D.length>g;g+=64){for(y=Array(64),m=0;16>m;m++)y[m]=u(D,g+4*m);for(m=16;64>m;m++)y[m]=s(y[m-2])+y[m-7]+a(y[m-15])+y[m-16]>>>0;for(v=t,w=c,k=l,b=d,C=_,S=p,P=f,E=h,m=0;64>m;m++)T=E+i(C)+n(C,S,P)+zt[m]+y[m]>>>0,x=o(v)+r(v,w,k)>>>0,E=P,P=S,S=C,C=b+T>>>0,b=k,k=w,w=v,v=T+x>>>0;t=t+v>>>0,c=c+w>>>0,l=l+k>>>0,d=d+b>>>0,_=_+C>>>0,p=p+S>>>0,f=f+P>>>0,h=h+E>>>0}return[t,c,l,d,_,p,f,h]}(e||[]),l="",d=0;t.length>d;d++)l+=c(t[d]);return l}function d(e){return l(function(e){var t,n,r,o,i,a;for(t=(null==e?"":e)+"",n=[],r=0;t.length>r;r++)128>(o=t.charCodeAt(r))?n.push(o):2048>o?n.push(192|o>>6,128|63&o):55296>o||o>56319||r+1>=t.length||56320>(i=t.charCodeAt(r+1))||i>57343?n.push(224|o>>12,128|o>>6&63,128|63&o):(n.push(240|(a=i-56320+(o-55296<<10)+65536)>>18,128|a>>12&63,128|a>>6&63,128|63&a),r++);return n}((null==e?"":e)+""))}function _(e){var t,n,r,o,i,a;for(t=(null==e?"":e)+"",n=[],r=0;t.length>r;r++)128>(o=t.charCodeAt(r))?n.push(o):2048>o?n.push(192|o>>6,128|63&o):55296>o||o>56319||r+1>=t.length||56320>(i=t.charCodeAt(r+1))||i>57343?n.push(224|o>>12,128|o>>6&63,128|63&o):(n.push(240|(a=i-56320+(o-55296<<10)+65536)>>18,128|a>>12&63,128|a>>6&63,128|63&a),r++);return n}function p(e){for(var t=(null==e?"":e)+"",n=[],r=0;t.length>r;r+=2)n.push(parseInt(t.substr(r,2),16));return n}function f(e,t){var n,r=Array(e.length+t.length);for(n=0;e.length>n;n++)r[n]=e[n];for(n=0;t.length>n;n++)r[e.length+n]=t[n];return r}function h(e,t){var n,r;return null===e?"__t:null__":"string"==typeof e?"__t:str__:"+e:"number"==typeof e&&isFinite(e)?"__t:num__:"+e:"boolean"==typeof e?"__t:bool__:"+(e?"1":"0"):Array.isArray(e)?-1!==t.indexOf(e)?"__t:circular__":(t.push(e),n="["+e.map(function(e){return h(e,t)}).join(",")+"]",t.splice(t.indexOf(e),1),n):null!==e&&"object"==typeof e?-1!==t.indexOf(e)?"__t:circular__":(t.push(e),r="{"+Object.keys(e).sort().map(function(n){return n+":"+h(e[n],t)}).join(",")+"}",t.splice(t.indexOf(e),1),r):"__t:any__:"+e}function g(e){return h(e,[])}function m(t){var n,r=t||{},o=Object.keys(r).filter(function(e){return"event_id"!==e}).sort();return o.length?(n=o.map(function(t){return e(g(r[t]))}).join(""),e(n)):e("")}function y(){if("undefined"==typeof navigator)return"PC";var e=navigator.userAgent||"";return/iPhone|iPad|iPod/i.test(e)?"iOS":function(e){return/HarmonyOS|OpenHarmony/i.test((null==e?"":e)+"")}(e)||/Android/i.test(e)?"Android":"PC"}function v(e){var t=((null==e?"":e)+"").trim();return t&&/^\d+\.\d+\.\d+$/.test(t)?t:"1.0.0"}function w(e,t){return null==e||""===e?"":"int"===t?parseInt(e,10)+"":(e+"").replace(/^\s+|\s+$/g,"").toLowerCase()}function k(e,t){try{return e()}catch(e){return t}}function b(t){return e((null==t?"":t)+"").substr(0,32)}function C(e){return b((e||[]).slice().sort().join("|"))}function S(){return k(function(){var e,t,n,r,o,i,a,s="undefined"!=typeof OfflineAudioContext?OfflineAudioContext:"undefined"!=typeof webkitOfflineAudioContext?webkitOfflineAudioContext:null;if(!s)return"";if((t=(e=new s(1,44100,44100)).createOscillator()).type="triangle",t.frequency.value=1e4,n=e.createDynamicsCompressor(),t.connect(n),n.connect(e.destination),t.start(0),(r=e.startRendering())&&"function"==typeof r.then)return"";if(!r||!r.getChannelData)return"";for(o=r.getChannelData(0),i=0,a=4500;5e3>a&&o.length>a;a++)i+=Math.abs(o[a]);return b(i+"")},"")}function P(){return k(function(){function e(e){var t,n=document.createElement("span");return n.style.cssText="position:absolute;left:-9999px;top:-9999px;visibility:hidden;font-size:72px;font-family:"+e+",monospace;",n.textContent="mmmmmmmmmmlli",document.body.appendChild(n),t=n.offsetWidth,document.body.removeChild(n),t}var t,n,r,o;if("undefined"==typeof document||!document.body)return{hash:"",installed:[],count:-1};for(t=e("monospace"),n=[],r=0;$t.length>r;r++)e("'"+(o=$t[r])+"'")!==t&&n.push(o);return n.sort(),{hash:C(n.map(function(e){return e.toLowerCase()})),installed:n,count:n.length}},{hash:"",installed:[],count:-1})}function E(){return k(function(){var e,t,n,r,o,i,a;if("undefined"==typeof document)return{hash:"",supported:[]};for(e=document.createElement("video"),t=document.createElement("audio"),n=[],r=0;Zt.length>r;r++){i="video"===(o=Zt[r]).kind?e:t,a="";try{a=i.canPlayType?(i.canPlayType(o.type)||"")+"":""}catch(e){}a&&"no"!==a&&n.push(o.type+":"+a)}return n.sort(),{hash:C(n.map(function(e){return e.toLowerCase()})),supported:n}},{hash:"",supported:[]})}function T(e){var t;try{return"undefined"==typeof window?0:(t="session"===e?window.sessionStorage:window.localStorage)?(t.setItem("__sdk_probe__","1"),t.removeItem("__sdk_probe__"),1):0}catch(e){return 0}}function x(){try{return"undefined"!=typeof indexedDB?1:0}catch(e){return 0}}function D(){try{return"undefined"!=typeof navigator&&navigator.serviceWorker?1:0}catch(e){return 0}}function M(e){return/Trident|MSIE/i.test(e=(e||"")+"")?"trident":/Edge\/\d/i.test(e)&&!/Edg\//i.test(e)?"edgehtml":/Edg\//i.test(e)||/Chrome\//i.test(e)&&!/Edg\//i.test(e)?"chromium":/Firefox\//i.test(e)?"gecko":/Safari\//i.test(e)&&!/Chrome\//i.test(e)?"webkit":"unknown"}function I(e,t){return t=(t||"")+"",/HarmonyOS|OpenHarmony/i.test(e=(e||"")+"")?"harmonyos":/iPhone|iPad|iPod/i.test(e)?"ios":/Android/i.test(e)?"android":/Mac OS X|Macintosh/i.test(e)||/^Mac/i.test(t)?"macos":/Windows/i.test(e)||/^Win/i.test(t)?"windows":/Linux/i.test(e)||/Linux/i.test(t)?"linux":"unknown"}function A(e){return/iPad|Tablet/i.test(e=(e||"")+"")?"tablet":/Mobile|iPhone|Android/i.test(e)?"phone":"desktop"}function O(e){var t,n,r;for(e=(e||"")+"",t=[[/EdgiOS\/([\d.]+)/i,"Edge"],[/CriOS\/([\d.]+)/i,"Chrome"],[/FxiOS\/([\d.]+)/i,"Firefox"],[/Edg\/([\d.]+)/i,"Edge"],[/OPR\/([\d.]+)/i,"Opera"],[/Chrome\/([\d.]+)/i,"Chrome"],[/Firefox\/([\d.]+)/i,"Firefox"],[/Version\/([\d.]+).*Safari/i,"Safari"]],n=0;t.length>n;n++)if(r=e.match(t[n][0]))return{browser_name:t[n][1],browser_version:r[1]||"unknown"};return{browser_name:"unknown",browser_version:"unknown"}}function L(){var e=k(function(){var e,t="undefined"!=typeof screen?screen:null;if(t&&t.orientation&&t.orientation.type)return t.orientation.type+"";if("undefined"!=typeof window&&"number"==typeof window.orientation){if(0===(e=window.orientation)||180===e)return"portrait-primary";if(90===e||-90===e)return"landscape-primary"}return t&&null!=t.width&&null!=t.height?t.width>t.height?"landscape-primary":"portrait-primary":"unknown"},"unknown");if(-1!==e.indexOf("landscape"))return"landscape";if(-1!==e.indexOf("portrait"))return"portrait";try{if("undefined"!=typeof screen&&null!=screen.width&&null!=screen.height){if(screen.width>screen.height)return"landscape";if(screen.widthu;u++){l=s[u];try{null!=(d=t[l])&&(c[l]=t.getParameter(d)+"")}catch(e){}}for(_=Object.keys(c).sort(),p=[],f=0;_.length>f;f++)p.push((h=_[f])+"="+c[h]);return g=b(p.join("|")),m=b([n,r,a,g].join("|")),{vendor:n,renderer:r,extensions_hash:a,params_hash:g,webgl_fp:m}},{vendor:"",renderer:"",extensions_hash:"",params_hash:"",webgl_fp:""}),H=F.webgl_fp||"",U=S(),X=k(function(){var e,t,n;if("undefined"==typeof navigator||!navigator.plugins)return"";for(e=[],t=0;t0?Math.round(t):-1},-1),J=(R.platform||"")+"",Q=0;try{Q=R.plugins?R.plugins.length:0}catch(R){}e=G.count,t=H?"available":"unavailable",n=!0===R.webdriver?1:0,r=0,o=0;try{K&&K.documentElement&&(r=K.documentElement.clientWidth||B.innerWidth||0,o=K.documentElement.clientHeight||B.innerHeight||0)}catch(R){}i=0,(1===n||"available"!==t&&0===Q&&e>=0&&2>=e&&(0>=r||0>=o))&&(i=1),a="";try{"undefined"!=typeof Intl&&Intl.DateTimeFormat&&(a=Intl.DateTimeFormat().resolvedOptions().timeZone||"")}catch(R){}s=function(){try{return"undefined"==typeof location?"":(location.protocol||"")+"//"+(location.host||"")+(location.pathname||"")}catch(R){return""}}(),c="",u="",l="";try{"undefined"!=typeof location&&(c=location.hostname||"",u=location.pathname||"",location.search&&(l=b(location.search)))}catch(R){}_=function(e){var t,n=((e||"")+"").trim();if(!n)return{referer_domain:"",referer_path:""};try{return{referer_domain:(t=new URL(n)).hostname||"",referer_path:t.pathname||""}}catch(e){return{referer_domain:"",referer_path:""}}}(d=K&&K.referrer?K.referrer+"":""),p=function(e){var t,n,r,o;if(t=(e=e||{}).userAgent,/iPhone|iPad|iPod/i.test(t=(t||"")+"")&&!/CriOS\/|FxiOS\/|EdgiOS\/|OPiOS\//i.test(t)&&/Version\/[\d.]+.*Safari/i.test(t))return O(e.userAgent);try{if((n=e.userAgentData&&e.userAgentData.brands)&&n.length)for(r=0;n.length>r;r++)if((o=(n[r].brand||"")+"")&&"Chromium"!==o&&0!==o.indexOf("Not")&&"Not.A/Brand"!==o)return{browser_name:o,browser_version:(n[r].version||"unknown")+""}}catch(e){}return O(e.userAgent)}(R),f=z.supported?z.supported.length:-1,g=Math.round(96*(h=B.devicePixelRatio||1)),m=-1;try{null!=R.hardwareConcurrency&&R.hardwareConcurrency>0&&(m=R.hardwareConcurrency)}catch(R){}y=m,v=-1;try{null!=R.deviceMemory&&R.deviceMemory>0&&(v=Math.round(10*R.deviceMemory)/10)}catch(R){}return w=j&&j.type?j.type+"":"unknown",C=R.maxTouchPoints||0,{platform:"web",os_family:I(V,J),device_type:A(V),browser_kernel:M(V),browser_name:p.browser_name,browser_version:p.browser_version,browser_platform:J,display_mode:k(function(){return"undefined"==typeof window||"function"!=typeof window.matchMedia?"unknown":window.matchMedia("(display-mode: standalone)").matches?"standalone":window.matchMedia("(display-mode: fullscreen)").matches?"fullscreen":window.matchMedia("(display-mode: minimal-ui)").matches?"minimal-ui":"browser"},"unknown"),visibility_state:k(function(){if("undefined"==typeof document)return"unknown";var e=document.visibilityState;return"visible"===e||"hidden"===e?e:"unknown"},"unknown"),screen_width:q.width||0,screen_height:q.height||0,screen_dpi:g,device_pixel_ratio:Math.round(100*h),orientation:L(),viewport_width:r>0?Math.round(r):-1,viewport_height:o>0?Math.round(o):-1,color_depth:q.colorDepth||0,max_touch_points:C,touch_support:C>0?1:0,hardware_concurrency:m,cpu_cores:y,device_memory_gb:v,is_debug:0,language:R.language||"",timezone:a,cookie_enabled:R.cookieEnabled?1:0,local_storage_enabled:T("local"),session_storage_enabled:T("session"),indexeddb_enabled:x(),service_worker_enabled:D(),network_type:"unknown",connection_type:w,effective_type:j&&j.effectiveType?j.effectiveType+"":"unknown",downlink:j&&null!=j.downlink?Math.round(1e3*j.downlink):-1,rtt:j&&null!=j.rtt?j.rtt:-1,canvas_fp:N,webgl_fp:H,webgl_vendor:F.vendor||"",webgl_renderer:F.renderer||"",webgl_extensions_hash:F.extensions_hash||"",webgl_params_hash:F.params_hash||"",audio_fp:U,audio_sample_rate:W>0?W:44100,fonts:G.installed||[],fonts_hash:G.hash,plugins:Y,plugins_hash:X,mimes:z.supported||[],mimes_hash:z.hash,mime_count:f,plugin_count:Q,font_count:e,webgl_status:t,has_webdriver:n,is_headless:i,headless_reason:i?n?"webdriver":"composite":"",current_url:s,current_domain:c,current_path:u,current_query_hash:l,referer:d,referer_domain:_.referer_domain,referer_path:_.referer_path}}function q(e){return null==e?"":e+""}function B(e){var t=e||{},n={app_id:(t.appId||"")+"",uid:null!=t.uid?t.uid:0,channel:(t.channel||"")+"",sid:(t.sid||"")+"",client_ts:Math.floor(Date.now()/1e3),device:(t.deviceType||"PC")+"",device_id:(t.deviceId||"")+"",device_brand:(t.deviceBrand||"")+"",device_model:(t.deviceModel||"")+"",device_fingerprint:(t.deviceFingerprint||"")+"",fp_version:(t.fpVersion||"")+"",user_agent:"undefined"!=typeof navigator&&navigator.userAgent||"",sdk_version:(t.sdkVersion||"")+"",app_version:(t.appVersion||"1.0.0")+""};return null!=t.deviceEnv&&(n.device_env=t.deviceEnv),n}function K(){var e,t,n,r=new Uint8Array(16);if("undefined"!=typeof crypto&&crypto.getRandomValues)crypto.getRandomValues(r);else for(e=0;16>e;e++)r[e]=Math.floor(256*Math.random());for(t="",n=0;r.length>n;n++)t+=("0"+r[n].toString(16)).slice(-2);return t}function V(){try{return!("undefined"==typeof window||!window.sessionStorage||(window.sessionStorage.setItem("__sdk_ss_ping__","1"),window.sessionStorage.removeItem("__sdk_ss_ping__"),0))}catch(e){return!1}}function j(){if(!un){if(un=!0,function(){try{return!("undefined"==typeof window||!window.localStorage||(window.localStorage.setItem("__web_sdk_ping__","1"),window.localStorage.removeItem("__web_sdk_ping__"),0))}catch(e){return!1}}())try{window.localStorage.removeItem(rn)}catch(e){}if("undefined"!=typeof document&&"string"==typeof document.cookie)try{var t="path=/;max-age=0;SameSite=Lax";"undefined"!=typeof location&&"https:"===location.protocol&&(t+=";Secure"),document.cookie=encodeURIComponent(on)+"=;"+t}catch(e){}}}function N(t){j();var n=function(e){return JSON.stringify({sid:e.sid,sessionStartTs:e.sessionStartTs,lastValidBehaviorAt:e.lastValidBehaviorAt})}(t);if(V())try{window.sessionStorage.setItem(tn,t.sid),window.sessionStorage.setItem(nn,n)}catch(e){}cn=t}function F(){if(cn)return cn;j();var t=null;if(V())try{t=window.sessionStorage.getItem(nn)}catch(e){}return cn=t?function(t){try{var n=JSON.parse(t);return n&&n.sid?{sid:n.sid+"",sessionStartTs:"number"==typeof n.sessionStartTs?n.sessionStartTs:Date.now(),lastValidBehaviorAt:"number"==typeof n.lastValidBehaviorAt?n.lastValidBehaviorAt:Date.now()}:null}catch(e){return null}}(t):null,cn}function H(e){return{sid:K(),sessionStartTs:e,lastValidBehaviorAt:e}}function U(e){return"number"==typeof e&&isFinite(e)?1e12>e?1e3*e:e:null}function X(e,t,n){var r=U(e.sessionStartTs)||n,o=U(e.lastValidBehaviorAt)||r,i=n;return"idle"===t?i=Math.min(i=Math.max(r,o+an),n):"forced"===t&&(i=Math.min(r+sn,n)),Math.max(r,i)}function Y(e,t,n){var r,o;return n=n||Date.now(),r=U(e.sessionStartTs)||n,o=X(e,t,n),{sid:e.sid,sessionStartTs:r,lastValidBehaviorAt:U(e.lastValidBehaviorAt)||r,sessionEndTs:o,endReason:t}}function G(e){var t,n,r,o,i;return e=e||Date.now(),(t=F())?(r=e-t.sessionStartTs>sn,e-t.lastValidBehaviorAt>an||r?(o=Y(t,r?"forced":"idle",e),N(i=H(e)),{sid:i.sid,isNew:!0,endedSession:o}):{sid:t.sid,isNew:!1}):(N(n=H(e)),{sid:n.sid,isNew:!0})}function z(){var e=F();return e?e.sid:""}function W(){cn=null}function J(){try{return!("undefined"==typeof window||!window.localStorage||(window.localStorage.setItem(dn,"1"),window.localStorage.removeItem(dn),0))}catch(e){return!1}}function Q(){var t,n;if(!J())return Promise.resolve(_n||"NULL");try{return(t=window.localStorage.getItem(ln))&&/^[0-9a-f]{32}$/.test(t)?Promise.resolve(t):(n=function(){var e,t,n,r=new Uint8Array(16);if("undefined"!=typeof crypto&&crypto.getRandomValues)crypto.getRandomValues(r);else for(e=0;16>e;e++)r[e]=Math.floor(256*Math.random());for(t="",n=0;r.length>n;n++)t+=("0"+r[n].toString(16)).slice(-2);return t}(),window.localStorage.setItem(ln,n),_n=n,Promise.resolve(n))}catch(e){return Promise.resolve("NULL")}}function $(t,n){n&&(t.audio_fp=n),fn=t;var r=function(){if(!J())return"";try{var t=window.localStorage.getItem(pn);if(t&&/^[0-9a-f]{16,64}$/i.test(t))return t.toLowerCase()}catch(e){}return""}();return r?hn=r:(hn=function(e){var t,n,r,o,i,a;for(t=e||{},n={screen_width:1,screen_height:1,screen_dpi:1,device_pixel_ratio:1,color_depth:1,max_touch_points:1,cpu_cores:1,cookie_enabled:1,local_storage_enabled:1,session_storage_enabled:1,indexeddb_enabled:1,service_worker_enabled:1},r={canvas_fp:1,webgl_fp:1,audio_fp:1,fonts_hash:1,plugins_hash:1,mimes_hash:1},o=[],i=0;Qt.length>i;i++)o.push((a=Qt[i])+"="+w(t[a],n[a]?"int":r[a]?"hash":"string"));return d("fp_v3|fingerprint_platform=web|"+o.join("|"))}(t),function(t){if(t&&J())try{window.localStorage.setItem(pn,t)}catch(e){}}(hn)),{snapshot:t,fingerprint:hn}}function Z(){return fn&&hn?Promise.resolve({snapshot:fn,fingerprint:hn}):gn||(gn=new Promise(function(e){k(function(){var t,n,r,o,i="undefined"!=typeof OfflineAudioContext?OfflineAudioContext:"undefined"!=typeof webkitOfflineAudioContext?webkitOfflineAudioContext:null;i?((n=(t=new i(1,44100,44100)).createOscillator()).type="triangle",n.frequency.value=1e4,r=t.createDynamicsCompressor(),n.connect(r),r.connect(t.destination),n.start(0),(o=t.startRendering())&&"function"==typeof o.then?o.then(function(t){for(var n=t.getChannelData(0),r=0,o=4500;5e3>o&&n.length>o;o++)r+=Math.abs(n[o]);e(b(r+""))}).catch(function(){e("")}):e(S())):e("")},e(""))}).then(function(e){return $(R(),e)}).catch(function(){return $(R(),"")}),gn)}function ee(){return hn}function te(){return Wt}function ne(){return mn.digest()}function re(){try{if("undefined"!=typeof screen&&screen.orientation&&screen.orientation.type){var t=screen.orientation.type+"";if(-1!==t.indexOf("landscape"))return"landscape";if(-1!==t.indexOf("portrait"))return"portrait"}if("undefined"!=typeof screen&&null!=screen.width&&null!=screen.height){if(screen.width>screen.height)return"landscape";if(screen.width0?Math.round(n):-1}catch(e){return-1}}function ie(e,t){var n=t.accumulated_ms||0;return null!=t.resume_at_ms&&(n+=Math.max(0,e-t.resume_at_ms)),n}function ae(){return xn.pageSeqCounter++,xn.currentPageSeq=xn.pageSeqCounter,xn.currentPageSeq}function se(){try{return!("undefined"==typeof window||!window.sessionStorage||(window.sessionStorage.setItem("__sdk_metrics_ping__","1"),window.sessionStorage.removeItem("__sdk_metrics_ping__"),0))}catch(e){return!1}}function ce(e){var t,n=e||xn;return{eventCountTotal:n.eventCountTotal||0,businessEventCount:n.businessEventCount||0,pageViewCount:n.pageViewCount||0,pageClickCount:n.pageClickCount||0,networkChangeCount:n.networkChangeCount||0,backgroundCount:n.backgroundCount||0,pageKeys:le({},n.pageKeys||{}),pageStayList:(n.pageStayList||[]).slice(),pageSeqCounter:n.pageSeqCounter||0,currentPageSeq:n.currentPageSeq||0,lastEventName:n.lastEventName||"",scrollTotalCount:n.scrollTotalCount||0,scrollTotalDistance:n.scrollTotalDistance||0,scrollUpCount:n.scrollUpCount||0,scrollDownCount:n.scrollDownCount||0,scrollDirectionChangeCount:n.scrollDirectionChangeCount||0,lastScrollDirection:n.lastScrollDirection||0,firstScrollDelayMs:null!=n.firstScrollDelayMs?n.firstScrollDelayMs:-1,hasScrolled:!!n.hasScrolled,maxScrollDepthPercent:n.maxScrollDepthPercent||0,isReachBottom:n.isReachBottom||0,keydownTotalCount:n.keydownTotalCount||0,inputCount:n.inputCount||0,focusTotalCount:n.focusTotalCount||0,firstInteractionDelayMs:null!=n.firstInteractionDelayMs?n.firstInteractionDelayMs:-1,firstInteractionRecorded:!!n.firstInteractionRecorded,lastOrientation:n.lastOrientation||"unknown",touchTotalCount:n.touchTotalCount||0,swipeTotalCount:n.swipeTotalCount||0,longPressTotalCount:n.longPressTotalCount||0,pinchTotalCount:n.pinchTotalCount||0,currentPage:(t=n.currentPage,t&&"object"==typeof t?{page_key:(t.page_key||"")+"",page_name:(t.page_name||"")+"",enter_time:null!=t.enter_time?t.enter_time:Date.now(),enter_time_ms:null!=t.enter_time_ms?t.enter_time_ms:t.enter_time,page_seq_in_sid:t.page_seq_in_sid||0,has_interaction:!!t.has_interaction,accumulated_ms:t.accumulated_ms||0,resume_at_ms:null!=t.resume_at_ms?t.resume_at_ms:null,hidden_paused:!!t.hidden_paused,stay_duration_ms:t.stay_duration_ms}:null)}}function ue(e,t){return null==e||0>e||t>0&&e>t?-1:e}function le(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function de(e){return kn+":"+(e||"")}function _e(t){var n,r;if(!t)return null;if(n=null,se())try{n=window.sessionStorage.getItem(de(t))}catch(e){}if(!n&&ye())try{n=window.localStorage.getItem(de(t))}catch(e){}if(!n)return null;try{return(r=JSON.parse(n))&&r.sid===t?r:null}catch(e){return null}}function pe(t,n){if(t){var r=JSON.stringify(n||{});if(se())try{window.sessionStorage.setItem(de(t),r)}catch(e){}if(ye())try{window.localStorage.setItem(de(t),r)}catch(e){}}}function fe(e){if(!e)return null;var t=_e(e);return t&&t.metrics?t.metrics:null}function he(){var e,t=z();t&&(Cn?Sn||(Sn=t):(Cn=!0,(e=_e(t))&&e.metrics?(xn=ce(e.metrics),e.sessionStartedAt&&(Dn=e.sessionStartedAt),Sn=t):Sn=t))}function ge(e){var t=_e(e);return t&&t.behaviorHash?t.behaviorHash:""}function me(){var e=z();e&&(Sn&&Sn!==e||(Sn||(Sn=e),function(e){e&&pe(e,{sid:e,savedAt:Date.now(),sessionStartedAt:Dn,behaviorHash:ne(),metrics:ce(xn)})}(e)))}function ye(){try{return!("undefined"==typeof window||!window.localStorage||(window.localStorage.setItem("__sdk_metrics_ping__","1"),window.localStorage.removeItem("__sdk_metrics_ping__"),0))}catch(e){return!1}}function ve(){var t,n;if(!ye())return[];try{return(t=window.localStorage.getItem(wn))?(n=JSON.parse(t),Array.isArray(n)?n:[]):[]}catch(e){return[]}}function we(t){if(ye())try{window.localStorage.setItem(wn,JSON.stringify(t||[]))}catch(e){}}function ke(e){we(ve().filter(function(t){return t&&t.sid!==e}))}function be(e){for(var t,n=e.slice(0,yn);n.length>0;){if(t=JSON.stringify(n),vn>=t.length)return n;n.pop()}return n}function Ce(e){var t,n=xn.currentPage;n&&(n.stay_duration_ms=ie(e,n),t={page_key:n.page_key,page_name:n.page_name||"",stay_duration_ms:n.stay_duration_ms,enter_time:Math.floor(n.enter_time/1e3),has_interaction:n.has_interaction?1:0},n.page_seq_in_sid&&(t.page_seq_in_sid=n.page_seq_in_sid),xn.pageStayList.push(t),xn.pageStayList=be(xn.pageStayList),xn.currentPage=null)}function Se(){xn.firstInteractionRecorded||(xn.firstInteractionRecorded=!0,xn.firstInteractionDelayMs=Math.max(0,Date.now()-Dn))}function Pe(){xn.lastOrientation=re()}function Ee(e){e&&xn.currentPage&&xn.currentPage.page_key===e&&(xn.currentPage.has_interaction=!0)}function Te(e,t,n,r){var o=Date.now();Ce(o),xn.currentPage={page_key:(e||"")+"",page_name:(t||"")+"",enter_time:null!=n?1e3*n:o,enter_time_ms:o,page_seq_in_sid:r||xn.currentPageSeq||0,has_interaction:!1,accumulated_ms:0,resume_at_ms:o,hidden_paused:!1}}function xe(){Ce(Date.now())}function De(t){var n,r=t;if(null==r)return null;if("string"==typeof r)try{r=JSON.parse(r)}catch(e){return null}return r&&"object"==typeof r?(n={page_key:(r.page_key||"")+"",page_name:(r.page_name||"")+"",stay_duration_ms:null!=r.stay_duration_ms?r.stay_duration_ms:0,enter_time:null!=r.enter_time?r.enter_time:0,has_interaction:r.has_interaction?1:0},null!=r.page_seq_in_sid&&(n.page_seq_in_sid=r.page_seq_in_sid),n):null}function Me(e,t,n){var r,o,i,a,s,c,u,l,d,_,p,f=e||{},h=f.sid||z(),g=h&&h===z();for(n?r=ce(n):g?(xe(),me(),r=ce(xn)):r=function(e){var t,n;return he(),!(t=e||z())||t!==z()||Sn&&Sn!==t?(n=fe(t))?ce(n):Sn&&Sn!==t?{eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null}:ce(xn):ce(xn)}(h),o=function(e,t){var n=Date.now(),r=e||{},o=U(r.sessionStartTs)||n,i=null!=r.sessionEndTs?U(r.sessionEndTs):X(r,t||"idle",n);return{startTs:o,endTs:i=Math.max(o,i||n)}}(f,t),s=Math.max(0,(a=o.endTs)-(i=o.startTs)),c=le({},r.pageKeys||{}),u=function(e){var t,n,r=[];for(t=0;(e||[]).length>t;t++)(n=De(e[t]))&&r.push(n);return r}(function(e){var t,n,r,o={},i=[];for(t=0;e.length>t;t++)null!=(r=(n=e[t])&&n.page_seq_in_sid)&&o[r]||(null!=r&&(o[r]=!0),i.push(n));return i}((r.pageStayList||[]).slice()).sort(function(e,t){return(e.page_seq_in_sid||0)-(t.page_seq_in_sid||0)})),u=function(e,t,n){var r,o,i,a=Math.floor((t||0)/1e3),s=Math.floor((n||Date.now())/1e3),c=Math.max(0,a-5),u=[];for(r=0;(e||[]).length>r;r++)(o=e[r])&&((i=null!=o.enter_time?+o.enter_time:0)>0&&c>i||i>s+5||u.push(o));return u}(u,i,a),l=0;u.length>l;l++)(d=u[l]&&u[l].page_key?u[l].page_key+"":"")&&(c[d]=1);return _=Object.keys(c),p=n?ge(h)||ne():g?ne():ge(h)||ne(),{session_start_ts:Math.floor(i/1e3),session_end_ts:Math.floor(a/1e3),session_duration_ms:s,event_count_total:r.eventCountTotal||0,business_event_count:r.businessEventCount||0,page_count:_.length,page_view_count:r.pageViewCount||0,page_click_count:r.pageClickCount||0,session_end_reason:(t||"idle")+"",last_event_name:r.lastEventName||"",page_stay_list:u,behavior_sequence_hash:p,network_change_count:r.networkChangeCount||0,scroll_total_count:r.scrollTotalCount||0,scroll_total_distance_px:Math.round(r.scrollTotalDistance||0),scroll_up_count:r.scrollUpCount||0,scroll_down_count:r.scrollDownCount||0,scroll_direction_change_count:r.scrollDirectionChangeCount||0,first_scroll_delay_ms:ue(null!=r.firstScrollDelayMs?r.firstScrollDelayMs:-1,s),max_scroll_depth_percent:r.maxScrollDepthPercent||0,is_reach_bottom:r.isReachBottom||0,content_height:oe(),keydown_total_count:r.keydownTotalCount||0,input_count:r.inputCount||0,focus_total_count:r.focusTotalCount||0,first_interaction_delay_ms:ue(null!=r.firstInteractionDelayMs?r.firstInteractionDelayMs:-1,s),orientation:r.lastOrientation||re(),touch_total_count:r.touchTotalCount||0,swipe_total_count:r.swipeTotalCount||0,long_press_total_count:r.longPressTotalCount||0,pinch_total_count:r.pinchTotalCount||0}}function Ie(){On=!0,setTimeout(function(){On=!1},2e3)}function Ae(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function Oe(t){return e((null==t?"":t)+"").substr(0,32)}function Le(e){return null!=e&&""!==(e+"").trim()}function Re(){try{return"undefined"==typeof location?"":(location.protocol||"")+"//"+(location.host||"")+(location.pathname||"")}catch(e){return""}}function qe(e){var t=(null==e?"":e)+"";return(t=(t=(t=t.replace(/\.html?$/i,"")).replace(/[^a-zA-Z0-9_]+/g,"_")).replace(/_+/g,"_").replace(/^_|_$/g,""))||"index"}function Be(){try{return qe(("undefined"!=typeof location&&location.pathname?location.pathname:"/").replace(/^\/+|\/+$/g,"").replace(/\//g,"_")||"index")}catch(e){return"index"}}function Ke(){try{return document.scrollingElement||document.documentElement||document.body||null}catch(e){return null}}function Ve(){try{var t=Ke();return t?{scroll_top:Math.max(0,Math.round(t.scrollTop||0)),scroll_left:Math.max(0,Math.round(t.scrollLeft||0))}:{scroll_top:-1,scroll_left:-1}}catch(e){return{scroll_top:-1,scroll_left:-1}}}function je(e,t){if(null==e||!isFinite(e))return-1;var n=Math.round(+e);return 0>n&&(n=0),null!=t&&t>0&&n>t&&(n=t),n}function Ne(){var t,n,r;try{return(t=document.documentElement||document.body)?(r=t.scrollHeight||0,(n=t.scrollWidth||0)>0&&r>0?{page_width:Math.round(n),page_height:Math.round(r)}:{page_width:-1,page_height:-1}):{page_width:-1,page_height:-1}}catch(e){return{page_width:-1,page_height:-1}}}function Fe(){var t,n;try{return"undefined"==typeof window?{viewport_width:-1,viewport_height:-1}:(t=window.innerWidth||0,n=window.innerHeight||0,t>0&&n>0?{viewport_width:Math.round(t),viewport_height:Math.round(n)}:{viewport_width:-1,viewport_height:-1})}catch(e){return{viewport_width:-1,viewport_height:-1}}}function He(){var t,n;try{return"undefined"==typeof screen?{screen_width:-1,screen_height:-1}:(t=screen.width||0,n=screen.height||0,t>0&&n>0?{screen_width:Math.round(t),screen_height:Math.round(n)}:{screen_width:-1,screen_height:-1})}catch(e){return{screen_width:-1,screen_height:-1}}}function Ue(){var t,n,r;try{if("undefined"!=typeof performance&&performance.timing&&(t=performance.timing).loadEventEnd&&t.navigationStart&&t.loadEventEnd>0)return Math.max(0,t.loadEventEnd-t.navigationStart);if("undefined"!=typeof performance&&"function"==typeof performance.getEntriesByType&&(n=performance.getEntriesByType("navigation"))&&n.length>0&&(r=n[0]).loadEventEnd>0&&r.fetchStart>=0)return Math.max(0,Math.round(r.loadEventEnd-r.fetchStart))}catch(e){}return 0}function Xe(e,t){return t>0?Math.min(100,Math.max(0,Math.round(e/t*100))):0}function Ye(e){return e?{clientX:null!=e.clientX?e.clientX:null,clientY:null!=e.clientY?e.clientY:null,pageX:null!=e.pageX?e.pageX:null,pageY:null!=e.pageY?e.pageY:null}:{}}function Ge(e){var t,n,r,o,i,a,s,c,u=e||{},l=Fe(),d=Ne(),_=Ve(),p=function(e){return!!e&&(null!=e.clientX||null!=e.clientY||null!=e.pageX||null!=e.pageY)}(u),f=d.page_width>0&&d.page_height>0,h=He(),g=f&&_.scroll_top>=0?_.scroll_top:0,m=f&&_.scroll_left>=0?_.scroll_left:0,y=l.viewport_width>0?l.viewport_width:-1,v=l.viewport_height>0?l.viewport_height:-1;return p?(n=null!=u.clientY?+u.clientY:null,r=null!=u.pageX?+u.pageX:null,o=null!=u.pageY?+u.pageY:null,null!=(t=null!=u.clientX?+u.clientX:null)||null!=n?(i=null!=t&&isFinite(t)?t:0,a=null!=n&&isFinite(n)?n:0,s=f?null!=r&&isFinite(r)?r:i+m:-1,c=f?null!=o&&isFinite(o)?o:a+g:-1):(s=f&&null!=r&&isFinite(r)?r:-1,c=f&&null!=o&&isFinite(o)?o:-1,i=null!=r&&isFinite(r)?r-m:0,a=null!=o&&isFinite(o)?o-g:0),i=je(i,y),a=je(a,v),f&&(s=je(s,d.page_width),c=je(c,d.page_height)),Ae({click_viewport_x:i,click_viewport_y:a,click_document_x:f?s:-1,click_document_y:f?c:-1,click_page_x:i,click_page_y:a,click_x_percent:y>0?Xe(i,y):-1,click_y_percent:v>0?Xe(a,v):-1,screen_width:h.screen_width,screen_height:h.screen_height},l,f?d:{page_width:-1,page_height:-1},f?_:{scroll_top:-1,scroll_left:-1})):{click_viewport_x:-1,click_viewport_y:-1,click_document_x:-1,click_document_y:-1,click_page_x:-1,click_page_y:-1,click_x_percent:-1,click_y_percent:-1,screen_width:h.screen_width,screen_height:h.screen_height,viewport_width:l.viewport_width,viewport_height:l.viewport_height,page_width:f?d.page_width:-1,page_height:f?d.page_height:-1,scroll_top:f?_.scroll_top:-1,scroll_left:f?_.scroll_left:-1}}function ze(t){var n=t||{},r=qe(n.pageKey||Be()),o=n.pageName||"undefined"!=typeof document&&document.title||r,i=function(){try{return"undefined"==typeof location?{}:{page_url:Re(),page_domain:(location.hostname||"")+"",page_path:(location.pathname||"")+"",page_query_hash:location.search?Oe(location.search):""}}catch(e){return{}}}(),a=Fe();return{user_type:"",page_key:r,page_name:o,referrer_page_key:(n.previousPageKey||"")+"",referrer_page_name:(n.previousPageName||"")+"",current_page_key:r,current_page_name:o,recommend_trace_id:"",page_url:i.page_url,viewport_width:a.viewport_width,viewport_height:a.viewport_height,is_sdk_auto_collected:1}}function We(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function Je(e,t){var n,r;return"app_session_summary"===t?We({},e.summaryFields||{},e.payload||{}):(n=function(e){var t=We({},e||{});return delete t._metricsRecorded,delete t._pageSeqPrealloc,delete t._clickSnap,t}(e.payload||{}),r={event:1,payload:1,event_id:1,summaryFields:1,_pageSeqInSid:1,_metricsRecorded:1,_sdkAutoClick:1,_pageSeqPrealloc:1},Object.keys(e).forEach(function(t){r[t]||(n[t]=e[t])}),n)}function Qe(e,t,n,r,o){var i,a,s,c,u,d,h,g=e||{},y=((n.event||"")+"").trim(),v=n.lifecycle_event_key||n.payload&&n.payload.lifecycle_event_key||"";return function(e,t){var n,r=(e||"")+"";return"app_page_view"===r||"page_click"===r||("app_lifecycle"===r?"cold_start"==(n=(t||"")+"")||"web_load"===n||"warm_start"===n||"foreground"===n:"sdk_init"!==r&&"network_state_change"!==r&&"app_session_summary"!==r&&"background"!==r)}(y,v)&&(c=Date.now(),(u=F())?(u.lastValidBehaviorAt=c,N(u)):N(H(c))),i="",n.payload&&null!=n.payload.page_key?i=n.payload.page_key+"":null!=n.page_key&&(i=n.page_key+""),function(e,t,n){"app_lifecycle"===e&&t&&en[t]?mn.append(t,n):("app_page_view"===e||"page_click"===e||e&&!Tn[e])&&mn.append(e,n)}(y,v,i),a=function(e,t){return B({appId:e.appId,channel:e.channel,uid:e.uid,sid:t,deviceId:(n=e.deviceId,((null==n?"":n)+"").trim()||"NULL"),deviceType:e.deviceType||"PC",deviceBrand:e.deviceBrand||"",deviceModel:e.deviceModel||"",deviceFingerprint:ee(),fpVersion:te(),sdkVersion:e.sdkVersion,appVersion:e.appVersion});var n}(g,t),function(e,t){var n,r;if(he(),(n=(e||"")+"")&&"app_session_summary"!==n){if("app_page_view"===n&&t&&t._metricsRecorded)return xn.lastEventName=n,void me();xn.eventCountTotal++,xn.lastEventName=n,Tn[n]||xn.businessEventCount++,r=t&&null!=t.page_key?t.page_key+"":"","app_page_view"===n?(xn.pageViewCount++,r&&(xn.pageKeys[r]=1)):"page_click"===n?(xn.pageClickCount++,Ee(r)):"network_state_change"===n?xn.networkChangeCount++:r&&Ee(r),me()}}(y,n.payload||{}),s=function(e,t){return function(e,t){for(var n=t||{},r={},o=Object.keys(e||{}),i=0;o.length>i;i++)r[o[i]]=e[o[i]];return r.event_sig_version=(n.eventSigVersion||Jt)+"",r.event_sig=n.hmacKey?function(e,t){return t?function(e,t){var n,r,o,i,a=_((null==t?"":t)+"");for(a.length>64&&(a=p(l(a)));64>a.length;)a.push(0);for(n=[],r=[],o=0;64>o;o++)n.push(54^a[o]),r.push(92^a[o]);return i=p(l(f(n,_((null==e?"":e)+"")))),l(f(r,i))}(function(e){var t=e||{};return[q(t.event),q(t.event_id),q(t.app_id),q(t.client_ts),q(t.device_fingerprint)].join("|")}(e),t+""):""}(e,n.hmacKey):"",r}(e,{hmacKey:t&&t.hmacKey?t.hmacKey:"",eventSigVersion:Jt})}(function(e,t,n){return function(e,t){var n=function(){var e,t,n,r,o;for(e={},t=0;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)e[r[o]]=n[r[o]];return e}(e,t);return n.event_id||(n.event_id=m(n)),n}(e,{event:n,payload:Je(t,n)})}(a,n,y),r),"app_page_view"===y&&n.payload&&n.payload.page_key&&(n.payload._metricsRecorded||n._metricsRecorded||Te(n.payload.page_key,n.payload.page_name,n.payload.page_enter_ts,null!=n._pageSeqInSid?n._pageSeqInSid:n.payload.page_seq_in_sid)),"page_click"===y&&n.payload&&!n._sdkAutoClick&&An.push({x:null!=(d=null!=n.payload.click_viewport_x?n.payload.click_viewport_x:n.payload.click_page_x)?d:0,y:null!=(h=null!=n.payload.click_viewport_y?n.payload.click_viewport_y:n.payload.click_page_y)?h:0,t:Date.now()}),s._sessionInfo=o||null,s}function $e(t,n,r){var o=We({},n||{}),i=((o.event||"")+"").trim();return Bn[i]&&o.payload&&1!==o.payload.is_sdk_auto_collected&&(o.payload=function(e,t,n){var r,o=e||{},i=1===o.is_sdk_auto_collected,a=((n||"")+"").trim(),s="app_page_view"===a||null!=o.page_key&&(null!=o.current_page_key||null!=o.user_type),c="page_click"===a,u={}||{},l=qe(o.page_key||Be());return r=s?ze(Ae(u,{pageKey:l,pageName:o.page_name})):c?Ae({page_key:l,page_name:o.page_name||"undefined"!=typeof document&&document.title||l,recommend_trace_id:"",referrer_page_key:u.previousPageKey||""},function(e){var t=e||{},n=Fe(),r=He(),o=Ne(),i=Ve(),a=o.page_width>0&&o.page_height>0,s=a&&i.scroll_top>=0?i.scroll_top:0,c=a&&i.scroll_left>=0?i.scroll_left:0,u=n.viewport_width>0?n.viewport_width:-1,l=n.viewport_height>0?n.viewport_height:-1,d=null!=t.click_page_x?+t.click_page_x:null,_=null!=t.click_page_y?+t.click_page_y:null,p=-1,f=-1,h=-1,g=-1;return null!=d&&isFinite(d)&&null!=_&&isFinite(_)?(p=d,f=_,a&&(h=d+c,g=_+s)):null!=t.click_x_percent&&null!=t.click_y_percent&&u>0&&l>0&&(p=+t.click_x_percent/100*u,f=+t.click_y_percent/100*l,a&&(h=p+c,g=f+s)),-1!==p&&(p=je(p,u)),-1!==f&&(f=je(f,l)),a&&-1!==h&&(h=je(h,o.page_width)),a&&-1!==g&&(g=je(g,o.page_height)),{click_viewport_x:p,click_viewport_y:f,click_document_x:a?h:-1,click_document_y:a?g:-1,screen_width:r.screen_width,screen_height:r.screen_height,viewport_width:n.viewport_width,viewport_height:n.viewport_height,page_width:a?o.page_width:-1,page_height:a?o.page_height:-1,scroll_top:a?i.scroll_top:-1,scroll_left:a?i.scroll_left:-1}}(o)):{page_key:l,page_name:o.page_name||"undefined"!=typeof document&&document.title||l,recommend_trace_id:"",referrer_page_key:u.previousPageKey||""},i||s?i&&(r.is_sdk_auto_collected=1):delete r.is_sdk_auto_collected,function(e,t){var n,r,o=Ae({},e||{}),i=Object.keys(t||{});for(n=0;i.length>n;n++)Le(o[r=i[n]])||(o[r]=t[r]);return o}(o,r)}(o.payload,0,i)),Z().then(function(){var n,a=G();if(a.endedSession&&qn)try{qn(a.endedSession)}catch(e){}return n=a.sid||z(),Bn[i]&&o.payload&&function(t,n){var r,o,i,a=((t.event||"")+"").trim();Bn[a]&&t.payload&&("app_page_view"===a?1===t.payload.is_sdk_auto_collected?(r=t.payload._pageSeqPrealloc||ae(),o=t.payload._metricsRecorded,null!=t.payload._pageSeqPrealloc&&delete t.payload._pageSeqPrealloc,t.payload=function(e,t,n){var r=e||{},o=Fe(),i=Math.floor(Date.now()/1e3),a=Oe((t||"")+"|"+(n||0)+"|"+i),s=Ue(),c=qe(r.page_key||""),u=(r.page_name||"")+"";return{user_type:(null!=r.user_type?r.user_type:"")+"",page_key:c,page_name:u,referrer_page_key:(null!=r.referrer_page_key?r.referrer_page_key:"")+"",referrer_page_name:(null!=r.referrer_page_name?r.referrer_page_name:"")+"",current_page_key:(null!=r.current_page_key?r.current_page_key:c)+"",current_page_name:(null!=r.current_page_name?r.current_page_name:u)+"",recommend_trace_id:(null!=r.recommend_trace_id?r.recommend_trace_id:"")+"",page_id:a,page_enter_ts:i,page_url:r.page_url||Re(),page_load_time:null!=s?s:0,viewport_width:o.viewport_width,viewport_height:o.viewport_height}}(t.payload,n,r),t._pageSeqInSid=r,o&&(t._metricsRecorded=1)):(i=function(t,n){var r,o=t||{},i=null!=o.page_enter_ts?o.page_enter_ts:Math.floor(Date.now()/1e3),a=o.page_seq_in_sid||ae();return{page_seq_in_sid:a,page_id:o.page_id||e((null==(r=(n||"")+"|"+a+"|"+i)?"":r)+"").substr(0,32),page_enter_ts:i}}(t.payload,n),t.payload=We(t.payload,i)):"page_click"===a&&1===t.payload.is_sdk_auto_collected&&(t.payload=function(e){var t=e||{},n=Ge(t._clickSnap||t);return{page_key:qe(t.page_key||""),page_name:(t.page_name||"")+"",recommend_trace_id:(null!=t.recommend_trace_id?t.recommend_trace_id:"")+"",referrer_page_key:(null!=t.referrer_page_key?t.referrer_page_key:"")+"",click_viewport_x:n.click_viewport_x,click_viewport_y:n.click_viewport_y,click_document_x:n.click_document_x,click_document_y:n.click_document_y,click_page_x:n.click_page_x,click_page_y:n.click_page_y,click_x_percent:n.click_x_percent,click_y_percent:n.click_y_percent,screen_width:n.screen_width,screen_height:n.screen_height,viewport_width:n.viewport_width,viewport_height:n.viewport_height,page_width:n.page_width,page_height:n.page_height,scroll_top:n.scroll_top,scroll_left:n.scroll_left}}(t.payload),t._sdkAutoClick=1,delete t.payload.is_sdk_auto_collected,delete t.payload._clickSnap))}(o,n),Qe(t,n,o,r,a)})}function Ze(){try{return"undefined"==typeof location?"":(location.protocol||"")+"//"+(location.host||"")+(location.pathname||"")}catch(e){return""}}function et(){var t,n;try{return(t=function(){try{return document.scrollingElement||document.documentElement||document.body||null}catch(e){return null}}())&&(n=Math.max(0,(t.scrollHeight||0)-(t.clientHeight||0)))>0?Math.min(100,Math.max(0,Math.round((t.scrollTop||0)/n*100))):0}catch(e){return 0}}function tt(){if(On)return"navigation";try{if("hidden"===document.visibilityState)return"visibility_hidden"}catch(e){}return"unknown"}function nt(){return function(){if(xn.currentPage&&xn.currentPage.page_key)return xn.currentPage.page_key;var e=xn.pageStayList||[];return e.length>0&&e[e.length-1].page_key||""}()||Be()}function rt(){var t,n="unknown";try{n="visible"===document.visibilityState||"hidden"===document.visibilityState?document.visibilityState:"unknown"}catch(e){}return{page_url:Ze(),unload_reason:tt(),scroll_depth_percent_at_unload:et(),web_visibility_state:n,page_stay_duration_ms:(t=xn.currentPage,t?ie(Date.now(),t):0),last_active_page_key:nt()}}function ot(e,t,n,r){var o,i,a=n||{};return a.lifecycle_event_key=t,t===Kn&&(a=at((o=Fe(),{page_url:Ze(),page_load_time_ms:Ue(),viewport_width:o.viewport_width,viewport_height:o.viewport_height}),a)),t===Nn&&(a=at(a,rt())),t===jn&&((i=xn.currentPage)&&!i.hidden_paused&&(i.accumulated_ms=ie(Date.now(),i),i.resume_at_ms=null,i.hidden_paused=!0),a.last_active_page_key||(a.last_active_page_key=nt())),t===Vn&&function(){var e=xn.currentPage;e&&e.hidden_paused&&(e.resume_at_ms=Date.now(),e.hidden_paused=!1)}(),$e(e,{event:"app_lifecycle",payload:a},r)}function it(e,t,n,r){return function(e,t,n,r){var o=We({},n||{});return Z().then(function(){return Qe(e,t,o,r,null)})}(e,r||z(),{event:"app_session_summary",summaryFields:t||{}},n)}function at(e){var t,n,r,o,i=e||{};for(t=1;arguments.length>t;t++)if(n=arguments[t])for(r=Object.keys(n),o=0;r.length>o;o++)i[r[o]]=n[r[o]];return i}function st(e){var t,n;return!e||2>e.length?0:Math.sqrt((t=e[0].clientX-e[1].clientX)*t+(n=e[0].clientY-e[1].clientY)*n)}function ct(){Gn&&(clearTimeout(Gn),Gn=null)}function ut(t,n,r){var o,i,a,s,c,u;if(r=r||{},!t)return Promise.resolve({sent:!1,ok:!1,status:0,reason:"no_endpoint"});if(!Array.isArray(n)||0===n.length)return Promise.resolve({sent:!0,ok:!0,status:200,reason:"empty_batch"});if(o=JSON.stringify(n),i=!0===r.useBeacon,a=!1!==r.requireHttps,c=isFinite(s=+r.timeoutMs)&&s>0?Math.floor(s):Qn,!function(e,t){if(!t)return!0;var n=((e||"")+"").match(/^(https?):\/\/([^/:?#]+)/i);return!(!n||"https"!==n[1].toLowerCase()&&("http"!==n[1].toLowerCase()||!function(e){var t=((e||"")+"").toLowerCase();return"localhost"===t||"127.0.0.1"===t||"::1"===t}(n[2])))}(t,a))return void 0!==console&&console.warn&&console.warn("[sdk][web] 已拒绝非 HTTPS 上报地址(localhost HTTP 除外)"),Promise.resolve({sent:!1,ok:!1,status:0,reason:"insecure_endpoint"});if(i&&"undefined"!=typeof navigator&&"function"==typeof navigator.sendBeacon)try{if(u=new Blob([o],{type:"application/json"}),navigator.sendBeacon(t,u))return Promise.resolve({sent:!0,ok:!0,status:200,transport:"beacon",uncertain:!0})}catch(e){}return function(e,t,n){var r,o;return"function"!=typeof fetch?Promise.resolve(null):(r="undefined"!=typeof AbortController?new AbortController:null,o=r&&n>0?setTimeout(function(){r.abort()},n):null,fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:t,keepalive:!0,signal:r?r.signal:void 0}).then(function(e){return o&&clearTimeout(o),{sent:!0,ok:e.ok,status:+(e.status||0),transport:"fetch"}}).catch(function(e){return o&&clearTimeout(o),e&&"object"==typeof e&&"AbortError"===e.name?{sent:!1,ok:!1,status:0,reason:"timeout",transport:"fetch"}:null}))}(t,o,c).then(function(n){return null!==n?n:function(t,n,r){return new Promise(function(o){try{if("undefined"==typeof XMLHttpRequest)return o({sent:!1,ok:!1,status:0,reason:"no_xhr"});var i=new XMLHttpRequest;i.open("POST",t,!0),i.timeout=r,i.setRequestHeader("Content-Type","application/json"),i.onreadystatechange=function(){4===i.readyState&&o({sent:!0,ok:i.status>=200&&300>i.status,status:i.status,transport:"xhr"})},i.onerror=function(){o({sent:!1,ok:!1,status:0,reason:"xhr_error"})},i.ontimeout=function(){o({sent:!1,ok:!1,status:0,reason:"timeout",transport:"xhr"})},i.send(n)}catch(e){o({sent:!1,ok:!1,status:0,reason:"xhr_exception"})}})}(t,o,c)})}function lt(e){e=e||{},this.selectedBatchEndpoint="",this.remoteEndpoints=[],this.remoteProbePromise=null,this.remoteProbeKey="",this.shouldPersistOnFail=!1,this.hasBatchProbeStarted=!1,this.initialized=!1,this.initPromise=null,this.endpointReadyListeners=[],this.timeoutMs=+e.pingTimeoutMs||1500}function dt(e){return 255&(e<<1^(128&e?27:0))}function _t(e){return(Mt[e>>>24&255]<<24|Mt[e>>>16&255]<<16|Mt[e>>>8&255]<<8|Mt[255&e])>>>0}function pt(e,t){var n,r,o,i,a,s,c,u,l,d,_,p,f=e.slice(0);for(o=[],i=0;16>i;i++)f[i]=255&(f[i]^t[i>>2]>>>24-8*(3&i));for(a=1;14>=a;a++){for(n=[],i=0;16>i;i++)n[i]=Mt[f[i]];for(r=[],c=0;4>c;c++)for(u=0;4>u;u++)r[c+4*u]=n[c+4*(u+c&3)];if(14>a){for(o=[],s=0;4>s;s++)d=r[4*s+1],_=r[4*s+2],p=r[4*s+3],o[4*s]=255&(dt(l=r[4*s])^dt(d)^d^_^p),o[4*s+1]=255&(l^dt(d)^dt(_)^_^p),o[4*s+2]=255&(l^d^dt(_)^dt(p)^p),o[4*s+3]=255&(dt(l)^l^d^_^dt(p));for(f=o,o=[],i=0;16>i;i++)f[i]=255&(f[i]^t[4*a+(i>>2)]>>>24-8*(3&i))}else for(f=r,i=0;16>i;i++)f[i]=255&(f[i]^t[4*a+(i>>2)]>>>24-8*(3&i))}return f}function ft(e,t){var n,r,o,i=[],a=t.slice(0);for(n=0;16>n;n++)i[n]=0;for(n=0;128>n;n++){if(e[n>>3]&128>>(7&n))for(r=0;16>r;r++)i[r]^=a[r];for(o=1&a[15],r=15;r>0;r--)a[r]=255&(a[r]>>>1|a[r-1]<<7);a[0]=a[0]>>>1&255,o&&(a[0]^=225)}return i}function ht(e){var t,n=e.slice(0);for(t=15;t>=12&&(n[t]=n[t]+1&255,!n[t]);t--);return n}function gt(e){this.encryptedConfigText=(e=e||{}).encryptedConfig||e.configCipher||"",this.cachedConfigPromise=null,this._secondGcmKeyBase64="en1BNo0VBrN/zi+mI2LO7E9W40ehCBYwC+frBn8s3rQ="}function mt(e,t){var n=((e&&e.event_id||"")+"").trim();return n?n+"|||"+(t||""):null}function yt(e){var t,n={},r=Object.keys(e);for(t=0;r.length>t;t++)n[r[t]]=e[r[t]];return n}function vt(){this.maxSize=tr}function wt(e){var t,n,r=e||{},o=r.im_behavior_key,i=qt[o];if(!i)throw(t=Error("[sdk][web] 未知 im_behavior_key:"+o)).name="EventValidationError",t.eventMethod="imEvent",t.typeErrorFields=["im_behavior_key(unknown)"],t;return n={},Object.keys(r).forEach(function(e){"im_behavior_name"!==e&&(n[e]=r[e])}),n.im_behavior_name=i,n}function kt(e,t){var n,r,o,i,a,s=Lt[e];if(!s)throw Error("[sdk][web] 未知事件方法:"+e);if(n=t||{},r={},o=[],i=[],Object.keys(s).forEach(function(e){var t,a=s[e],c=1&a,u=a>>1&3,l=!!(8&a),d=n[e];l&&null===d&&(d=""),!c||l||null!=d&&""!==d?c&&l&&void 0===d?o.push(e):(c||null!=d)&&(t=1===u?function(e){return"number"==typeof e&&isFinite(e)&&e===Math.floor(e)}(d):2===u?function(e){return"number"==typeof e&&isFinite(e)}(d):function(e){return"string"==typeof e}(d),t?r[e]=d:i.push(e+"(expected "+(1===u?"int":2===u?"number":"string")+")")):o.push(e)}),o.length||i.length)throw(a=Error("[sdk][web] 事件字段校验失败:"+e)).name="EventValidationError",a.eventMethod=e,a.missingFields=o.slice(),a.typeErrorFields=i.slice(),a;return r}function bt(){var e,t,n,r,o={};for(e=0;arguments.length>e;e++)if(t=arguments[e])for(n=Object.keys(t),r=0;n.length>r;r++)o[n[r]]=t[n[r]];return o}function Ct(e){var t,n,r={};if(!e||!Array.isArray(e))return r;for(t=0;e.length>t;t++)(n=((null==e[t]?"":e[t])+"").trim())&&(Vt[n]?r[n]=1:Kt('[sdk][web] disableAutoEvents 含未知事件 "'+n+'",已忽略'));return r}function St(e){var t,n=[],r=Object.keys(e||{});for(t=0;r.length>t;t++)n.push(r[t]);return n}function Pt(e,t){this.opts=e,this.resolver=t,this.queue=[],this.timer=null,this.flushTail=Promise.resolve(),this.replaying=!1,this.replayTimer=null,this.hasPersistentItems=!0,this.persistentQueue=new vt,this._resolveEndpoint=null,this._getEventCodes=null,this.startReplayTimer()}function Et(e){var t,n;if("undefined"==typeof window)return Kt("[web-sdk] 非浏览器环境(SSR/Node.js),SDK 仅支持浏览器,初始化已跳过。"),this._ssrSkipped=!0,this.options=e,this.configMgr=null,this.resolver=null,void(this.batcher=null);if(!Object.prototype.hasOwnProperty.call(e,"channel"))throw Error('channel 为必传字段(可传空字符串 "")');this._ssrSkipped=!1,(t=bt(e)).channel=null==e.channel?"":e.channel+"",this._disableAutoEvents=Ct(t.disableAutoEvents),t.disableAutoEvents=St(this._disableAutoEvents),this.options=t,this.configMgr=new gt(e),this.resolver=new lt({pingTimeoutMs:nr}),this.batcher=new Pt(this.options,this.resolver),n=this,this.batcher._resolveEndpoint=function(e){return n.resolver.resolveEndpoint(function(){return n.configMgr.getBatchEndpoints()},e)},this.batcher._getEventCodes=function(){return n.configMgr.getEventCodes()},this.batcher._getSigningConfig=function(){return n.configMgr.getSigningConfig()},this._unsubReady=null,this._refreshTimer=null,this._autoLifecycleTeardown=null,this._webLoadTeardown=null,this._autoNetworkTeardown=null,this._pageTrackerTeardown=null,this._webInteractionTeardown=null,this._sessionPollTimer=null,this._autoEventsStarted=!1,this._init({}),this._startAutoEvents()}function Tt(e){if(!Ht)throw Error("请先调用 init() 初始化(参数须包含 channel)");return Ht}function xt(e,t,n){var r,o,i;if(n>=e.length)return Promise.resolve();if(!Ht||Ht!==t){for(o=(r=e.slice(n)).length-1;o>=0;o--)Ut.unshift(r[o]);return Promise.resolve()}return t.track((i=e[n]).event,i.endpoint).catch(function(){}).then(function(){return xt(e,t,n+1)})}function Dt(e,t){var n;return Ht?(n=Ht,(Xt||Promise.resolve()).then(function(){return n.track(e,t)})):(Ut.push({event:e,endpoint:t}),Ut.length>500&&Ut.shift(),Promise.resolve({sent:!1,ok:!0,status:0,reason:"pre_init_cached"}))}var Mt,It,At,Ot,Lt,Rt,qt,Bt,Kt,Vt,jt,Nt,Ft,Ht,Ut,Xt,Yt,Gt,zt=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298],Wt="3.0.1",Jt="sig_v2",Qt=["os_family","browser_kernel","browser_platform","screen_width","screen_height","screen_dpi","device_pixel_ratio","color_depth","max_touch_points","cpu_cores","cookie_enabled","local_storage_enabled","session_storage_enabled","indexeddb_enabled","service_worker_enabled","canvas_fp","webgl_fp","audio_fp","fonts_hash","plugins_hash","mimes_hash"],$t=["Arial","Arial Black","Comic Sans MS","Courier New","Georgia","Impact","Times New Roman","Trebuchet MS","Verdana","Microsoft YaHei","SimHei","SimSun","PingFang SC","Helvetica","Roboto","Monaco","Menlo","Consolas","Palatino Linotype","Tahoma","Lucida Console","Candara","Futura","Geneva"],Zt=[{kind:"video",type:'video/mp4; codecs="avc1.42E01E"'},{kind:"video",type:'video/webm; codecs="vp8, vorbis"'},{kind:"video",type:'video/ogg; codecs="theora"'},{kind:"video",type:"video/mp4"},{kind:"video",type:"video/webm"},{kind:"audio",type:"audio/mpeg"},{kind:"audio",type:'audio/ogg; codecs="vorbis"'},{kind:"audio",type:'audio/wav; codecs="1"'},{kind:"audio",type:"audio/mp4"},{kind:"audio",type:"audio/aac"}],en={cold_start:1,web_load:1,warm_start:1,foreground:1,background:1,web_unload:1,terminate:1,unknown:1},tn="__web_sdk_sid__",nn="__web_sdk_sid_meta__",rn="__web_sdk_sid__",on="__web_sdk_sid_v1",an=18e5,sn=216e5,cn=null,un=!1,ln="__web_sdk_device_id__",dn="__web_sdk_ping__",_n=null,pn="__web_sdk_device_fp__",fn=null,hn="",gn=null,mn=function(){var e="";return{append:function(t,n){var r=((t||"")+"").trim();r&&(e+=r+"|"+(null!=n?n+"":"")+";")},digest:function(){return d(e)},reset:function(){e=""}}}(),yn=50,vn=16384,wn="__web_sdk_pending_session_summaries__",kn="__web_sdk_session_metrics_v1__",bn="__web_sdk_summary_claimed__:",Cn=!1,Sn="",Pn={},En={},Tn={sdk_init:1,app_lifecycle:1,app_session_summary:1,network_state_change:1},xn={eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null},Dn=Date.now(),Mn=0,In=0,An=[],On=!1,Ln="",Rn="",qn=null,Bn={app_page_view:1,page_click:1},Kn="web_load",Vn="foreground",jn="background",Nn="web_unload",Fn=null,Hn=0,Un=0,Xn=0,Yn=0,Gn=null,zn=!1,Wn=null,Jn=!1,Qn=1e4,$n="1.2.0",Zn=5e3,er=3e4,tr=2e3,nr=1500,rr=!1;if(lt.prototype.normalizeBatchEndpoints=function(e){return(Array.isArray(e)?e:[]).map(function(e){return((e||"")+"").trim()}).filter(Boolean).filter(function(e){return/^https:\/\//i.test(e)}).map(function(e){var t=e.match(/^(https?:\/\/[^/?#]+)/i);return(t?t[1]:e.replace(/\/+$/,""))+"/api/eventTracking/batchReport.json"})},lt.prototype._withTimeout=function(e,t){return new Promise(function(n,r){var o=setTimeout(function(){r(Error("timeout"))},t);e.then(function(e){clearTimeout(o),n(e)}).catch(function(e){clearTimeout(o),r(e)})})},lt.prototype._xhrRace=function(t,n){return t.length?new Promise(function(r){function o(){for(var t=0;c.length>t;t++)try{c[t].abort()}catch(e){}}var i,a=t.length,s=!1,c=[];for(i=0;t.length>i;i++)(function(t){var i,u,l;try{function d(e){l||(l=!0,u&&(clearTimeout(u),u=null),e&&!s?(s=!0,o(),r(t)):e||(a--,s||0!==a||r("")))}i=new XMLHttpRequest,u=null,l=!1,c.push(i),u=setTimeout(function(){d(!1)},n),i.open("POST",t,!0),i.setRequestHeader("Content-Type","application/json"),i.onreadystatechange=function(){4===i.readyState&&d(i.status>0)},i.onerror=function(){d(!1)},i.send("[]")}catch(e){a--,s||0!==a||r("")}})(t[i])}):Promise.resolve("")},lt.prototype._pickFastest=function(t,n){function r(){if(o)for(var t=0;i.length>t;t++)try{i[t].abort()}catch(e){}}var o,i,a=this;return"undefined"==typeof fetch?a._xhrRace(t,n):t.length?(o="undefined"!=typeof AbortController,i=[],new Promise(function(e){var s,c=t.length,u=!1;for(s=0;t.length>s;s++)(function(t){var s,l=o?new AbortController:null;l&&i.push(l),s={method:"POST",mode:"cors",headers:{"Content-Type":"application/json"},body:"[]",cache:"no-store"},l&&(s.signal=l.signal),a._withTimeout(fetch(t,s),n).then(function(n){n&&n.status>0?u||(u=!0,r(),e(t)):(c--,u||0!==c||e(""))}).catch(function(){c--,u||0!==c||e("")})})(t[s])})):Promise.resolve("")},lt.prototype._notifyReady=function(){var t=this.selectedBatchEndpoint;this.endpointReadyListeners.forEach(function(n){try{n(t)}catch(e){}})},lt.prototype._setSelectedEndpoint=function(e){var t=(e||"")+"";t&&t!==this.selectedBatchEndpoint&&(this.selectedBatchEndpoint=t,this._notifyReady())},lt.prototype.startRemoteProbe=function(e){var t,n,r=e.join("|");return e.length?this.remoteProbePromise&&this.remoteProbeKey===r?this.remoteProbePromise:(this.remoteProbeKey=r,this.shouldPersistOnFail=!1,t=this,n=this._pickFastest(e,this.timeoutMs).then(function(r){if(t.remoteProbePromise!==n)return t.selectedBatchEndpoint;if(t.remoteProbePromise=null,r)t._setSelectedEndpoint(r);else if(!t.selectedBatchEndpoint&&e.length){t.shouldPersistOnFail=!0;var o=e[Math.floor(Math.random()*e.length)];t._setSelectedEndpoint(o)}return t.selectedBatchEndpoint}).catch(function(){return t.remoteProbePromise===n&&(t.remoteProbePromise=null),t.selectedBatchEndpoint}),this.remoteProbePromise=n,n):Promise.resolve("")},lt.prototype.refreshDomainConfig=function(e){var t=this;return e().then(function(e){var n=t.normalizeBatchEndpoints(e);return t.remoteEndpoints=n,n.length?(t.hasBatchProbeStarted=!0,t.startRemoteProbe(n)):t.selectedBatchEndpoint})},lt.prototype.init=function(e){return this.initialized?Promise.resolve():(this.initPromise||(this.initialized=!0,this.initPromise=this.refreshDomainConfig(e).catch(function(){})),this.initPromise)},lt.prototype.resolveEndpoint=function(e,t){if(t)return Promise.resolve(t);var n=this;return this.init(e).then(function(){return n.selectedBatchEndpoint||""})},lt.prototype.onEndpointReady=function(e){if("function"!=typeof e)return function(){};this.endpointReadyListeners.push(e);var t=this;return function(){var n=t.endpointReadyListeners.indexOf(e);0>n||t.endpointReadyListeners.splice(n,1)}},lt.prototype.consumeProbeFailurePersistSignal=function(){return!!this.shouldPersistOnFail&&(this.shouldPersistOnFail=!1,!0)},lt.prototype.getEndpointState=function(){return this.selectedBatchEndpoint?"ready":this.remoteEndpoints.length>0?"probing":"unavailable"},lt.prototype.getSelectedBatchEndpoint=function(){return this.selectedBatchEndpoint},Mt=function(){var e,t=[];for(e=0;1272>e;e+=2)t.push(parseInt("637c777bf26b6fc53001672bed2a28a248888da76f3248a3043813899432c6b326fb8d0d37e84760dad3574759e0b764532a380602b1fc71a9e7f97d3b6415c21d02b5612440a4500bf9c043d33e085f45c20e0b65124603dfa8cd7a76b65c11b829e69a4e7f7872c6c7150b6a0f0801d163ae27d4d6cd5d19e77a9b1b48aaf2428594d3752f1d67250bab67676203b9b40df3d7cdf7eebf4b1bd5b6e16416be39a4c4a56e644726087d013b8ea78a6750d8b0339b871ee1b6a9ab66c8032cc1ac2462c695459dc034deae0678ae1779d9e7abce45d167e719b7776c73222f3c7e3cc11ae6da5cd659b79ce60ca82ec97f6059f674c029a68db0853dc22bb6518048ed4f66256d430b74de2f1a95f0589c341befce783e7c6d9dc7dd8e21cb643ee710b3c11329260195e0c0a956f14f970a9e61e594c663f68266a11045baf84a11e051ea5b6a42067253b2d37392c3516e1fc226a5cd4ab8990c475651a4698910219b94e2eb7d2b7129f38c94e3b1246a3298fef32dcae306137c39e1f0e32fe97d11b986c41a55bd5351319f7f078010fa81eb86b8a0556f880000e3630c6a0b136307027bf70e1b5722632937a93b53bf2d240e3949fa5a6e4e6a2a6f4d5b43a39d051df60bc98f25ff3130c31fd85b6e4167b276aed3276b70c3e527f2f2963b60554543e32e1f0c1754766b610c20b0ffe75d3fb88f6fa49605d6a3d6ca3022ffc62f5c8d297b65c778da96c8941ac9c55845e094f1c4a020bff5bb01a0cb04f5a8ac42b1cb41c38d27c1e1358256f3e99e3295a6e3b8c9c0481ed49af7d43f580a8571a0b13162171ebe8666ee5365e9ce6bef0a682080268b3bb67a1076a5776a996c26c2c8c12c7490c356348d10351b5279fa09851ebf".substr(e,2),16));return t}(),It=[1,2,4,8,16,32,64,128,27,54],At="__web_sdk_remote_config_cache__",gt.prototype.setEncryptedConfig=function(e){this.encryptedConfigText!==e&&(this.encryptedConfigText=e,this.cachedConfigPromise=null)},gt.prototype._tryParse=function(t){try{return JSON.parse(t)}catch(e){return t}},gt.prototype._unwrap=function(e,t){var n,r,o;for(t=t||3,n=e,r=0;t>r&&"string"==typeof n&&(o=this._tryParse(n))!==n;r++)n=o;return n},gt.prototype._toStringArray=function(e){var t,n=this._unwrap(e);return Array.isArray(n)?n.map(function(e){return((e||"")+"").trim()}).filter(Boolean):"string"==typeof n&&n.trim()?(t=this._tryParse(n),Array.isArray(t)?t.map(function(e){return((e||"")+"").trim()}).filter(Boolean):[n.trim()]):[]},gt.prototype._getStorage=function(){try{return"undefined"!=typeof window&&window.localStorage?window.localStorage:null}catch(e){return null}},gt.prototype._readPersisted=function(){var t,n,r;try{return(t=this._getStorage())&&(n=t.getItem(At))?(r=JSON.parse(n),Date.now()-(r.updatedAt||0)>24e5?{}:this._mapPayload(r)):{}}catch(e){return{}}},gt.prototype._mapPayload=function(e){if(!e||"object"!=typeof e)return{};var t=e.data&&"object"==typeof e.data?e.data:e;return{batchUrls:this._toStringArray(null!=t.batchUrls?t.batchUrls:t.domainList),eventCodes:this._toStringArray(null!=t.eventCodes?t.eventCodes:t.eventList),signKey:(t.signKey||t.hmacKey||t.eventSignKey||"")+"",securityKeyVersion:(t.securityKeyVersion||t.signKeyVersion||"")+""}},gt.prototype._persist=function(t){try{var n=this._getStorage();if(!n)return;n.setItem(At,JSON.stringify({batchUrls:this._toStringArray(t.batchUrls),eventCodes:this._toStringArray(t.eventCodes),signKey:(t.signKey||"")+"",securityKeyVersion:(t.securityKeyVersion||"")+"",updatedAt:Date.now()}))}catch(e){}},gt.prototype._base64ToBytes=function(e){var t,n,r,o,i=((e||"")+"").trim().replace(/-/g,"+").replace(/_/g,"/"),a=i.length%4?4-i.length%4:0;for(t=0;a>t;t++)i+="=";if("undefined"!=typeof atob&&"undefined"!=typeof Uint8Array){for(n=atob(i),r=new Uint8Array(n.length),o=0;n.length>o;o++)r[o]=n.charCodeAt(o);return r}return new Uint8Array(0)},gt.prototype._decryptAes256Gcm=function(t,n){var r,o,i,a,s,c,u;try{if(28>(r=this._base64ToBytes(t)).length)return Promise.reject(Error("密文过短"));if(o=this._base64ToBytes(n),i=r.slice(0,12),a=r.slice(12),"undefined"!=typeof crypto&&crypto.subtle)return crypto.subtle.importKey("raw",o,{name:"AES-GCM"},!1,["decrypt"]).then(function(e){return crypto.subtle.decrypt({name:"AES-GCM",iv:i},e,a)}).then(function(e){return new Uint8Array(e)});if(!(s=function(e,t,n){var r,o,i,a,s,c,u,l,d,_,p,f,h,g,m=[],y=[],v=[],w=[];for(r=0;32>r;r++)m[r]=e[r]||0;for(r=0;12>r;r++)y[r]=t[r]||0;if(0>(i=n.length-16))return null;for(r=0;i>r;r++)v[r]=n[r];for(r=0;16>r;r++)w[r]=n[i+r];for(a=function(e){var t,n,r=[];for(t=0;8>t;t++)r[t]=(e[4*t]<<24|e[4*t+1]<<16|e[4*t+2]<<8|e[4*t+3])>>>0;for(t=8;60>t;t++)n=r[t-1],t%8?t%8==4&&(n=_t(n)):n=(_t((n<<8|n>>>24)>>>0)^It[t/8-1]<<24)>>>0,r[t]=(r[t-8]^n)>>>0;return r}(m),s=[],r=0;16>r;r++)s[r]=0;for(c=pt(s,a),u=y.slice(0),r=12;15>r;r++)u[r]=0;for(u[15]=1,l=pt(u,a),d=ht(u),_=[],r=0;i>r;r+=16)for(p=pt(d,a),d=ht(d),o=0;16>o&&i>r+o;o++)_[r+o]=v[r+o]^p[o];for(f=v.slice(0);15&f.length;)f.push(0);for(r=0;8>r;r++)f.push(0);for(h=8*i,f.push(0,0,0,0),f.push(h>>>24&255,h>>>16&255,h>>>8&255,255&h),g=function(e,t){var n,r,o=[];for(n=0;16>n;n++)o[n]=0;for(n=0;t.length>n;n+=16){for(r=0;16>r;r++)o[r]^=t[n+r]||0;o=ft(o,e)}return o}(c,f),r=0;16>r;r++)if((l[r]^g[r])!==w[r])return null;return _}(o,i,a)))return Promise.reject(Error("AES-GCM 认证标签校验失败"));if("undefined"!=typeof Uint8Array)for(c=new Uint8Array(s.length),u=0;s.length>u;u++)c[u]=s[u];else c=s;return Promise.resolve(c)}catch(e){return Promise.reject(e)}},gt.prototype.fetchAndDecryptConfig=function(){var e,t,n;return this.cachedConfigPromise?this.cachedConfigPromise:(e=this,t=this._readPersisted(),this.encryptedConfigText?(n=this._decryptAes256Gcm(this.encryptedConfigText,this._secondGcmKeyBase64).then(function(t){var n,r,o,i;if("undefined"!=typeof TextDecoder)n=(new TextDecoder).decode(t);else{for(r=[],o=0;t.length>o;o++)r[o]=t[o];n=String.fromCharCode.apply(null,r)}return i=e._mapPayload(JSON.parse(n)),e._persist(i),i}).catch(function(e){return void 0!==console&&console.warn&&console.warn("[sdk][config] 解密失败:",e),t}),this.cachedConfigPromise=n,n):(this.cachedConfigPromise=Promise.resolve(t),this.cachedConfigPromise))},gt.prototype.getBatchEndpoints=function(){return this.fetchAndDecryptConfig().then(function(e){return e.batchUrls||[]})},gt.prototype.getEventCodes=function(){return this.fetchAndDecryptConfig().then(function(e){return Array.isArray(e.eventCodes)&&e.eventCodes.length>0?e.eventCodes:[]})},gt.prototype.getSigningConfig=function(){return this.fetchAndDecryptConfig().then(function(e){return{hmacKey:(e.signKey||"")+"",securityKeyVersion:(e.securityKeyVersion||"")+""}})},gt.prototype.prewarm=function(){this.fetchAndDecryptConfig().catch(function(){})},gt.prototype.forceRefresh=function(){this.cachedConfigPromise=null},Ot="events",vt.prototype.isAvailable=function(){return"undefined"!=typeof indexedDB&&"undefined"!=typeof window&&null!=window.indexedDB},vt.prototype.openDb=function(){var t=this;return new Promise(function(n,r){if(!t.isAvailable())return n(null);var o=indexedDB.open("web_sdk_event_queue",3);o.onerror=function(){r(o.error||Error("open indexedDB failed"))},o.onupgradeneeded=function(t){var n,r,i,a,s=o.result,c=t.oldVersion,u=t.target.transaction;if(!s.objectStoreNames.contains(Ot))return(n=s.createObjectStore(Ot,{keyPath:"id",autoIncrement:!0})).createIndex("createdAt","createdAt",{unique:!1}),void n.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!0});2>c&&u&&((r=u.objectStore(Ot)).indexNames.contains("pendingDedupeKey")||r.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!1})),3>c&&u&&((i=u.objectStore(Ot)).indexNames.contains("pendingDedupeKey")&&i.deleteIndex("pendingDedupeKey"),(a=i.getAll()).onsuccess=function(){var t,n,r,o,s,c,u,l=a.result||[],d={};for(t=0;l.length>t;t++)(r=(n=l[t]).pendingDedupeKey||mt(n.record,n.endpointOverride))&&(d[r]||(d[r]=[]),d[r].push(n.id));for(o=Object.keys(d),s=0;o.length>s;s++)if((c=d[o[s]]).length>=2)for(c.sort(function(e,t){return e-t}),u=1;c.length>u;u++)i.delete(c[u]);try{i.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!0})}catch(e){}},a.onerror=function(){try{i.createIndex("pendingDedupeKey","pendingDedupeKey",{unique:!0})}catch(e){}})},o.onsuccess=function(){n(o.result)}})},vt.prototype._txDone=function(e){return new Promise(function(t,n){e.oncomplete=function(){t()},e.onerror=function(){n(e.error||Error("tx failed"))},e.onabort=function(){n(e.error||Error("tx aborted"))}})},vt.prototype._trimOverflow=function(e){var t=this,n=Ot,r=e.transaction(n,"readonly");return new Promise(function(e,t){var o=r.objectStore(n).count();o.onsuccess=function(){e(+(o.result||0))},o.onerror=function(){t(o.error)}}).then(function(e){return t._txDone(r).then(function(){return e})}).then(function(r){var o,i,a,s;if(r>t.maxSize)return o=r-t.maxSize,i=e.transaction(n,"readwrite"),a=i.objectStore(n).index("createdAt"),s=0,new Promise(function(e,t){var r=a.openCursor();r.onerror=function(){t(r.error)},r.onsuccess=function(){var t=r.result;if(!t||s>=o)return e();i.objectStore(n).delete(t.primaryKey),s++,t.continue()}}).then(function(){return t._txDone(i)})})},vt.prototype.enqueueMany=function(e){var t=this;return e&&e.length?this.openDb().then(function(n){var r,o;if(n)return r=Date.now(),o=e.map(function(e,t){var n=e&&"object"==typeof e&&"record"in e?e:{record:e},o=mt(n.record,n.endpointOverride||""),i={createdAt:r+t,record:n.record,endpointOverride:n.endpointOverride||"",retryCount:0,nextRetryAt:0};return o&&(i.pendingDedupeKey=o),i}),new Promise(function(e,t){var r,i=n.transaction(Ot,"readwrite"),a=i.objectStore(Ot);i.oncomplete=function(){e()},i.onerror=function(){t(i.error)},i.onabort=function(){t(i.error)},r=0,function e(){var n,i;o.length>r&&(n=o[r++],(i=a.add(n)).onsuccess=function(){e()},i.onerror=function(n){var r=i.error;if(r&&"ConstraintError"===r.name)return n.preventDefault(),void e();t(r||Error("add failed"))})}()}).then(function(){return t._trimOverflow(n)}).then(function(){n.close()}).catch(function(e){throw n.close(),e})}):Promise.resolve()},vt.prototype.readOldest=function(e){return e=e||20,this.openDb().then(function(t){var n,r;return t?(n=Date.now(),r=[],new Promise(function(o,i){var a=t.transaction(Ot,"readonly").objectStore(Ot).index("createdAt").openCursor();a.onerror=function(){i(a.error)},a.onsuccess=function(){var t,i,s,c,u,l=a.result;if(!l||r.length>=e)return o();(s=isFinite(i=+(t=l.value).nextRetryAt)?Math.floor(i):0)>n||((c=yt(t)).endpointOverride=t.endpointOverride||"",c.retryCount=isFinite(u=+t.retryCount)?Math.floor(u):0,c.nextRetryAt=s,r.push(c)),l.continue()}}).then(function(){return t.close(),r}).catch(function(e){throw t.close(),e})):[]})},vt.prototype.removeByIds=function(e){var t=this;return e&&e.length?this.openDb().then(function(n){var r,o,i;if(n){for(o=(r=n.transaction(Ot,"readwrite")).objectStore(Ot),i=0;e.length>i;i++)o.delete(e[i]);return t._txDone(r).then(function(){n.close()}).catch(function(e){throw n.close(),e})}}):Promise.resolve()},vt.prototype.updateRetryMeta=function(e,t,n){var r=this;return e&&e.length?this.openDb().then(function(o){var i,a,s,c,u,l,d,_,p,f,h,g,m,y,v;if(!o)return{dropped:0,kept:0};for(i=Date.now(),s=isFinite(a=+t)?Math.max(0,Math.floor(a)):0,u=isFinite(c=+n)?Math.max(0,Math.floor(c)):0,l=0,d=0,p=(_=o.transaction(Ot,"readwrite")).objectStore(Ot),f=0;e.length>f;f++)u<(m=(isFinite(g=+(h=e[f]).retryCount)?Math.floor(g):0)+1)?(l++,p.delete(h.id)):(d++,y=h.pendingDedupeKey||mt(h.record,h.endpointOverride||""),v=yt(h),y&&(v.pendingDedupeKey=y),v.retryCount=m,v.nextRetryAt=i+s,p.put(v));return r._txDone(_).then(function(){return o.close(),{dropped:l,kept:d}}).catch(function(e){throw o.close(),e})}):Promise.resolve({dropped:0,kept:0})},vt.prototype.count=function(){var e=this;return this.openDb().then(function(t){if(!t)return 0;var n=t.transaction(Ot,"readonly");return new Promise(function(e,t){var r=n.objectStore(Ot).count();r.onsuccess=function(){e(+(r.result||0))},r.onerror=function(){t(r.error)}}).then(function(r){return e._txDone(n).then(function(){return t.close(),r})}).catch(function(e){throw t.close(),e})})},Lt={navigation:{navigation_key:1,navigation_name:1},appPageView:{user_type:1,page_key:1,page_name:1,referrer_page_key:9,referrer_page_name:9,current_page_key:1,current_page_name:1,page_load_time:3,recommend_trace_id:0},appInstall:{trace_id:1,attribution_flag:2},pageClick:{page_key:1,page_name:1,click_page_x:3,click_page_y:3,click_x_percent:3,click_y_percent:3,screen_width:3,screen_height:3,recommend_trace_id:0},recommendListView:{page_key:1,page_name:1,recommend_content_type:1,recommend_trace_id:0,recommend_id:1,recommend_trace_info:0,client_version:0},recommendListClick:{page_key:1,page_name:1,recommend_content_type:1,recommend_trace_id:0,recommend_id:1,recommend_trace_info:0,client_version:0},advertising:{event_type:1,advertising_key:1,advertising_name:1,advertising_id:1},videoEvent:{media_id:0,video_id:1,video_title:1,video_type_id:1,video_type_name:1,video_content_type:9,recommend_trace_id:0,video_tag_key:9,video_tag_name:9,video_duration:3,play_duration:3,play_progress:3,play_speed:3,video_behavior_key:1,video_behavior_name:1,post_id:0},novelEvent:{media_id:0,novel_id:1,novel_title:1,chapter_id:0,chapter_name:0,novel_type_id:1,novel_type_name:1,recommend_trace_id:0,novel_tag_key:9,novel_tag_name:9,read_progress:3,page_no:3,novel_behavior_key:1,novel_behavior_name:1},comicEvent:{media_id:0,comic_id:1,comic_title:1,chapter_id:0,chapter_name:0,comic_type_id:1,comic_type_name:1,recommend_trace_id:0,comic_tag_key:9,comic_tag_name:9,read_progress:3,page_no:3,comic_behavior_key:1,comic_behavior_name:1},keywordSearch:{keyword:1,search_result_count:3,search_content_type:0,search_trace_id:0,search_id:0},keywordClick:{keyword:1,click_item_id:1,click_item_type_key:1,click_item_type_name:1,click_position:3,search_trace_id:0},adImpression:{page_key:1,page_name:1,ad_slot_key:1,ad_slot_name:1,ad_id:1,creative_id:0,ad_type:0},adClick:{page_key:1,page_name:1,ad_slot_key:1,ad_slot_name:1,ad_id:1,creative_id:0,ad_type:0},appPageitemExposure:{item_id:1,item_type:1,item_name:1,parent_item_id:1,page_key:1,page_name:1,referrer_page_key:9,referrer_page_name:9,recommend_trace_id:9,recommend_id:9,recommend_trace_info:9},appPageitemClick:{item_id:1,item_type:1,item_name:1,parent_item_id:1,position:3,page_key:1,page_name:1,recommend_trace_id:9,recommend_id:9,recommend_trace_info:9},adRequest:{request_id:1,ad_slot_key:1},adFill:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,fill_count:3,is_skippable:3,is_closable:3},adNoFill:{request_id:1,ad_slot_key:1,ad_type:1,reason:1},adLoadSuccess:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,load_duration:3},adImpressionSdk:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,material_size:1,is_skippable:3,is_closable:3},adClickSdk:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1},adClose:{trace_id:1,request_id:1,ad_slot_key:1,ad_id:1,ad_type:1,close_type:1},adError:{trace_id:1,request_id:0,ad_slot_key:1,ad_type:1,ad_id:9,error_code:1,error_msg:1,error_stage:1},imEvent:{conversation_id:1,conversation_type:1,current_uid:1,target_uid:1,target_role_key:1,target_role_name:1,message_id:9,message_type:9,im_behavior_key:1,im_behavior_name:1},imConversationView:{current_uid:1,conversation_count:3,total_unread_count:3,normal_conversation_count:3,archived_count:3,pinned_count:3},imConnectionSuccess:{current_uid:9,im_sdk_version:1},imMessageContent:{conversation_id:1,conversation_type:1,current_uid:1,target_uid:1,sender_uid:1,message_id:1,message_type:1,message:9,message_content_length:3,content_hash:9},postEvent:{media_id:0,post_id:1,post_title:1,post_create_time:3,post_user_id:1,post_user_nick:1,post_type_id:1,post_type_name:1,post_content_type:1,recommend_trace_id:9,post_tag_key:9,post_tag_name:9,post_behavior_key:1,post_behavior_name:1},dramaEvent:{media_id:0,drama_id:1,drama_title:1,episode:3,total_episode:3,video_id:1,drama_type_id:1,drama_type_name:1,drama_content_type:1,recommend_trace_id:9,drama_tag_key:9,drama_tag_name:9,drama_duration:3,play_duration:3,play_progress:3,play_speed:3,play_behavior_key:1,play_behavior_name:1},dramaStatusChange:{media_id:0,drama_id:1,drama_title:1,episode:3,video_id:1,pre_episode:3,drama_behavior_key:1,drama_behavior_name:1}},qt={message_send:"发送消息",message_receive:"收到消息",message_read:"消息已读",message_recall:"消息撤回",conversation_pin:"置顶会话",conversation_unpin:"取消置顶",conversation_archive:"归档会话",conversation_unarchive:"取消归档"},Bt=Object.keys(Rt={navigation:"navigation",appPageView:"app_page_view",appInstall:"app_install",pageClick:"page_click",recommendListView:"recommend_list_view",recommendListClick:"recommend_list_click",advertising:"advertising",videoEvent:"video_event",novelEvent:"novel_event",comicEvent:"comic_event",keywordSearch:"keyword_search",keywordClick:"keyword_click",adImpression:"ad_impression",adClick:"ad_click",appPageitemExposure:"app_pageitem_exposure",appPageitemClick:"app_pageitem_click",adRequest:"ad_request",adFill:"ad_fill",adNoFill:"ad_no_fill",adLoadSuccess:"ad_load_success",adImpressionSdk:"ad_impression_sdk",adClickSdk:"ad_click_sdk",adClose:"ad_close",adError:"ad_error",imEvent:"im_event",imConversationView:"im_conversation_view",imConnectionSuccess:"im_connection_success",imMessageContent:"im_message_content",postEvent:"post_event",dramaEvent:"drama_event",dramaStatusChange:"drama_status_change"}).reduce(function(e,t){var n=Rt[t];return e[t]="imEvent"===t?function(e){return{event:n,payload:kt(t,wt(e))}}:function(e){return{event:n,payload:kt(t,e||{})}},e},{}),Kt=void 0!==console&&console.warn?function(e){console.warn(e)}:function(){},Vt={sdk_init:1,app_lifecycle:1,network_state_change:1,app_session_summary:1,app_page_view:1,page_click:1},jt=6e4,Nt="__web_sdk_recent_ok_event_ids__",Ft=function(){function t(e){var t,n=Object.keys(r);for(t=0;n.length>t;t++)e-r[n[t]]>jt&&delete r[n[t]]}function n(){try{"undefined"!=typeof window&&window.localStorage&&window.localStorage.setItem(Nt,JSON.stringify(r))}catch(e){}}var r={};if(function(){var t,n,o,i,a;try{if("undefined"!=typeof window&&window.localStorage&&(t=window.localStorage.getItem(Nt)))for(n=JSON.parse(t),o=Date.now(),i=Object.keys(n),a=0;i.length>a;a++)/^[0-9a-f]{32}$/.test(i[a])&&(o-n[i[a]]>jt||(r[i[a]]=n[i[a]]))}catch(e){}}(),"undefined"!=typeof window)try{window.addEventListener("storage",function(e){var t,n,o,i,a;try{if(e.key!==Nt||!e.newValue)return;for(t=JSON.parse(e.newValue),n=Date.now(),o=Object.keys(t),i=0;o.length>i;i++)a=t[o[i]],/^[0-9a-f]{32}$/.test(o[i])&&(n-a>jt||(r[o[i]]=a))}catch(s){}})}catch(e){}return{has:function(e){var n,o;return!!e&&(t(n=Date.now()),void 0!==(o=r[e])&&jt>=n-o)},set:function(e){var o,i,a,s,c,u=Date.now();for(t(u),o=0;e.length>o;o++)e[o]&&(r[e[o]]=u);if((i=Object.keys(r)).length>500){for(a="",s=1/0,c=0;i.length>c;c++)s>r[i[c]]&&(s=r[i[c]],a=i[c]);a&&delete r[a]}n()},reset:function(){r={},n()}}}(),Pt.prototype.startReplayTimer=function(){var e=this;this.replayTimer=setInterval(function(){e.replayPersistent()},Zn)},Pt.prototype._stopTimer=function(){this.timer&&(clearTimeout(this.timer),this.timer=null)},Pt.prototype._settleItem=function(e,t){e.settled||(e.settled=!0,e.timeoutTimer&&(clearTimeout(e.timeoutTimer),e.timeoutTimer=null),e.resolve(t))},Pt.prototype._purgeSettled=function(){this.queue.length&&(this.queue=this.queue.filter(function(e){return!e.settled}))},Pt.prototype._toPersistentEntries=function(e){return e.map(function(e){return{record:e.record,endpointOverride:e.endpointOverride||"",retryCount:0,nextRetryAt:0}})},Pt.prototype._getRetryDelay=function(e){return Math.min(6e4,Math.max(Zn,Math.floor(Zn*Math.pow(2,Math.min(10,Math.max(0,e))))))},Pt.prototype.schedule=function(){var e=this;this._purgeSettled(),!this.timer&&this.queue.length&&(this.timer=setTimeout(function(){e.timer=null,e.flush().catch(function(){})},5e3))},Pt.prototype.flush=function(){var e=this,t=this.flushTail.then(function(){return e.flushOnce()});return this.flushTail=t.catch(function(){}),t},Pt.prototype._processFlushGroup=function(e,t,n){var r,o,i,a,s,c,u,l,d,_,p,f=this,h="__default__"===e?"":e,g=[],m=[];for(r=0;t.length>r;r++)((i=(((o=t[r]).record&&o.record.event_id||"")+"").trim())&&Ft.has(i)?m:g).push(o);for(a=0;m.length>a;a++)f._settleItem(m[a],{sent:!1,ok:!1,status:0,reason:"cross_batch_recent_success_deduped"});if(!g.length)return Promise.resolve();if(!f._resolveEndpoint){for(Kt("[sdk][web] _resolveEndpoint 未注入,事件无法发送"),s=0;g.length>s;s++)f._settleItem(g[s],{sent:!1,ok:!1,status:0,reason:"no_endpoint_resolver"});return Promise.resolve()}for(c={},u=[],l={},d=0;g.length>d;d++)c[p=(((_=g[d]).record&&_.record.event_id||"")+"").trim()||"__empty__:"+d]||(c[p]=!0,u.push(_.record),l[d]=!0);return f.resolver.consumeProbeFailurePersistSignal()?f.persistentQueue.enqueueMany(f._toPersistentEntries(g)).then(function(){f.hasPersistentItems=!0;for(var e=0;g.length>e;e++)f._settleItem(g[e],{sent:!1,ok:!1,status:0,reason:"probe_all_failed"})}).catch(function(){}):f._resolveEndpoint(h).then(function(e){var t,r,o,i,a,s;if(e){for(r=[],o=0;u.length>o;o++)if((i=((u[o]&&u[o].event_id||"")+"").trim())&&Ft.has(i)){for(a=0;g.length>a;a++)if(l[a]&&((g[a].record&&g[a].record.event_id||"")+"").trim()===i){f._settleItem(g[a],{sent:!1,ok:!1,status:0,reason:"cross_batch_recent_success_deduped"}),l[a]=!1;break}}else r.push(u[o]);if(!r.length){for(s=0;g.length>s;s++)l[s]||f._settleItem(g[s],{sent:!1,ok:!1,status:0,reason:"batch_deduped_duplicate"});return Promise.resolve()}return ut(e,r,{useBeacon:rr}).then(function(e){var t,n,o,i,a,s=Promise.resolve();if(e&&e.ok){for(t=[],n=0;r.length>n;n++)(o=((r[n]&&r[n].event_id||"")+"").trim())&&t.push(o);Ft.set(t)}else{for(i=[],a=0;g.length>a;a++)l[a]&&i.push(g[a]);s=f.persistentQueue.enqueueMany(f._toPersistentEntries(i)).then(function(){f.hasPersistentItems=!0}).catch(function(){})}return s.then(function(){for(var t=0;g.length>t;t++)f._settleItem(g[t],l[t]?e:{sent:!1,ok:!1,status:0,reason:"batch_deduped_duplicate"})})})}if("unavailable"===f.resolver.getEndpointState())return f.persistentQueue.enqueueMany(f._toPersistentEntries(g)).then(function(){f.hasPersistentItems=!0;for(var e=0;g.length>e;e++)f._settleItem(g[e],{sent:!1,ok:!1,status:0,reason:"endpoint_unavailable"})}).catch(function(){});for(t=0;g.length>t;t++)n.push(g[t])})},Pt.prototype.flushOnce=function(){var e,t,n,r,o,i,a,s,c,u=this;if(this._purgeSettled(),!this.queue.length)return Promise.resolve();for(this._stopTimer(),e=this.queue.splice(0,10),t={},n=0;e.length>n;n++)(r=e[n]).settled||(t[o=r.endpointOverride||"__default__"]||(t[o]=[]),t[o].push(r));for(i=[],a=Object.keys(t),s=Promise.resolve(),c=0;a.length>c;c++)(function(e){s=s.then(function(){return u._processFlushGroup(e,t[e],i)})})(a[c]);return s.then(function(){var e,t,n,r,o,a,s;if(i.length){for(e=Date.now(),t=[],n=[],r=0;i.length>r;r++)(i[r].timeoutAt>e?n:t).push(i[r]);for(o=0;n.length>o;o++)u.queue.push(n[o]);for(a=Promise.resolve(),s=0;t.length>s;s++)(function(e){a=a.then(function(){return u.persistentQueue.enqueueMany(u._toPersistentEntries([e])).catch(function(){}).then(function(){u.hasPersistentItems=!0,u._settleItem(e,{sent:!1,ok:!1,status:0,reason:"track_timeout",transport:"queue"})})})})(t[s]);return a}}).then(function(){u.queue.length&&u.schedule()},function(e){throw u.queue.length&&u.schedule(),e})},Pt.prototype._processReplayGroup=function(e,t){var n=this;return n._resolveEndpoint?n._resolveEndpoint("__default__"===e?"":e).then(function(e){var r,o,i,a,s,c,u,l,d;if(e){for(r={},o=[],i=[],a=[],s=0;t.length>s;s++)u=(((c=t[s]).record&&c.record.event_id||"")+"").trim()||"__empty__:"+c.id,(l=((c.record&&c.record.event_id||"")+"").trim())&&Ft.has(l)||r[u]?a.push(c.id):(r[u]=!0,o.push(c.record),i.push(c.id));return d=Promise.resolve(),a.length&&(d=d.then(function(){return n.persistentQueue.removeByIds(a)})),o.length?d.then(function(){return ut(e,o,{useBeacon:rr})}).then(function(e){var r,s,c,u,l,d,_,p,f,h;if(e&&e.ok){for(r=[],s=0;o.length>s;s++)(c=((o[s]&&o[s].event_id||"")+"").trim())&&r.push(c);for(Ft.set(r),u=[],l=0;i.length>l;l++)u.push(i[l]);for(d=0;a.length>d;d++)u.push(a[d]);return n.persistentQueue.removeByIds(u)}for(_=0,p=0;t.length>p;p++)(f=t[p].retryCount||0)>_&&(_=f);return h=n._getRetryDelay(_),n.persistentQueue.updateRetryMeta(t,h,5)}):d}}):(Kt("[sdk][web] _resolveEndpoint 未注入,持久队列重试跳过"),Promise.resolve())},Pt.prototype.replayPersistent=function(){var e=this;return!this.hasPersistentItems||this.replaying?Promise.resolve():(this.replaying=!0,this.persistentQueue.readOldest(10).then(function(t){var n,r,o,i,a,s,c;if(!t.length)return e.persistentQueue.count().then(function(t){0===t&&(e.hasPersistentItems=!1)});for(n={},r=0;t.length>r;r++)n[i=(o=t[r]).endpointOverride||"__default__"]||(n[i]=[]),n[i].push(o);for(a=Object.keys(n),s=Promise.resolve(),c=0;a.length>c;c++)(function(t){s=s.then(function(){return e._processReplayGroup(t,n[t])})})(a[c]);return s}).catch(function(){}).then(function(){e.replaying=!1},function(){e.replaying=!1}))},Pt.prototype.enqueueAssembled=function(e,t){var n=this;return(n._getEventCodes?n._getEventCodes():Promise.resolve([])).then(function(r){return r&&0===r.length||r&&-1===r.indexOf(e.event)?{sent:!1,ok:!1,status:0,reason:"event_code_not_allowed",event:e.event}:new Promise(function(r){var o={record:e,endpointOverride:t||"",resolve:r,reject:r,settled:!1,timeoutAt:Date.now()+er,timeoutTimer:null};o.timeoutTimer=setTimeout(function(){o.settled||n.persistentQueue.enqueueMany(n._toPersistentEntries([o])).catch(function(){}).then(function(){n.hasPersistentItems=!0,n._settleItem(o,{sent:!1,ok:!1,status:0,reason:"track_timeout",transport:"queue"})})},er),n.queue.push(o),10>n.queue.length?n.schedule():n.flush().catch(function(){})})})},Pt.prototype.track=function(e,t){var n=this;return Q().then(function(r){var o={appId:n.opts.appId,channel:n.opts.channel,uid:n.opts.uid||"",deviceId:n.opts.deviceId||r,deviceType:y(),sdkVersion:$n,appVersion:v(n.opts.appVersion)};return(n._getSigningConfig?n._getSigningConfig():Promise.resolve({hmacKey:"",securityKeyVersion:""})).then(function(t){return $e(o,e,t)}).then(function(e){return delete e._sessionInfo,(n._getEventCodes?n._getEventCodes():Promise.resolve([])).then(function(r){return r&&0===r.length?(Kt("[sdk][web] 当前没有事件允许上报,请检查服务端配置(eventCodes 为空)"),{sent:!1,ok:!1,status:0,reason:"event_code_not_allowed",event:e.event}):r&&-1===r.indexOf(e.event)?(Kt('[sdk][web] 事件 "'+e.event+'" 不在服务端允许上报的列表中,已忽略'),{sent:!1,ok:!1,status:0,reason:"event_code_not_allowed",event:e.event}):new Promise(function(r,o){var i={record:e,endpointOverride:t||"",resolve:r,reject:o,settled:!1,timeoutAt:Date.now()+er,timeoutTimer:null};i.timeoutTimer=setTimeout(function(){i.settled||n.persistentQueue.enqueueMany(n._toPersistentEntries([i])).catch(function(){}).then(function(){n.hasPersistentItems=!0,n._settleItem(i,{sent:!1,ok:!1,status:0,reason:"track_timeout",transport:"queue"})})},er),n.queue.push(i),10>n.queue.length?n.schedule():n.flush().catch(function(){})})})})})},Pt.prototype.stop=function(){var e,t;this._stopTimer(),this.replayTimer&&(clearInterval(this.replayTimer),this.replayTimer=null),e={sent:!1,ok:!1,status:0,reason:"stopped"},t=this,this.queue.splice(0).forEach(function(n){t._settleItem(n,e)})},Pt.prototype.getConfig=function(){return{batchMaxSize:10,batchIntervalMs:5e3,replayBatchSize:10,replayIntervalMs:Zn}},Et.prototype._sdkOpts=function(){return{appId:this.options.appId,channel:this.options.channel,uid:this.options.uid||"",deviceId:this.options.deviceId||"",deviceType:y(),sdkVersion:$n,appVersion:v(this.options.appVersion)}},Et.prototype._isAutoDisabled=function(e){return!(!this._disableAutoEvents||!this._disableAutoEvents[e])},Et.prototype._handleEndedSession=function(t){var n,r,o,i,a,s,c;return t&&t.sid&&!this._ssrSkipped?this._isAutoDisabled("app_session_summary")?Promise.resolve():function(t){if(!t)return!1;if(Pn[t]||En[t])return!1;if(se())try{if(window.sessionStorage.getItem(bn+(t+"")))return Pn[t]=1,!1}catch(e){}return En[t]=1,!0}(t.sid)?(n=this,o=t.sid,a=(null!=t.sessionEndTs?1e12>t.sessionEndTs?1e3*t.sessionEndTs:t.sessionEndTs:Date.now())||Date.now(),s=fe(o),c=!Sn||Sn===o||!s&&(xn.eventCountTotal>0||xn.currentPage||(xn.pageStayList||[]).length>0),s&&Sn&&Sn!==o&&(c=!1),s&&c&&(s.eventCountTotal||0)>(xn.eventCountTotal||0)&&(c=!1),c?(xe(),i=ce(xn)):i=s?function(e,t){var n,r,o,i=e&&e.currentPage;return i?(n=t||Date.now(),r=i.accumulated_ms||0,null!=i.resume_at_ms&&(r+=Math.max(0,n-i.resume_at_ms)),o={page_key:i.page_key,page_name:i.page_name||"",stay_duration_ms:r,enter_time:Math.floor((i.enter_time||n)/1e3),has_interaction:i.has_interaction?1:0},i.page_seq_in_sid&&(o.page_seq_in_sid=i.page_seq_in_sid),e.pageStayList=be((e.pageStayList||[]).concat([o])),e.currentPage=null,e):e}(ce(s),a):{eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null},pe(o,{sid:o,savedAt:Date.now(),sessionStartedAt:Dn,behaviorHash:ne(),metrics:i}),r=i,Q().then(function(e){var o=bt(n._sdkOpts(),{deviceId:e});return n.configMgr.getSigningConfig().then(function(e){var n=Me(t,t.endReason,r);return it(o,n,e,t.sid)})}).then(function(e){return e}).then(function(e){return delete e._sessionInfo,n.batcher.enqueueAssembled(e,"")}).then(function(){var n,r;!function(t){if(t&&(delete En[t],Pn[t]=1,se()))try{window.sessionStorage.setItem(bn+(t+""),"1")}catch(e){}}(t.sid),ke(t.sid),n={pageKey:Ln||"",pageName:Rn||""},r=function(e){if(!e||null==e.sessionStartTs)return Date.now();var t=e.sessionStartTs;return 1e12>t?1e3*t:t}(F()),function(t,n){var r,o;t&&function(t){if(t){if(se())try{window.sessionStorage.removeItem(de(t))}catch(e){}if(ye())try{window.localStorage.removeItem(de(t))}catch(e){}}}(t),xn={eventCountTotal:0,businessEventCount:0,pageViewCount:0,pageClickCount:0,networkChangeCount:0,backgroundCount:0,pageKeys:{},pageStayList:[],pageSeqCounter:0,currentPageSeq:0,lastEventName:"",scrollTotalCount:0,scrollTotalDistance:0,scrollUpCount:0,scrollDownCount:0,scrollDirectionChangeCount:0,lastScrollDirection:0,firstScrollDelayMs:-1,hasScrolled:!1,maxScrollDepthPercent:0,isReachBottom:0,keydownTotalCount:0,inputCount:0,focusTotalCount:0,firstInteractionDelayMs:-1,firstInteractionRecorded:!1,lastOrientation:"unknown",touchTotalCount:0,swipeTotalCount:0,longPressTotalCount:0,pinchTotalCount:0,currentPage:null},Dn=r=n&&"number"==typeof n.sessionStartMs&&isFinite(n.sessionStartMs)?n.sessionStartMs:Date.now(),xn.lastOrientation=re(),mn.reset(),Cn=!0,Sn=z()||"",(o=n&&n.carryOver)&&o.pageKey?function(t,n,r){var o,i,a,s,c=((t||"")+"").trim();if(!c)return!1;o="number"==typeof r&&isFinite(r)?r:Date.now(),i=Date.now(),a=!1;try{a="undefined"!=typeof document&&"hidden"===document.visibilityState}catch(e){}xn.pageKeys[c]=1,s=ae(),xn.currentPage={page_key:c,page_name:(n||"")+"",enter_time:o,enter_time_ms:o,page_seq_in_sid:s,has_interaction:!1,accumulated_ms:0,resume_at_ms:a?null:i,hidden_paused:a},me()}(o.pageKey,o.pageName,r):me()}(t.sid,{sessionStartMs:r,carryOver:n.pageKey?{pageKey:n.pageKey,pageName:n.pageName}:null})}).catch(function(){var e;(e=t.sid)&&delete En[e],function(e){if(e&&e.sid){var t=ve().filter(function(t){return t&&t.sid!==e.sid});t.push(e),we(t)}}({sid:t.sid,sessionStartTs:t.sessionStartTs,sessionEndTs:t.sessionEndTs,endReason:t.endReason,fields:Me(t,t.endReason,r)})})):Promise.resolve():Promise.resolve()},Et.prototype._replayPendingSummaries=function(e,t,n){var r,o,i=ve();if(!i.length)return Promise.resolve();for(r=Promise.resolve(),o=0;i.length>o;o++)(function(o){r=r.then(function(){return n(it(e,o.fields,t,o.sid)).then(function(){return ke(o.sid),null}).catch(function(){})})})(i[o]);return r},Et.prototype._startAutoEvents=function(){if(!this._ssrSkipped&&!this._autoEventsStarted){var t=this;this._autoEventsStarted=!0,qn=function(e){t._handleEndedSession(e).catch(function(){})},Z().then(function(){return Q()}).then(function(n){var r=bt(t._sdkOpts(),{deviceId:n});return t.configMgr.getSigningConfig().then(function(n){return t.configMgr.fetchAndDecryptConfig().then(function(o){function i(e){return!s||-1!==s.indexOf(e)}function a(e){return i(e)&&!t._isAutoDisabled(e)}var s=o.eventCodes&&o.eventCodes.length>0?o.eventCodes:null,c=function(e){return e.then(function(e){return delete e._sessionInfo,t.batcher.enqueueAssembled(e,"")})};return t._replayPendingSummaries(r,n,c).then(function(){function o(e){if(!s&&(s=!0,a("app_session_summary"))){var n=function(e){var t,n,r=Date.now()||Date.now(),o=F();return o?(t=Y(o,e||"web_visibility_hidden",r),"web_visibility_hidden"!==e&&"app_exit"!==e||(t.sessionEndTs=r),N(n=H(r)),{endedSession:t,sid:n.sid}):null}(e);n&&n.endedSession&&t._handleEndedSession(n.endedSession).catch(function(){})}}var s,u,l,d,_,p,f,h,g,m,y,v,w,k,b;a("sdk_init")&&c(function(e,t){var n,r=fn||{},o={},i=Object.keys(r);for(n=0;i.length>n;n++)o[i[n]]=r[i[n]];return $e(e,{event:"sdk_init",payload:o},t)}(r,n)).catch(function(){}),a("app_lifecycle")&&(t._webLoadTeardown=function(t,n,r){function o(){a||(a=!0,s&&(window.removeEventListener("load",s,!1),s=null),t(ot(n,Kn,{},r)).catch(function(){}))}function i(){(function(){var t,n,r;try{if("undefined"!=typeof performance&&performance.timing&&(t=performance.timing).loadEventEnd&&t.navigationStart&&t.loadEventEnd>0)return!0;if("undefined"!=typeof performance&&"function"==typeof performance.getEntriesByType&&(n=performance.getEntriesByType("navigation"))&&n.length>0&&(r=n[0]).loadEventEnd>0&&r.fetchStart>=0)return!0}catch(e){}return!1})()&&o()}var a,s;return"undefined"==typeof window?(t(ot(n,Kn,{},r)).catch(function(){}),function(){}):(a=!1,s=null,"complete"===document.readyState?(i(),a||o(),function(){}):(s=function(){i(),a||o()},window.addEventListener("load",s,!1),setTimeout(function(){a||o()},1e4),function(){a=!0,s&&(window.removeEventListener("load",s,!1),s=null)}))}(c,r,n)),s=!1,t._autoLifecycleTeardown=function(e,t){var n,r,o,i,a;return"undefined"==typeof window||"undefined"==typeof document?function(){}:(r=!0===(n=t||{}).webMode,o=function(){"visible"===document.visibilityState?r?n.onVisible&&n.onVisible():e(Vn,{}):"hidden"===document.visibilityState&&(r?n.onVisibleHidden&&n.onVisibleHidden():e(jn,{}),n.onHidden&&n.onHidden())},i=function(){e(Nn,rt()),n.onPageUnload&&n.onPageUnload()},a=function(e){e&&e.persisted||n.onPageHide&&n.onPageHide()},window.addEventListener("visibilitychange",o,!1),window.addEventListener("beforeunload",i,!1),window.addEventListener("pagehide",a,!1),function(){window.removeEventListener("visibilitychange",o,!1),window.removeEventListener("beforeunload",i,!1),window.removeEventListener("pagehide",a,!1)})}(function(e,t){return a("app_lifecycle")?c(ot(r,e,t||{},n)):Promise.resolve()},{onHidden:function(){t.batcher.flush().catch(function(){})},onPageUnload:function(){t.batcher.flush().catch(function(){}),o("app_exit")},onPageHide:function(){t.batcher.flush().catch(function(){}),o("web_visibility_hidden")}}),t._autoNetworkTeardown=(l=function(e){return a("network_state_change")?c(function(e,t,n){return $e(e,{event:"network_state_change",payload:t||{}},n)}(r,e,n)):Promise.resolve()},"undefined"==typeof navigator?function(){}:(d=navigator.connection||navigator.mozConnection||navigator.webkitConnection)?(_="unknown",p=d.type?d.type+"":"",f="boolean"==typeof navigator.onLine?navigator.onLine:null,d.onchange=function(){var e,t,n,r,o,i,a=d.type?d.type+"":"",s="boolean"==typeof navigator.onLine?navigator.onLine:null;(a!==p||s!==f)&&(p=a,f=s,r="boolean"==typeof(t=s)?t?1:0:-1,"none"==(n=(a||"")+"")&&1===r?n="unknown":n&&"none"!==n&&"unknown"!==n&&0===r&&(r=1),l(at({network_type:_,from_network_type:_,to_network_type:_,is_online:(e={connection_type:n,is_online:r}).is_online,change_count_in_sid:(xn.networkChangeCount||0)+1},(i={},(o=e.connection_type)?i.connection_type=o+"":d.type&&(i.connection_type=d.type+""),d.effectiveType&&(i.effective_type=d.effectiveType+""),null!=d.downlink&&(i.downlink=Math.round(1e3*d.downlink)),null!=d.rtt&&(i.rtt=d.rtt),i))))},function(){d.onchange=null}):function(){}),t._webInteractionTeardown="undefined"==typeof window||"undefined"==typeof document?function(){}:(h=function(e){!e||e.repeat||e.isComposing||(Se(),he(),xn.keydownTotalCount++,me())},g=function(e){var t,n=e&&e.target;(function(t){if(!t||1!==t.nodeType)return!1;var n=((t.tagName||"")+"").toLowerCase();if("input"===n||"textarea"===n)return!0;try{if(t.isContentEditable)return!0}catch(e){}return!1})(n)&&(t=Date.now(),Fn===n&&50>t-Hn||(Fn=n,Hn=t,Se(),he(),xn.inputCount++,me()))},m=function(e){(function(t){if(!t||1!==t.nodeType)return!1;var n=((t.tagName||"")+"").toLowerCase();if("input"===n||"textarea"===n||"select"===n)return!0;try{if(t.isContentEditable)return!0}catch(e){}return!1})(e&&e.target)&&(Se(),he(),xn.focusTotalCount++,me())},y=function(){Pe()},v=function(e){e&&e.changedTouches&&e.changedTouches.length&&(Se(),he(),xn.touchTotalCount++,me(),e.touches&&1===e.touches.length?(Un=e.touches[0].clientX,Xn=e.touches[0].clientY,Yn=Date.now(),ct(),zn=!1,Gn=setTimeout(function(){zn=!0,he(),xn.longPressTotalCount++,me()},500)):(Yn=0,ct()),e.touches&&2===e.touches.length&&(Wn=st(e.touches),Jn=!1))},w=function(e){var t,n,r;e&&e.touches&&(1===e.touches.length&&Yn>0&&Math.sqrt((t=e.touches[0].clientX-Un)*t+(n=e.touches[0].clientY-Xn)*n)>8&&ct(),2===e.touches.length&&(r=st(e.touches),null==Wn||Jn||12>Math.abs(r-Wn)||(he(),xn.pinchTotalCount++,me(),Jn=!0),Wn=r))},k=function(e){var t,n;ct(),Yn>0&&e.changedTouches&&e.changedTouches.length&&!zn&&(30>Math.sqrt((t=e.changedTouches[0].clientX-Un)*t+(n=e.changedTouches[0].clientY-Xn)*n)||(he(),xn.swipeTotalCount++,me())),Yn=0,zn=!1,e.touches&&e.touches.length>=2||(Wn=null,Jn=!1)},b=function(){ct(),Yn=0,zn=!1,Wn=null,Jn=!1},document.addEventListener("keydown",h,!1),document.addEventListener("input",g,!1),document.addEventListener("focusin",m,!1),window.addEventListener("orientationchange",y,!1),document.addEventListener("touchstart",v,!1),document.addEventListener("touchmove",w,!1),document.addEventListener("touchend",k,!1),document.addEventListener("touchcancel",b,!1),Pe(),function(){document.removeEventListener("keydown",h,!1),document.removeEventListener("input",g,!1),document.removeEventListener("focusin",m,!1),window.removeEventListener("orientationchange",y,!1),document.removeEventListener("touchstart",v,!1),document.removeEventListener("touchmove",w,!1),document.removeEventListener("touchend",k,!1),document.removeEventListener("touchcancel",b,!1),ct()}),(i("app_page_view")||i("page_click"))&&(u=a("app_page_view"),t._pageTrackerTeardown=function(t){function n(){if(u.resolvePageKey)try{var t=u.resolvePageKey();if(t)return qe(t+"")}catch(e){}return Be()}function r(){var e,t=n(),r=ze({previousPageKey:d,previousPageName:_,pageKey:t});d=t,_=r.page_name,Ln=t,Rn=r.page_name||"",l&&(e=ae(),function(e,t,n,r){he(),xn.eventCountTotal++,xn.lastEventName="app_page_view",xn.pageViewCount++;var o=(e||"")+"";o&&(xn.pageKeys[o]=1),Te(o,t,n,r),me()}(t,r.page_name,Math.floor(Date.now()/1e3),e),r._pageSeqPrealloc=e,r._metricsRecorded=1,u.onPageView&&u.onPageView(r))}function o(){var t,n,r,o,i=Date.now();200>i-Mn||(Mn=i,(t=Ke())&&(r=(n=t.scrollTop||0)-In,In=n,o=function(){var t,n,r;try{return(t=Ke())?(n=Math.max(0,(t.scrollHeight||0)-(t.clientHeight||0)))>0?{depthPercent:Math.min(100,Math.max(0,Math.round((r=t.scrollTop||0)/n*100))),atBottom:r>=n-8}:{depthPercent:0,atBottom:!0}:{depthPercent:0,atBottom:!1}}catch(e){return{depthPercent:0,atBottom:!1}}}(),u.onScroll&&u.onScroll(r,o.depthPercent,o.atBottom)))}function i(e){var t,r,o,i;e&&0>=e.button&&((t=e.target)&&"function"==typeof t.closest&&t.closest("[data-sdk-no-auto-click]")||(r=null!=e.clientX?e.clientX:0,o=null!=e.clientY?e.clientY:0,i=Ye(e),setTimeout(function(){if(!function(e,t){var n,r;for(function(e){for(var t=An.length-1;t>=0;t--)e-An[t].t>300&&An.splice(t,1)}(Date.now()),n=0;An.length>n;n++)if(10>=Math.abs((r=An[n]).x-e)&&10>=Math.abs(r.y-t))return!0;return!1}(r,o)){Se();var e=function(e,t){var n=e||{},r=qe(n.pageKey||Be()),o=Ye(t);return Ae({page_key:r,page_name:n.pageName||"undefined"!=typeof document&&document.title||r,recommend_trace_id:"",referrer_page_key:n.previousPageKey||"",is_sdk_auto_collected:1,_clickSnap:o},Ge(o))}({pageKey:d||n(),pageName:_,previousPageKey:d},i);u.onPageClick&&u.onPageClick(e)}},50)))}function a(){Ie(),r()}function s(e){e&&e.persisted&&r()}function c(){var e,t,n=Ke();In=n&&n.scrollTop||0,r(),!p&&window.history&&(p=!0,e=history.pushState,t=history.replaceState,history.pushState=function(){var t=e.apply(history,arguments);return Ie(),setTimeout(r,0),t},history.replaceState=function(){var e=t.apply(history,arguments);return Ie(),setTimeout(r,0),e},window.addEventListener("popstate",a,!1))}var u,l,d,_,p;return"undefined"==typeof window||"undefined"==typeof document?function(){}:(l=!1!==(u=t||{}).enableAutoPageView,d="",_="",p=!1,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c,!1):c(),window.addEventListener("pageshow",s,!1),window.addEventListener("scroll",o,!1),document.addEventListener("click",i,!0),function(){document.removeEventListener("click",i,!0),window.removeEventListener("scroll",o,!1),window.removeEventListener("pageshow",s,!1),window.removeEventListener("popstate",a,!1),document.removeEventListener("DOMContentLoaded",c,!1)})}({resolvePageKey:t.options.pageKeyResolver,enableAutoPageView:u,onPageView:function(e){u&&t.batcher.track({event:"app_page_view",payload:e},"").catch(function(){})},onPageClick:function(e){a("page_click")&&t.batcher.track({event:"page_click",payload:e},"").catch(function(){})},onScroll:function(e,t,n){!function(e,t,n){var r,o;he(),8>(r=Math.abs(e))||(xn.hasScrolled||(xn.hasScrolled=!0,xn.firstScrollDelayMs=Math.max(0,Date.now()-Dn)),xn.scrollTotalCount++,xn.scrollTotalDistance+=r,o=e>0?1:-1,0!==xn.lastScrollDirection&&o!==xn.lastScrollDirection&&xn.scrollDirectionChangeCount++,xn.lastScrollDirection=o,0>e?xn.scrollUpCount++:e>0&&xn.scrollDownCount++,null!=t&&(xn.maxScrollDepthPercent=Math.max(xn.maxScrollDepthPercent,Math.min(100,Math.max(0,Math.round(t))))),n&&(xn.isReachBottom=1),me())}(e,t,n)}})),t._sessionPollTimer&&clearInterval(t._sessionPollTimer),t._sessionPollTimer=setInterval(function(){var e=G();e.endedSession&&t._handleEndedSession(e.endedSession).catch(function(){})},6e4)})})})}).catch(function(){})}},Et.prototype._init=function(e){var t,n,r;if(this._ssrSkipped)return this._buildInitReturn();for(t=Object.keys(e||{}),n=0;t.length>n;n++)this.options[t[n]]=e[t[n]];return null==this.options.channel&&(this.options.channel=""),this._disableAutoEvents=Ct(this.options.disableAutoEvents),this.options.disableAutoEvents=St(this._disableAutoEvents),this.configMgr.setEncryptedConfig(this.options.encryptedConfig||this.options.configCipher||""),this.configMgr.prewarm(),r=this,this.resolver.init(function(){return r.configMgr.getBatchEndpoints()}),this._refreshTimer&&clearInterval(this._refreshTimer),this._refreshTimer=setInterval(function(){r.refreshConfig()},12e5),this._unsubReady&&this._unsubReady(),this._unsubReady=this.resolver.onEndpointReady(function(){r.batcher.flush().catch(function(){})}),this._buildInitReturn()},Et.prototype.refreshConfig=function(){var e=this;return this.configMgr.forceRefresh(),this.resolver.refreshDomainConfig(function(){return e.configMgr.getBatchEndpoints()}).catch(function(){})},Et.prototype.track=function(e,t){return this._ssrSkipped?Promise.resolve({sent:!1,ok:!0,status:0,reason:"ssr_skipped"}):this.batcher.track(e,t)},Et.prototype.setContext=function(e){if(this._ssrSkipped)return this.getConfig();var t=e&&"object"==typeof e?e:{};return Object.prototype.hasOwnProperty.call(t,"uid")&&(this.options.uid=null==t.uid?"":t.uid+""),Object.prototype.hasOwnProperty.call(t,"channel")&&(this.options.channel=null==t.channel?"":t.channel+""),this.batcher.opts=bt(this.options),this.getConfig()},Et.prototype.setUid=function(e){return this.setContext({uid:e})},Et.prototype.getSelectedHost=function(){var e,t;return this._ssrSkipped||!this.resolver?"":(e=this.resolver.getSelectedBatchEndpoint())&&(t=/^(https?:\/\/[^/]+)/.exec(e))?t[1]:""},Et.prototype._buildInitReturn=function(){return this._ssrSkipped||!this.batcher?{selectedHost:"",version:$n}:bt(this.batcher.getConfig(),{selectedHost:this.getSelectedHost(),version:$n})},Et.prototype.getConfig=function(){if(this._ssrSkipped||!this.batcher)return bt(this.options,{selectedHost:"",selectedBatchEndpoint:"",version:$n});var e=bt(this.options,this.batcher.getConfig(),{selectedHost:this.getSelectedHost(),selectedBatchEndpoint:this.resolver.getSelectedBatchEndpoint(),version:$n});return delete e.encryptedConfig,delete e.configCipher,e},Et.prototype.flush=function(){return this._ssrSkipped?Promise.resolve():this.batcher.flush()},Et.prototype.refreshDomainConfig=function(){if(this._ssrSkipped)return Promise.resolve({selectedBatchEndpoint:"",selectedHost:"",version:$n});var e=this;return this.resolver.refreshDomainConfig(function(){return e.configMgr.getBatchEndpoints()}).then(function(t){return{selectedBatchEndpoint:t,selectedHost:e.getSelectedHost(),version:$n}})},Et.prototype.stop=function(){this._ssrSkipped||(this._sessionPollTimer&&(clearInterval(this._sessionPollTimer),this._sessionPollTimer=null),this._pageTrackerTeardown&&(this._pageTrackerTeardown(),this._pageTrackerTeardown=null),this._webInteractionTeardown&&(this._webInteractionTeardown(),this._webInteractionTeardown=null),this._webLoadTeardown&&(this._webLoadTeardown(),this._webLoadTeardown=null),this._autoLifecycleTeardown&&(this._autoLifecycleTeardown(),this._autoLifecycleTeardown=null),this._autoNetworkTeardown&&(this._autoNetworkTeardown(),this._autoNetworkTeardown=null),this._unsubReady&&(this._unsubReady(),this._unsubReady=null),this._refreshTimer&&(clearInterval(this._refreshTimer),this._refreshTimer=null),this.batcher.stop())},Ht=null,Ut=[],Xt=null,(Yt=Dt).setContext=function(e){return Tt().setContext(e)},Yt.setUid=function(e){return Tt().setUid(e)},Yt.setChannel=function(e){return Tt().setContext({channel:e})},Yt.notifyPageLoad=function(){!function(){if(cn=null,V())try{window.sessionStorage.removeItem(tn),window.sessionStorage.removeItem(nn)}catch(e){}}(),G(Date.now())},Yt.debugInvalidateSessionCache=function(){W()},Yt.debugForceEnsureSession=function(){var e,t;return W(),e=G(),t=Tt(),e.endedSession&&t._handleEndedSession(e.endedSession).catch(function(){}),e},Yt.getConfig=function(){return Tt().getConfig()},Yt.flush=function(){return Tt().flush()},Yt.refreshDomainConfig=function(){return Tt().refreshDomainConfig()},Yt.stop=function(){Ht?(Ht.stop(),Ht=null):Ut.length=0},Yt.version=$n,Yt.getCommonFields=function(e){var t=Ht?Ht.getConfig():{};return Z().then(function(){return Q().then(function(n){var r,o,i,a,s=B({appId:t.appId,channel:t.channel,uid:t.uid,deviceId:t.deviceId||n,sid:(a=z(),a||G().sid),deviceType:y(),deviceBrand:"",deviceModel:"",deviceFingerprint:ee(),fpVersion:te(),sdkVersion:$n,appVersion:v(t.appVersion)});if(!Array.isArray(e)||!e.length)return s;for(r={},o=0;e.length>o;o++)(i=((e[o]||"")+"").trim())&&(r[i]=Object.prototype.hasOwnProperty.call(s,i)?s[i]:"");return r})})},Yt.getCommonField=function(e){var t=((e||"")+"").trim();return t?Yt.getCommonFields([t]).then(function(e){return e[t]}):Promise.resolve("")},Yt.init=function(e){var t,n,r,o;return Ht&&(t=Ht,Ht=null,t.flush().catch(function(){}).then(function(){t.stop()})),n=new Et(e),Ht=n,o=xt(Ut.splice(0),n,0).then(function(){Xt===r&&(Xt=null)},function(){Xt===r&&(Xt=null)}),Xt=r=o,bt(n._buildInitReturn(),{track:Yt.track,setContext:Yt.setContext,setUid:Yt.setUid,setChannel:Yt.setChannel,notifyPageLoad:Yt.notifyPageLoad,getCommonFields:Yt.getCommonFields,getCommonField:Yt.getCommonField})},Gt=function(){return Kt("[sdk][web] 非法上报:禁止直接调用 WebSDK.track({...}),请使用 WebSDK.track.videoEvent({...}) 等具名方法"),Promise.resolve({sent:!1,ok:!1,status:0,reason:"direct_track_forbidden"})},Object.keys(Bt).forEach(function(t){var n=Bt[t];Gt[t]=function(r,o){try{return Dt(n(r),o)}catch(e){if(e&&"EventValidationError"===e.name)return Promise.resolve({sent:!1,ok:!1,status:0,reason:"event_validation_failed",event:e.eventMethod||t,missingFields:e.missingFields||[],typeErrorFields:e.typeErrorFields||[]});throw e}}}),Yt.track="undefined"!=typeof Proxy?new Proxy(Gt,{get:function(e,t){return"string"!=typeof t||t in e||!/^[a-z]/.test(t)?e[t]:function(){return Kt('[sdk][web] 未知上报事件 "'+t+'",该方法不存在,事件未上报'),Promise.resolve({sent:!1,ok:!1,status:0,reason:"unknown_event_method",method:t})}}}):Gt,"undefined"!=typeof globalThis)try{globalThis.WebSDK=Yt}catch(e){}else"undefined"!=typeof window&&(window.WebSDK=Yt);return Yt}); diff --git a/app/static/css/account-empty.css b/app/static/css/account-empty.css deleted file mode 100644 index 10834e5..0000000 --- a/app/static/css/account-empty.css +++ /dev/null @@ -1,72 +0,0 @@ -/* 与「我的收藏」一致的账户列表空状态。 */ -/* 空态时仍保持与个人中心同宽的主栏,避免筛选条贴到视口左缘。 */ - -.account-empty { - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - width: 100%; - padding: 96px 16px; - font-size: 14px; - line-height: 22px; - text-align: center; -} - -.account-empty[hidden] { display: none !important; } - -.account-empty__icon { - display: flex; - align-items: center; - justify-content: center; - width: 96px; - height: 96px; - flex: 0 0 96px; - border: 1px solid var(--line); - border-radius: 50%; - background: #1b1b1f; -} - -.account-empty__icon img { width: 40px; height: 40px; } - -.account-empty__title { - margin: 0; - color: var(--text); - font-size: 20px; - font-weight: 600; - line-height: 28px; -} - -.account-empty__description { - margin: 0; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.account-empty__description-mobile { display: none; } - -.account-empty__action { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 147px; - height: 48px; - border-radius: 999px; - background: linear-gradient(105deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - color: #fff; - font-size: 14px; - font-weight: 600; - line-height: 20px; - white-space: nowrap; -} - -.account-empty__action img { width: 20px; height: 20px; } -.account-empty__action:focus-visible { outline: 2px solid var(--accent-hot); outline-offset: 3px; } - -@media (max-width: 768px) { - .account-empty { padding: 48px 16px; } - .account-empty__description-desktop { display: none; } - .account-empty__description-mobile { display: inline; } -} diff --git a/app/static/css/base.css b/app/static/css/base.css index b4757b8..7a416ce 100644 --- a/app/static/css/base.css +++ b/app/static/css/base.css @@ -770,3 +770,35 @@ img { white-space: nowrap !important; border: 0 !important; } + +/* 卡片信息行:集数/状态前置为金色小标,标签随后(首页、列表、排行榜共用) */ +.card__meta--ep { + display: flex; + align-items: center; + gap: 6px; +} + +.card__meta-ep { + flex-shrink: 0; + padding: 0 6px; + border-radius: 4px; + color: var(--gold); + font-size: 12px; + line-height: 18px; + background: rgba(233, 201, 119, 0.12); +} + +.card__meta-tags { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 768px) { + .card__meta-ep { + padding: 0 4px; + font-size: 10px; + line-height: 16px; + } +} diff --git a/app/static/css/favorites-mobile.css b/app/static/css/favorites-mobile.css deleted file mode 100644 index d262fe8..0000000 --- a/app/static/css/favorites-mobile.css +++ /dev/null @@ -1,266 +0,0 @@ -/* 我的收藏 H5:以 Figma 375px 画板为基准。 */ -@media (max-width: 768px) { - .favorites-main { - display: flex; - flex-direction: column; - gap: 12px; - width: 100%; - max-width: none; - min-width: 0; - margin: 0; - padding: 12px var(--profile-gutter, 16px) 20px; - } - - .favorites-main.is-empty { - width: 100%; - gap: 0; - padding: 12px 0 20px; - } - - .favorites-main.is-empty .favorites-toolbar { - width: 100%; - padding-inline: var(--profile-gutter, 16px); - } - - .favorites-main.is-empty .favorites-recommendations { - width: auto; - margin-top: 8px; - padding: 0 var(--profile-gutter, 16px) 24px; - } - - .favorites-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - width: 100%; - min-width: 0; - min-height: 30px; - } - - .favorites-filters { - display: flex; - flex: 1 1 auto; - flex-wrap: nowrap; - align-items: center; - gap: clamp(6px, 2.666667vw, 10px); - min-width: 0; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - } - - .favorites-filters::-webkit-scrollbar { - display: none; - } - - .favorites-filter { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - height: 30px; - padding: 0 clamp(11px, 4.266667vw, 16px); - border: 1px solid rgba(255, 255, 255, .12); - border-radius: 999px; - background: transparent; - color: #a1a1aa; - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; - } - - .favorites-filter.is-active { - border-color: rgba(236, 35, 127, .3); - background: rgba(236, 35, 127, .1); - color: #ff4a7b; - } - - .favorites-manage { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - height: 30px; - padding: 0; - border: 0; - background: none; - color: #a1a1aa; - font-size: 12px; - font-weight: 400; - line-height: 18px; - white-space: nowrap; - } - - .favorites-manage svg, - .favorites-manage img { - display: none; - } - - .favorites-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px var(--profile-card-gap, 8px); - width: 100%; - min-width: 0; - } - - .favorites-card { - position: relative; - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; - } - - .favorites-card .profile-card__cover { - width: 100%; - aspect-ratio: 109 / 145; - border-radius: 8px; - } - - .favorites-card .profile-card__title { - overflow: hidden; - font-size: 14px; - font-weight: 600; - line-height: 20px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .favorites-card .profile-card__meta { - overflow: hidden; - font-size: 11px; - line-height: 16px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .favorites-card__desktop-meta { - display: none; - } - - .favorites-card__mobile-meta { - display: inline; - } - - .favorites-card__remove { - display: none; - } - - .favorites-page.is-managing .favorites-card__remove { - position: absolute; - top: 6px; - left: 6px; - z-index: 3; - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - padding: 0; - border: 1px solid rgba(255, 255, 255, .5); - border-radius: 50%; - background: rgba(11, 11, 13, .8); - color: #f2f2f4; - } - - .favorites-card .profile-card__episodes { - bottom: 10px; - left: 10px; - font-size: 11px; - line-height: 14px; - } - - .favorites-card .profile-card__badge { - top: 4px; - right: 4px; - min-width: 30px; - height: 20px; - padding: 0 5px; - } - - .favorites-load-more { - display: inline-flex; - align-items: center; - justify-content: center; - width: 100%; - height: 40px; - padding: 0 16px; - border: 1px solid rgba(255, 255, 255, .24); - border-radius: 999px; - background: transparent; - color: #f2f2f4; - font-size: 14px; - font-weight: 500; - line-height: 20px; - } - - .favorites-pagination { - display: none; - } - - .favorites-empty { - gap: 16px; - padding: 48px 16px; - } - - .favorites-empty__description-desktop, - .favorites-recommendations__more-desktop { - display: none; - } - - .favorites-empty__description-mobile, - .favorites-recommendations__more-mobile { - display: inline; - } - - .favorites-recommendations { - width: 100%; - padding: 0 var(--profile-gutter, 16px) 20px; - } - - .favorites-recommendations__head { - height: 26px; - margin-bottom: 14px; - } - - .favorites-recommendations__head h2 { - font-size: 18px; - line-height: 26px; - } - - .favorites-recommendations__head a { - font-size: 12px; - line-height: 18px; - } - - .favorites-recommendations__head a img { - width: 14px; - height: 14px; - } - - .favorites-recommendations__grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px var(--profile-card-gap, 8px); - } - - .favorites-card[hidden], - .favorites-empty[hidden], - .favorites-load-more[hidden] { - display: none !important; - } -} - -@media (max-width: 359px) { - .favorites-filters { - flex-wrap: nowrap; - gap: 6px; - } - - .favorites-filter { - padding-inline: 8px; - font-size: 12px; - } -} diff --git a/app/static/css/favorites.css b/app/static/css/favorites.css deleted file mode 100644 index c2230ab..0000000 --- a/app/static/css/favorites.css +++ /dev/null @@ -1,352 +0,0 @@ -/* 我的收藏:1440px 桌面稿,公共个人页头与封面卡复用 profile.css。 */ -.favorites-page [hidden] { - display: none !important; -} - -.favorites-main { - display: flex; - flex-direction: column; - gap: 24px; - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; - padding: 24px 0 40px; -} - -.favorites-main.is-empty { - width: 100%; - max-width: none; - gap: 24px; - margin: 0; - padding: 24px 0 40px; -} - -.favorites-main.is-empty .favorites-toolbar { - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; -} - -.favorites-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-height: 36px; -} - -.favorites-filters { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; - min-width: 0; -} - -.favorites-filter, -.favorites-manage { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - height: 36px; - border: 1px solid var(--line); - border-radius: 999px; - background: transparent; - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; -} - -.favorites-filter { - padding-inline: 16px; - color: var(--text-muted); -} - -.favorites-filter.is-active { - border-color: rgba(236, 35, 127, .3); - background: rgba(236, 35, 127, .1); - color: var(--accent-hot); -} - -.favorites-manage { - gap: 6px; - padding-inline: 12px; - border-color: rgba(255, 255, 255, .24); - color: var(--text); - cursor: pointer; -} - -.favorites-manage img, -.favorites-manage svg { - display: block; - width: 16px; - height: 16px; - flex: 0 0 16px; -} - -.favorites-filter:hover, -.favorites-manage:hover { - border-color: rgba(255, 255, 255, .36); - color: var(--text); -} - -.favorites-filter:focus-visible, -.favorites-manage:focus-visible, -.favorites-pagination button:focus-visible { - outline: 2px solid var(--accent-hot); - outline-offset: 3px; -} - -.favorites-grid { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 32px 24px; - min-width: 0; -} - -.favorites-grid .favorites-card { - min-width: 0; -} - -.favorites-card__mobile-meta, -.favorites-card__remove { - display: none; -} - -.favorites-card__media { - position: relative; - min-width: 0; -} - -.favorites-page.is-managing .favorites-card__remove { - position: absolute; - top: 6px; - left: 6px; - z-index: 3; - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border: 1px solid rgba(255, 255, 255, .4); - border-radius: 50%; - background: rgba(11, 11, 13, .78); - color: #fff; - font-size: 18px; - line-height: 1; -} - -.favorites-card__remove img { - display: block; - width: 14px; - height: 14px; -} - -.favorites-page.is-managing .favorites-manage { - border-color: rgba(236, 35, 127, .3); - background: rgba(236, 35, 127, .1); - color: var(--accent-hot); -} - -.favorites-load-more { - display: none; -} - -.favorites-pagination, -.favorites-pagination__pages { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; -} - -.favorites-pagination { - min-height: 36px; -} - -.favorites-pagination button { - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 36px; - width: 36px; - height: 36px; - border: 1px solid var(--line-soft); - border-radius: 8px; - background: var(--bg-elevated); - color: var(--text-muted); - font-family: Inter, var(--font); - font-size: 16px; - font-weight: 600; - line-height: 20px; -} - -.favorites-pagination button.is-active { - border-color: transparent; - background: linear-gradient(135deg, #ff4a7b 15%, #ec237f 51%, #93226f 85%); - color: #fff; -} - -.favorites-pagination button:not(:disabled):not(.is-active):hover { - border-color: rgba(255, 255, 255, .24); - color: var(--text); -} - -.favorites-pagination button:disabled { - cursor: default; - opacity: .45; -} - -.favorites-pagination button img, -.favorites-pagination button svg { - display: block; - width: 16px; - height: 16px; -} - -.favorites-pagination > .favorites-pagination__button:first-child img { - transform: rotate(180deg); -} - -.favorites-pagination__total { - color: var(--text-dim); - font-size: 12px; - line-height: 18px; - white-space: nowrap; -} - -.favorites-empty { - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - width: 100%; - padding: 96px 16px; - font-size: 14px; - line-height: 22px; - text-align: center; -} - -.favorites-main.is-empty:has(.favorites-recommendations) .favorites-empty { - padding-bottom: 40px; -} - -.favorites-main.is-empty .favorites-recommendations { - padding-top: 8px; -} - -.favorites-filter-empty { - margin: 0; - padding: 56px 16px; - color: var(--text-dim); - font-size: 14px; - line-height: 22px; - text-align: center; -} - -.favorites-empty__icon { - display: flex; - align-items: center; - justify-content: center; - width: 96px; - height: 96px; - flex: 0 0 96px; - border: 1px solid var(--line); - border-radius: 50%; - background: #1b1b1f; -} - -.favorites-empty__icon img { - width: 40px; - height: 40px; -} - -.favorites-empty__title { - margin: 0; - color: var(--text); - font-size: 20px; - font-weight: 600; - line-height: 28px; -} - -.favorites-empty__description { - margin: 0; - color: var(--text-muted); - font-size: 14px; - font-weight: 400; - line-height: 22px; -} - -.favorites-empty__description-mobile, -.favorites-recommendations__more-mobile { - display: none; -} - -.favorites-empty__action { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 147px; - height: 48px; - border-radius: 999px; - background: linear-gradient(105deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - color: #fff; - font-size: 14px; - font-weight: 600; - line-height: 20px; - white-space: nowrap; -} - -.favorites-empty__action img { - width: 20px; - height: 20px; -} - -.favorites-empty__action:focus-visible, -.favorites-recommendations__head a:focus-visible { - outline: 2px solid var(--accent-hot); - outline-offset: 3px; -} - -.favorites-recommendations { - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; - padding-bottom: 40px; -} - -.favorites-recommendations__head { - display: flex; - align-items: center; - justify-content: space-between; - height: 32px; - margin-bottom: 18px; -} - -.favorites-recommendations__head h2 { - margin: 0; - color: var(--text); - font-size: 20px; - font-weight: 600; - line-height: 28px; -} - -.favorites-recommendations__head a { - display: inline-flex; - align-items: center; - gap: 4px; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.favorites-recommendations__head a img { - width: 16px; - height: 16px; -} - -.favorites-recommendations__grid { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 24px; -} diff --git a/app/static/css/footer.css b/app/static/css/footer.css index e3c3cda..d017a09 100644 --- a/app/static/css/footer.css +++ b/app/static/css/footer.css @@ -255,3 +255,99 @@ text-align: center; } } + +/* 访问地址:横向地址卡片,窄屏改为纵向堆叠 */ +.footer-addrs { + display: flex; + flex-direction: column; + gap: 14px; +} + +.footer-addrs__head { + display: flex; + align-items: baseline; + gap: 12px; +} + +.footer-addrs__head h3 { + margin: 0; + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +.footer-addrs__head p { + margin: 0; + color: var(--text-dim); + font-size: 12px; + line-height: 18px; +} + +.footer-addrs__list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 10px; +} + +.footer-addr { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + padding: 10px 14px; + border: 1px solid var(--line-soft); + border-left: 3px solid var(--accent); + border-radius: var(--radius-sm); + background: var(--bg-panel); + text-decoration: none; +} + +.footer-addr:hover { + border-color: var(--line); + border-left-color: var(--accent-hot); + background: rgba(236, 35, 127, 0.08); +} + +.footer-addr__label { + flex-shrink: 0; + color: var(--text-muted); + font-size: 12px; + line-height: 18px; +} + +.footer-addr__url { + flex: 1; + min-width: 0; + color: var(--text); + font-size: 13px; + line-height: 18px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.footer-addr__go { + flex-shrink: 0; + opacity: 0.6; +} + +@media (max-width: 768px) { + .footer-addrs { + width: 100%; + gap: 10px; + text-align: left; + } + + .footer-addrs__head { + justify-content: space-between; + } + + .footer-addrs__list { + grid-template-columns: 1fr; + gap: 8px; + } + + .footer-addr { + padding: 9px 12px; + } +} diff --git a/app/static/css/header.css b/app/static/css/header.css index b49431e..2caeac7 100644 --- a/app/static/css/header.css +++ b/app/static/css/header.css @@ -1194,3 +1194,13 @@ body.drawer-open { overflow: hidden; } + +/* 手机页头:商务合作 Telegram 入口,与搜索按钮同尺寸,带品牌色底 */ +.site-header__tg { + background: rgba(42, 171, 238, 0.14); +} + +.site-header__tg .icon { + width: 18px; + height: 18px; +} diff --git a/app/static/css/history-mobile.css b/app/static/css/history-mobile.css deleted file mode 100644 index 0d6a0c2..0000000 --- a/app/static/css/history-mobile.css +++ /dev/null @@ -1,256 +0,0 @@ -/* 观看历史 H5:对齐 Figma 375px 画板。 */ -@media (max-width: 768px) { - .history-main { - display: flex; - flex-direction: column; - gap: 12px; - width: 100%; - max-width: none; - margin: 0; - padding: 12px 16px 20px; - } - - .history-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - min-width: 0; - min-height: 30px; - } - - .history-filters { - display: flex; - flex: 1 1 auto; - align-items: center; - gap: 10px; - min-width: 0; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - } - - .history-filters::-webkit-scrollbar { display: none; } - - .history-filter { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - height: 30px; - padding: 0 16px; - border: 1px solid rgba(255, 255, 255, .12); - border-radius: 999px; - background: transparent; - color: #a1a1aa; - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; - cursor: pointer; - } - - .history-filter.is-active { - border-color: rgba(236, 35, 127, .3); - background: rgba(236, 35, 127, .1); - color: #ff4a7b; - } - - .history-clear { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - height: auto; - padding: 0; - border: 0; - background: transparent; - color: #a1a1aa; - font-size: 12px; - font-weight: 400; - line-height: 18px; - white-space: nowrap; - cursor: pointer; - } - - .history-clear img, - .history-clear__desktop { display: none; } - .history-clear__mobile { display: inline; } - - .history-group, - .history-list { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; - } - - .history-group__title { - margin: 0; - color: #7c7c86; - font-size: 12px; - font-weight: 500; - line-height: 16px; - } - - .history-item { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - min-width: 0; - padding: 12px 0; - border-bottom: 1px solid rgba(255, 255, 255, .07); - } - - .history-item__cover { - display: block; - flex: 0 0 48px; - width: 48px; - height: 64px; - overflow: hidden; - border-radius: 6px; - background: #141417; - } - - .history-item__image { - display: block; - width: 100%; - height: 100%; - object-fit: cover; - } - - .history-item__body { - display: flex; - flex: 1 1 0; - flex-direction: column; - align-items: flex-start; - gap: 5px; - min-width: 0; - } - - .history-item__title { - display: block; - align-self: stretch; - overflow: hidden; - margin: 0; - color: #f2f2f4; - font-size: 15px; - font-weight: 600; - line-height: 21px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .history-item__title a { - color: inherit; - text-decoration: none; - } - - .history-item__details { - display: block; - align-self: stretch; - overflow: hidden; - color: #7c7c86; - font-size: 11px; - font-weight: 400; - line-height: 16px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .history-item__episodes, - .history-item__mobile-time { - display: inline; - } - - .history-item__mobile-time::before { - content: " "; - white-space: pre; - } - - .history-item__genre, - .history-item__time, - .history-item__resume-desktop { - display: none; - } - - .history-item__progress { - display: block; - width: 140px; - max-width: 100%; - height: 3px; - overflow: hidden; - border-radius: 999px; - background: rgba(255, 255, 255, .12); - } - - .history-item__progress-fill { - display: block; - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - } - - .history-item__resume { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - gap: 8px; - width: auto; - min-width: 0; - height: 32px; - padding: 0 12px; - border: 1px solid rgba(255, 255, 255, .24); - border-radius: 999px; - background: transparent; - color: #f2f2f4; - font-size: 14px; - font-weight: 500; - line-height: 20px; - text-decoration: none; - white-space: nowrap; - } - - .history-item__resume svg, - .history-item__resume img { - flex: 0 0 16px; - width: 16px; - height: 16px; - } - - .history-item__resume-mobile { - display: inline; - } - - .history-item__remove { - display: inline-flex; - flex: 0 0 14px; - align-items: center; - justify-content: center; - width: 14px; - height: 14px; - padding: 0; - border: 0; - background: transparent; - color: #7c7c86; - cursor: pointer; - } - - .history-item__remove svg, - .history-item__remove img { - display: block; - width: 14px; - height: 14px; - } - - .history-main .account-pagination { - margin-top: 4px; - } - - .history-empty, - .history-main .account-empty { - padding: 48px 0; - } -} diff --git a/app/static/css/history.css b/app/static/css/history.css deleted file mode 100644 index a8ad0c2..0000000 --- a/app/static/css/history.css +++ /dev/null @@ -1,256 +0,0 @@ -/* 观看历史:1440px Figma 桌面画板,沿用个人中心公共页头与页签。 */ -.history-page [hidden] { display: none !important; } - -.history-main { - display: flex; - flex-direction: column; - gap: 24px; - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; - padding: 24px 0 40px; -} - -.history-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-height: 36px; -} - -.history-filters { - display: flex; - align-items: center; - gap: 10px; -} - -.history-filter, -.history-clear, -.history-item__resume { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - justify-content: center; - height: 36px; - padding: 0; - border: 1px solid var(--line); - border-radius: 999px; - background: transparent; - color: var(--text-muted); - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; - cursor: pointer; -} - -.history-filter { - padding-inline: 16px; -} - -.history-filter.is-active { - border-color: rgba(236, 35, 127, .3); - background: rgba(236, 35, 127, .1); - color: var(--accent-hot); -} - -.history-clear { - gap: 6px; - padding-inline: 12px; - border-color: rgba(255, 255, 255, .24); - color: var(--text); -} - -.history-clear__mobile { display: none; } - -.history-clear svg, -.history-clear img, -.history-item__resume svg, -.history-item__resume img, -.history-item__remove svg, -.history-item__remove img { - display: block; - width: 16px; - height: 16px; - flex: 0 0 16px; -} - -.history-filter:hover, -.history-clear:hover, -.history-item__resume:hover, -.history-item__remove:hover { - border-color: rgba(255, 255, 255, .36); - color: var(--text); -} - -.history-filter:focus-visible, -.history-clear:focus-visible, -.history-item__resume:focus-visible, -.history-item__remove:focus-visible { - outline: 2px solid var(--accent-hot); - outline-offset: 3px; -} - -.history-group { - min-width: 0; -} - -.history-group__title { - margin: 0; - color: var(--text-dim); - font-size: 12px; - font-weight: 500; - line-height: 16px; -} - -.history-list { - margin: 0; - padding: 0; - list-style: none; -} - -.history-item { - display: grid; - grid-template-columns: 56px minmax(0, 1fr) 120px 106px 16px; - align-items: center; - gap: 16px; - min-width: 0; - min-height: 104px; - padding-block: 14px; - border-bottom: 1px solid var(--line-soft); -} - -.history-item__cover { - display: block; - width: 56px; - height: 75px; - overflow: hidden; - border-radius: 6px; - background: var(--bg-panel); -} - -.history-item__image { - display: block; - width: 100%; - height: 100%; - object-fit: cover; -} - -.history-item__body { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - min-width: 0; -} - -.history-item__title { - display: block; - max-width: 100%; - margin: 0; - overflow: hidden; - color: var(--text); - font-size: 16px; - font-weight: 600; - line-height: 22px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.history-item__title a { - color: inherit; - text-decoration: none; -} - -.history-item__title a:hover { - color: var(--accent-hot); -} - -.history-item__details { - display: flex; - align-items: center; - gap: 8px; - max-width: 100%; - overflow: hidden; - color: #a1a1aa; - font-size: 13px; - font-weight: 400; - line-height: 20px; - white-space: nowrap; -} - -.history-item__episodes, -.history-item__genre { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.history-item__episodes { - flex: 0 0 auto; -} - -.history-item__progress { - display: block; - width: 240px; - max-width: 100%; - height: 3px; - overflow: hidden; - border-radius: 999px; - background: var(--line); -} - -.history-item__progress-fill { - display: block; - width: var(--progress, 0%); - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.history-item__time { - color: var(--text-dim); - font-size: 12px; - line-height: 18px; - white-space: nowrap; -} - -.history-item__mobile-time, -.history-item__resume-mobile { - display: none; -} - -.history-item__resume { - gap: 8px; - width: 106px; - padding-inline: 12px; - border-color: rgba(255, 255, 255, .24); - color: var(--text); - text-decoration: none; -} - -.history-item__remove { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 36px; - padding: 0; - border: 0; - background: transparent; - color: var(--text-dim); - cursor: pointer; -} - -.history-group + .history-group { - margin-top: 0; -} - -.history-main.is-empty { - gap: 24px; - padding: 24px 0 40px; -} - -.history-main.is-empty .account-empty { - padding-block: 72px; -} diff --git a/app/static/css/index.css b/app/static/css/index.css index c1b5a73..4b0b64f 100644 --- a/app/static/css/index.css +++ b/app/static/css/index.css @@ -1364,3 +1364,78 @@ width: 100%; } } + +/* 公告跑马灯:官方访问地址以金色描边胶囊展示 */ +.announce__addrs { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.announce__addrs > span { + color: var(--gold); +} + +.announce__addr { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 10px; + border: 1px solid rgba(233, 201, 119, 0.45); + border-radius: 999px; + color: var(--text); + font-size: 12px; + line-height: 20px; + text-decoration: none; + background: rgba(233, 201, 119, 0.08); +} + +.announce__addr:hover { + border-color: var(--gold); + background: rgba(233, 201, 119, 0.16); +} + +@media (max-width: 768px) { + .announce__addr { + height: 20px; + padding: 0 8px; + font-size: 11px; + line-height: 18px; + } +} + +/* Banner 可读性(桌面端):背景海报保持原样,只在左侧文字区域加一道渐变 */ +@media (min-width: 769px) { + .hero__shade--side { + background: + linear-gradient(90deg, #0b0b0d 0%, rgba(11, 11, 13, 0.78) 16%, rgba(11, 11, 13, 0.45) 36%, rgba(11, 11, 13, 0) 52%), + linear-gradient(270deg, #0b0b0d 0%, rgba(11, 11, 13, 0.72) 8%, rgba(11, 11, 13, 0) 24%); + } + + .hero__title, + .hero__desc { + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6); + } +} + +.hero__attrs { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px 12px; +} + +@media (max-width: 768px) { + /* 手机端背景即主画面,不模糊;拉高底部渐变保证文字可读 */ + .hero__shade--bottom { + background: linear-gradient(180deg, rgba(11, 11, 13, 0) 0%, rgba(11, 11, 13, 0.25) 28%, rgba(11, 11, 13, 0.82) 56%, #0b0b0d 78%); + } + + .hero__desc { + -webkit-line-clamp: 2; + } + + .hero__attrs { + gap: 4px 10px; + } +} diff --git a/app/static/css/login.css b/app/static/css/login.css deleted file mode 100644 index 81a3e34..0000000 --- a/app/static/css/login.css +++ /dev/null @@ -1,283 +0,0 @@ -/* H5 登录页 */ - -body.page-login { - min-height: 100vh; - background: var(--bg); -} - -.page-login .page-subheader { - display: flex; - border-bottom: 0; -} - -.login-page { - min-height: 100vh; - padding: 56px 0 32px; - box-sizing: border-box; -} - -.login-page__inner { - width: 100%; - max-width: 440px; - margin: 0 auto; - padding: 24px var(--h5-gutter); - display: flex; - flex-direction: column; - align-items: center; - gap: 24px; - box-sizing: border-box; -} - -.login-page__brand { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; -} - -.login-page__logo { - width: 64px; - height: 64px; - border-radius: 12px; - object-fit: cover; - box-shadow: 0 8px 24px rgba(236, 35, 127, 0.4); -} - -.login-page__content { - align-self: stretch; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 18px; -} - -.login-page__intro { - align-self: stretch; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; -} - -.login-page__title { - margin: 0; - color: var(--text); - font-size: 28px; - font-weight: 600; - line-height: 38px; -} - -.login-page__desc { - margin: 0; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.login-page__form { - align-self: stretch; - display: flex; - flex-direction: column; - gap: 16px; -} - -.login-page__field { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; -} - -.login-page__label { - color: var(--text-muted); - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.login-page__control { - align-self: stretch; - height: 48px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 10px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid transparent; - border-radius: 12px; - outline: 1px solid transparent; - outline-offset: -1px; -} - -.login-page__control:focus-within { - outline-color: rgba(236, 35, 127, 0.45); -} - -.login-page__field.is-error .login-page__control { - outline-color: #ff5a5f; -} - -.login-page__error { - display: inline-flex; - align-items: center; - gap: 6px; - color: #ff5a5f; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.login-page__error[hidden] { - display: none !important; -} - -.login-page__error-icon { - width: 14px; - height: 14px; - flex-shrink: 0; -} - -.login-page__control > .icon { - width: 18px; - height: 18px; - flex-shrink: 0; -} - -.login-page__input { - flex: 1 1 0; - min-width: 0; - height: 100%; - padding: 0; - border: 0; - outline: none; - background: transparent; - color: var(--text); - font: inherit; - font-size: 15px; - line-height: 24px; -} - -.login-page__input::placeholder { - color: var(--text-dim); -} - -.login-page__toggle { - width: 18px; - height: 18px; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-dim); -} - -.login-page__toggle .icon { - width: 18px; - height: 18px; -} - -.login-page__meta { - align-self: stretch; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.login-page__remember { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--text-muted); - font-size: 13px; - line-height: 20px; - cursor: pointer; -} - -.login-page__checkbox { - position: absolute; - opacity: 0; - width: 1px; - height: 1px; - pointer-events: none; -} - -.login-page__check { - width: 18px; - height: 18px; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 4px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid var(--line); -} - -.login-page__checkbox:checked + .login-page__check { - border-color: transparent; - background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.login-page__checkbox:checked + .login-page__check::after { - content: ""; - width: 10px; - height: 6px; - border-left: 1.5px solid #fff; - border-bottom: 1.5px solid #fff; - transform: translateY(-1px) rotate(-45deg); -} - -.login-page__forgot { - color: #ff4a7b; - font-size: 13px; - line-height: 20px; -} - -.login-page__submit { - align-self: stretch; - height: 52px; - padding: 0 28px; - font-size: 16px; - line-height: 24px; -} - -.login-page__switch { - margin: 0; - align-self: stretch; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.login-page__switch a { - color: #ff4a7b; - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.login-page__rule { - align-self: stretch; - height: 1px; - background: var(--line-soft); -} - -.login-page__legal { - margin: 0; - align-self: stretch; - text-align: center; - color: var(--text-dim); - font-size: 12px; - line-height: 18px; -} - -.login-page__legal a { - color: inherit; - text-decoration: underline; - text-underline-offset: 2px; -} diff --git a/app/static/css/loginPop.css b/app/static/css/loginPop.css deleted file mode 100644 index 3269103..0000000 --- a/app/static/css/loginPop.css +++ /dev/null @@ -1,308 +0,0 @@ -/* PC 登录弹窗 */ - -.login-pop { - position: fixed; - inset: 0; - z-index: 290; - display: none; - align-items: center; - justify-content: center; - padding: 24px; -} - -.login-pop.is-open { - display: flex; -} - -.login-pop__mask { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.7); -} - -.login-pop__panel { - position: relative; - z-index: 1; - width: 440px; - max-width: 100%; - padding: 32px; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 20px; - background: #141417; - border: 1px solid var(--line); - border-radius: var(--radius-xl); - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55); - overflow: hidden; -} - -.login-pop__close { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.04); - border-radius: 999px; -} - -.login-pop__close .icon { - width: 18px; - height: 18px; -} - -.login-pop__intro { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - padding-right: 40px; -} - -.login-pop__title { - margin: 0; - color: var(--text); - font-size: 28px; - font-weight: 600; - line-height: 38px; -} - -.login-pop__desc { - margin: 0; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.login-pop__form { - align-self: stretch; - display: flex; - flex-direction: column; - gap: 16px; -} - -.login-pop__field { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; -} - -.login-pop__label { - color: var(--text-muted); - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.login-pop__control { - align-self: stretch; - height: 48px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 10px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid transparent; - border-radius: 12px; - outline: 1px solid transparent; - outline-offset: -1px; -} - -.login-pop__control:focus-within { - outline-color: rgba(236, 35, 127, 0.45); -} - -.login-pop__field.is-error .login-pop__control { - outline-color: #ff5a5f; -} - -.login-pop__error { - display: inline-flex; - align-items: center; - gap: 6px; - color: #ff5a5f; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.login-pop__error[hidden] { - display: none !important; -} - -.login-pop__error-icon { - width: 14px; - height: 14px; - flex-shrink: 0; -} - -.login-pop__control > .icon { - width: 18px; - height: 18px; - flex-shrink: 0; - color: var(--text-dim); -} - -.login-pop__input { - flex: 1 1 0; - min-width: 0; - height: 100%; - padding: 0; - border: 0; - outline: none; - background: transparent; - color: var(--text); - font: inherit; - font-size: 15px; - line-height: 24px; -} - -.login-pop__input::placeholder { - color: var(--text-dim); -} - -.login-pop__toggle { - width: 18px; - height: 18px; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-dim); -} - -.login-pop__toggle .icon { - width: 18px; - height: 18px; -} - -.login-pop__meta { - align-self: stretch; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; -} - -.login-pop__remember { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--text-muted); - font-size: 13px; - line-height: 20px; - cursor: pointer; -} - -.login-pop__checkbox { - position: absolute; - opacity: 0; - width: 1px; - height: 1px; - pointer-events: none; -} - -.login-pop__check { - width: 18px; - height: 18px; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 4px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid var(--line); -} - -.login-pop__checkbox:checked + .login-pop__check { - border-color: transparent; - background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.login-pop__checkbox:checked + .login-pop__check::after { - content: ""; - width: 10px; - height: 6px; - border-left: 1.5px solid #fff; - border-bottom: 1.5px solid #fff; - transform: translateY(-1px) rotate(-45deg); -} - -.login-pop__submit { - align-self: stretch; - height: 52px; - padding: 0 28px; - font-size: 16px; - line-height: 24px; -} - -.login-pop__submit.is-loading, -.login-pop__submit:disabled { - opacity: 0.72; - cursor: wait; -} - -.login-pop__tip { - margin: 0; - align-self: stretch; - padding: 8px 12px; - border-radius: 8px; - font-size: 13px; - line-height: 20px; - background: rgba(255, 74, 123, 0.1); - color: #ff4a7b; -} - -.login-pop__tip.is-success { - background: rgba(30, 120, 70, 0.12); - color: #1e7846; -} - -.login-pop__tip[hidden] { - display: none !important; -} - -.login-pop__rule { - align-self: stretch; - height: 1px; - background: var(--line-soft); -} - -.login-pop__legal { - margin: 0; - align-self: stretch; - text-align: center; - color: var(--text-dim); - font-size: 12px; - line-height: 18px; -} - -.login-pop__legal a { - color: inherit; - text-decoration: underline; - text-underline-offset: 2px; -} - -@media (max-width: 768px) { - .login-pop { - padding: 16px; - } - - .login-pop__panel { - max-height: calc(100vh - 32px); - max-height: calc(100dvh - 32px); - padding: 24px 20px; - border-radius: 18px; - overflow-y: auto; - } - - .login-pop__title { - font-size: 24px; - line-height: 32px; - } -} diff --git a/app/static/css/play.css b/app/static/css/play.css index 2255f6d..bffa0b1 100644 --- a/app/static/css/play.css +++ b/app/static/css/play.css @@ -980,10 +980,8 @@ a.play-meta__tag:focus-visible { width: 12px; height: 12px; border-radius: 999px; - background: - url("/static/images/coin.png") center / 8px 8px no-repeat, - linear-gradient(135deg, #e9c977 0%, #d9a94a 100%); - border: 1px solid #e9c977; + background: url("/static/images/lock.svg") center / 8px 8px no-repeat, var(--accent-deep); + border: 1px solid var(--accent); box-sizing: border-box; } @@ -1817,3 +1815,45 @@ a.play-meta__tag:focus-visible { line-height: 18px; } } + +/* 简介下方标签行 */ +.play-synopsis__tags { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} + +.play-synopsis__tag { + padding: 3px 10px; + border-radius: var(--radius-sm); + color: var(--text-muted); + font-size: 12px; + line-height: 18px; + text-decoration: none; + background: rgba(255, 255, 255, 0.06); +} + +.play-synopsis__tag::before { + content: "#"; + margin-right: 2px; + color: var(--accent); +} + +a.play-synopsis__tag:hover { + color: var(--text); + background: rgba(236, 35, 127, 0.12); +} + +@media (max-width: 768px) { + .play-synopsis__tags { + gap: 6px; + margin-top: 10px; + } + + .play-synopsis__tag { + padding: 2px 8px; + font-size: 11px; + line-height: 16px; + } +} diff --git a/app/static/css/profile-mobile.css b/app/static/css/profile-mobile.css deleted file mode 100644 index 41fe34f..0000000 --- a/app/static/css/profile-mobile.css +++ /dev/null @@ -1,590 +0,0 @@ -/* 个人中心 H5:以 Figma 375px 画板为基准。 */ -@media (max-width: 768px) { - .profile-page { - --profile-gutter: clamp(14px, 4.266667vw, 18px); - --profile-card-gap: clamp(7px, 2.133333vw, 9px); - width: 100%; - min-width: 0; - padding-top: 0; - background: #0b0b0d; - } - - .profile-hero { - width: 100%; - padding-top: 56px; /* 公共导航固定在视口顶部 */ - background: linear-gradient(180deg, #2a0d22 0%, #150a17 60%, #0b0b0d 100%); - } - - .profile-hero::before { - top: -90px; - left: -60px; - width: 360px; - height: 220px; - filter: blur(55px); - opacity: 0.22; - } - - .profile-hero__inner { - display: block; - grid-template-columns: none; - width: 100%; - max-width: none; - margin: 0; - padding: 0; - } - - .profile-identity { - display: flex; - align-items: center; - gap: clamp(12px, 3.733333vw, 16px); - min-width: 0; - margin: 0; - padding: 12px var(--profile-gutter) 16px; - } - - .profile-identity__avatar { - display: block; - flex: 0 0 clamp(55px, 17.066667vw, 73px); - width: clamp(55px, 17.066667vw, 73px); - height: clamp(55px, 17.066667vw, 73px); - border: 2px solid rgba(255, 255, 255, 0.24); - border-radius: 50%; - object-fit: cover; - } - - .profile-identity__copy { - display: grid; - flex: 1 1 0; - grid-template-columns: minmax(0, 1fr) auto; - grid-template-rows: 26px 16px; - column-gap: 12px; - row-gap: 6px; - min-width: 0; - } - - .profile-identity__name { - grid-column: 1; - grid-row: 1; - overflow: hidden; - color: #f2f2f4; - font-size: 18px; - font-weight: 600; - line-height: 26px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .profile-identity__meta { - grid-column: 1; - grid-row: 2; - overflow: hidden; - color: #a1a1aa; - font-size: 11px; - font-weight: 400; - line-height: 16px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .profile-identity__edit { - display: inline-flex; - grid-column: 2; - grid-row: 1 / 3; - align-self: center; - flex: 0 0 auto; - align-items: center; - justify-content: center; - gap: 8px; - height: 32px; - padding: 0 8px; - color: #f2f2f4; - border: 1px solid rgba(255, 255, 255, 0.24); - border-radius: 999px; - font-size: 0; - font-weight: 500; - line-height: 0; - white-space: nowrap; - } - - .profile-identity__edit::after { - content: "编辑"; - font-size: 14px; - line-height: 20px; - } - - .profile-identity__edit .icon, - .profile-identity__edit img, - .profile-identity__edit svg { - width: 16px; - height: 16px; - } - - .profile-wallet { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - min-height: 101px; - margin: 0 var(--profile-gutter) 16px; - padding: 14px; - background: #141417; - border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 12px; - } - - .profile-wallet__label { - display: flex; - align-items: center; - gap: 6px; - color: #f2f2f4; - font-size: 15px; - font-weight: 600; - line-height: 21px; - } - - .profile-wallet__amount { - display: block; - margin-top: 4px; - color: #e9c977; - font-family: Inter, var(--font); - font-size: 20px; - font-weight: 600; - line-height: 26px; - } - - .profile-wallet__hint { - margin: 4px 0 0; - color: #7c7c86; - font-size: 11px; - line-height: 16px; - white-space: nowrap; - } - - .profile-wallet__actions { - display: flex; - flex: 0 0 auto; - align-items: center; - } - - .profile-wallet__recharge { - display: inline-flex; - align-items: center; - justify-content: center; - height: 32px; - padding: 0 12px; - color: #fff; - background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - border: 0; - border-radius: 999px; - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; - } - - .profile-wallet__details { - display: none; - } - - .profile-stats { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - margin: 0; - min-height: 64px; - padding: 12px var(--profile-gutter) 14px; - border-top: 1px solid rgba(255, 255, 255, 0.07); - } - - .profile-stat { - display: flex; - flex: 0 0 auto; - flex-direction: column; - align-items: center; - gap: 2px; - min-width: 0; - text-align: center; - } - - .profile-stat__value { - color: #f2f2f4; - font-family: Inter, var(--font); - font-size: 16px; - font-weight: 600; - line-height: 20px; - } - - .profile-stat__value span { display: none; } - - .profile-stat__label { - color: #7c7c86; - font-size: 11px; - font-weight: 400; - line-height: 16px; - white-space: nowrap; - } - - .profile-tabs { - width: 100%; - margin: 0; - padding-top: 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.07); - } - - .profile-tabs__inner { - display: flex; - align-items: flex-end; - gap: 24px; - width: 100%; - max-width: none; - min-height: 38px; - height: 38px; - margin: 0; - padding: 6px var(--profile-gutter) 0; - overflow-x: auto; - overflow-y: hidden; - scrollbar-width: none; - -webkit-overflow-scrolling: touch; - } - - .profile-tabs__inner::-webkit-scrollbar { display: none; } - - .profile-tabs__link { - position: relative; - display: inline-flex; - flex: 0 0 auto; - align-items: flex-start; - justify-content: center; - min-height: 31px; - padding: 0 0 10px; - color: #a1a1aa; - font-size: 15px; - font-weight: 600; - line-height: 21px; - white-space: nowrap; - } - - .profile-tabs__link.is-active { color: #f2f2f4; } - - .profile-tabs__link.is-active::after { - content: ""; - position: absolute; - bottom: 0; - left: 50%; - width: 20px; - height: 2px; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - transform: translateX(-50%); - } - - .profile-main { - display: block; - width: 100%; - max-width: none; - min-width: 0; - margin: 0; - padding: 0; - } - - .profile-section { - display: flex; - flex-direction: column; - gap: 14px; - width: 100%; - max-width: none; - margin: 0; - padding-right: var(--profile-gutter); - padding-left: var(--profile-gutter); - } - - .profile-section--recent { padding-top: 16px; padding-bottom: 8px; } - .profile-section--favorites { padding-top: 12px; padding-bottom: 8px; } - - .profile-section__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - min-height: 26px; - } - - .profile-section__heading { - display: flex; - align-items: baseline; - gap: 10px; - min-width: 0; - } - - .profile-section__title { - margin: 0; - color: #f2f2f4; - font-size: 18px; - font-weight: 600; - line-height: 26px; - white-space: nowrap; - } - - .profile-section__subtitle { - color: #7c7c86; - font-size: 12px; - line-height: 18px; - white-space: nowrap; - } - - .profile-section--recent .profile-section__subtitle { display: none; } - - .profile-section__more { - display: inline-flex; - flex: 0 0 auto; - align-items: center; - gap: 2px; - color: #a1a1aa; - font-size: 13px; - line-height: 20px; - white-space: nowrap; - } - - .profile-section__more .icon, - .profile-section__more img, - .profile-section__more svg { width: 14px; height: 14px; } - - .profile-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px var(--profile-card-gap); - width: 100%; - min-width: 0; - } - - .profile-section--recent .profile-card:nth-child(n + 4) { display: none; } - - .profile-card { - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; - } - - .profile-card__cover { - position: relative; - display: block; - width: 100%; - aspect-ratio: 109 / 145; - overflow: hidden; - background: #1b1b1f; - border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 8px; - } - - .profile-card__cover::after { - content: ""; - position: absolute; - inset: 0; - z-index: 1; - background: linear-gradient(180deg, transparent 58%, rgba(0, 0, 0, 0.7) 100%); - pointer-events: none; - } - - .profile-card__image { - display: block; - width: 100%; - height: 100%; - object-fit: cover; - } - - .profile-card__progress { - position: absolute; - right: 10px; - bottom: 6px; - left: 10px; - z-index: 2; - height: 3px; - overflow: hidden; - background: rgba(255, 255, 255, 0.12); - border-radius: 999px; - } - - .profile-card__progress-fill { - display: block; - width: 60%; - height: 100%; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - } - - .profile-section--recent .profile-card:nth-child(1) .profile-card__progress-fill { width: 59.6%; } - .profile-section--recent .profile-card:nth-child(2) .profile-card__progress-fill { width: 24.7%; } - .profile-section--recent .profile-card:nth-child(3) .profile-card__progress-fill { width: 79.8%; } - - .profile-card__episodes { - position: absolute; - bottom: 10px; - left: 10px; - z-index: 2; - color: #f2f2f4; - font-size: 11px; - font-weight: 500; - line-height: 14px; - white-space: nowrap; - } - - .profile-card__badge { - position: absolute; - top: 4px; - right: 4px; - z-index: 2; - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 30px; - height: 20px; - padding: 0 5px; - color: #141414; - background: linear-gradient(135deg, #fadc44 15%, #f9c24e 85%); - border-radius: 6px; - font-size: 11px; - font-weight: 500; - line-height: 14px; - white-space: nowrap; - } - - .profile-card__badge:not(.profile-card__badge--hot) { - color: #141414; - background: linear-gradient(135deg, #fadc44 15%, #f9c24e 85%); - } - - .profile-card__badge--hot, - .profile-card__badge[data-badge="hot"] { - color: #fff; - background: linear-gradient(135deg, #f04050 15%, #d01030 85%); - } - - .profile-card__title { - display: block; - overflow: hidden; - color: #f2f2f4; - font-size: 14px; - font-weight: 600; - line-height: 20px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .profile-card__meta { - display: block; - overflow: hidden; - color: #a1a1aa; - font-size: 11px; - font-weight: 400; - line-height: 16px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .profile-card__remaining { display: none; } - - .profile-promo { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - max-width: none; - margin: 0; - padding: 8px var(--profile-gutter); - } - - .profile-ad { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 4px; - width: 100%; - aspect-ratio: 343 / 98; - min-height: 0; - background: #232328; - border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 12px; - text-align: center; - } - - .profile-ad__icon { width: 20px; height: 20px; object-fit: contain; } - - .profile-ad__title { - color: #a1a1aa; - font-size: 15px; - font-weight: 600; - line-height: 21px; - } - - .profile-ad__mobile-size { display: inline; } - .profile-ad__desktop-size { display: none; } - - .profile-ad__meta { - color: #7c7c86; - font-size: 11px; - line-height: 16px; - } - - .profile-ad__badge { - position: absolute; - top: 8px; - right: 8px; - display: inline-flex; - align-items: center; - justify-content: center; - height: 20px; - padding: 0 6px; - color: #f2f2f4; - background: rgba(0, 0, 0, 0.6); - border-radius: 999px; - font-size: 11px; - font-weight: 500; - line-height: 14px; - } - - /* The shared footer's compact profile arrangement in the mobile design. */ - .profile-page .site-footer__brand { gap: 18px; } - .profile-page .site-footer__links { - flex-wrap: wrap; - gap: 12px 16px; - } - .profile-page .footer-col { flex-wrap: nowrap; } - .profile-page .footer-col:first-child, - .profile-page .footer-col:nth-child(2) a:last-child, - .profile-page .footer-col:nth-child(3) a:last-child { display: none; } - .profile-page .footer-download { - display: flex; - flex-basis: 100%; - justify-content: center; - padding: 0; - border: 0; - background: none; - } - .profile-page .footer-download .qr-box, - .profile-page .footer-download__copy strong, - .profile-page .footer-download__copy p, - .profile-page .footer-download__copy .icon { display: none; } - .profile-page .footer-download .btn { - height: 20px; - padding: 0; - color: var(--text-muted); - background: none; - font-size: 13px; - line-height: 20px; - } - .profile-page .site-footer__rule { display: none; } - .profile-page .site-footer__legal p:first-child { - order: 2; - width: 100%; - padding-top: 16px; - margin-top: 16px; - border-top: 1px solid var(--line-soft); - } - .profile-page .site-footer__legal p:last-child { order: 1; } - .profile-page .site-footer__adult { display: none; } - .profile-page .site-footer__rights { display: none; } - .profile-page .site-footer__mobile-adult { display: inline; } -} diff --git a/app/static/css/profile.css b/app/static/css/profile.css deleted file mode 100644 index a4d8a8b..0000000 --- a/app/static/css/profile.css +++ /dev/null @@ -1,557 +0,0 @@ -/* Personal center: desktop layout based on the 1440px design frame. */ -.profile-page { - min-height: 100vh; - padding-top: var(--site-header-height, 72px); - background: var(--bg); -} - -.profile-hero { - position: relative; - overflow: hidden; - background: linear-gradient(180deg, #2a0d22 0%, #150a17 60%, #0b0b0d 100%); -} - -.profile-hero::before { - position: absolute; - top: -140px; - left: -120px; - width: 620px; - height: 360px; - border-radius: 50%; - background: var(--accent); - filter: blur(80px); - opacity: .2; - content: ""; - pointer-events: none; -} - -.profile-hero__inner, -.profile-stats, -.profile-tabs, -.profile-main, -.profile-promo { - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; -} - -.profile-hero__inner { - position: relative; - display: flex; - align-items: center; - justify-content: space-between; - gap: 32px; - padding-block: 36px 28px; -} - -.profile-identity { - display: flex; - align-items: center; - gap: 24px; - min-width: 0; -} - -.profile-identity__avatar { - width: 96px; - height: 96px; - flex: 0 0 96px; - border: 3px solid rgba(255, 255, 255, .24); - border-radius: 50%; - object-fit: cover; -} - -.profile-identity__copy { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; - min-width: 0; -} - -.profile-identity__name { - max-width: 100%; - margin: 0; - overflow: hidden; - color: var(--text); - font-size: 32px; - font-weight: 600; - line-height: 40px; - letter-spacing: -.01em; - text-overflow: ellipsis; - white-space: nowrap; -} - -.profile-identity__meta { - margin: 0; - color: var(--text-muted); - font-size: 13px; - line-height: 18px; - letter-spacing: .06em; -} - -.profile-identity__edit { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - height: 36px; - padding-inline: 12px; - border: 1px solid rgba(255, 255, 255, .24); - border-radius: 999px; - color: var(--text); - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; -} - -.profile-identity__edit img, -.profile-wallet__label img { - width: 16px; - height: 16px; - flex: 0 0 16px; -} - -.profile-wallet { - display: flex; - align-items: center; - gap: 28px; - min-height: 120px; - padding: 18px 24px; - border: 1px solid var(--line-soft); - border-radius: 16px; - background: var(--bg-elevated); -} - -.profile-wallet__label { - display: flex; - align-items: center; - gap: 8px; - margin: 0; - color: var(--text); - font-size: 15px; - font-weight: 600; - line-height: 21px; - white-space: nowrap; -} - -.profile-wallet__amount { - display: block; - margin: 4px 0 0; - color: var(--gold); - font-family: Inter, var(--font); - font-size: 28px; - font-weight: 600; - line-height: 34px; -} - -.profile-wallet__hint { - margin: 2px 0 0; - color: var(--text-dim); - font-size: 12px; - line-height: 18px; - white-space: nowrap; -} - -.profile-wallet__actions { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; - flex: 0 0 auto; -} - -.profile-wallet__recharge { - display: inline-flex; - align-items: center; - justify-content: center; - height: 40px; - padding-inline: 16px; - border-radius: 999px; - background: linear-gradient(135deg, #ff4a7b 15%, #ec237f 51%, #93226f 85%); - color: #fff; - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; -} - -.profile-wallet__details { - color: var(--text-muted); - font-size: 12px; - line-height: 18px; - white-space: nowrap; - text-decoration: none; -} - -.profile-wallet__details:hover { - color: var(--text); -} - -.profile-stats { - position: relative; - display: flex; - align-items: center; - gap: 40px; - padding: 18px 0 22px; - border-top: 1px solid var(--line-soft); -} - -.profile-stat { - display: flex; - flex-direction: column; - gap: 2px; -} - -.profile-stat__value { - color: var(--text); - font-family: Inter, var(--font); - font-size: 20px; - font-weight: 600; - line-height: 26px; - white-space: nowrap; -} - -.profile-stat__label { - color: var(--text-dim); - font-size: 12px; - line-height: 18px; -} - -.profile-tabs { - padding-top: 4px; - border-bottom: 1px solid var(--line-soft); -} - -.profile-tabs__inner { - display: flex; - align-items: flex-start; - gap: 32px; - height: 38px; -} - -.profile-tabs__link { - position: relative; - display: inline-flex; - flex-direction: column; - align-items: center; - height: 100%; - padding: 4px 0 0; - color: var(--text-muted); - font-size: 16px; - font-weight: 600; - line-height: 22px; - white-space: nowrap; -} - -.profile-tabs__link::after { - position: absolute; - bottom: 0; - left: 50%; - width: 24px; - height: 2px; - transform: translateX(-50%); - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - content: ""; - opacity: 0; -} - -.profile-tabs__link.is-active { - color: var(--text); -} - -.profile-tabs__link.is-active::after { - opacity: 1; -} - -.profile-section { - display: flex; - flex-direction: column; - gap: 20px; -} - -.profile-section--recent { - padding-block: 28px 20px; -} - -.profile-section--favorites { - padding-block: 20px; -} - -.profile-section__head, -.profile-section__heading { - display: flex; - align-items: center; -} - -.profile-section__head { - justify-content: space-between; - gap: 20px; -} - -.profile-section__heading { - gap: 14px; - min-width: 0; -} - -.profile-section__title { - margin: 0; - color: var(--text); - font-size: 22px; - font-weight: 600; - line-height: 30px; - white-space: nowrap; -} - -.profile-section__subtitle { - color: var(--text-dim); - font-size: 14px; - line-height: 22px; - white-space: nowrap; -} - -.profile-section__more { - display: inline-flex; - align-items: center; - gap: 4px; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; - white-space: nowrap; -} - -.profile-section__more img { - width: 16px; - height: 16px; -} - -.profile-grid { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 32px 24px; -} - -.profile-section__empty { - grid-column: 1 / -1; - margin: 8px 0 32px; - color: var(--text-muted); -} - -.profile-card { - display: flex; - flex-direction: column; - gap: 10px; - min-width: 0; -} - -.profile-card__cover { - position: relative; - display: block; - width: 100%; - aspect-ratio: 3 / 4; - overflow: hidden; - border: 1px solid var(--line-soft); - border-radius: 12px; - background: var(--bg-panel); -} - -.profile-card__cover::after { - position: absolute; - inset: 58% 0 0; - z-index: 1; - background: linear-gradient(180deg, transparent, rgba(0, 0, 0, .7)); - content: ""; - pointer-events: none; -} - -.profile-card__image { - width: 100%; - height: 100%; - object-fit: cover; -} - -.profile-card__episodes { - position: absolute; - bottom: 10px; - left: 10px; - z-index: 2; - color: var(--text); - font-size: 12px; - font-weight: 500; - line-height: 16px; - text-shadow: 0 1px 4px #000; -} - -.profile-card__progress { - position: absolute; - right: 10px; - bottom: 6px; - left: 10px; - z-index: 2; - height: 3px; - overflow: hidden; - border-radius: 999px; - background: rgba(255, 255, 255, .12); -} - -.profile-card__progress-fill { - display: block; - width: var(--progress, 0%); - height: 100%; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.profile-card__badge { - position: absolute; - top: 4px; - right: 4px; - z-index: 2; - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 36px; - height: 20px; - padding-inline: 8px; - border-radius: 6px; - background: linear-gradient(135deg, #fadc44 15%, #f9c24e 85%); - color: #141414; - font-size: 11px; - font-weight: 500; - line-height: 14px; - white-space: nowrap; -} - -.profile-card__badge--hot { - background: linear-gradient(135deg, #f04050 15%, #d01030 85%); - color: #fff; -} - -.profile-card__title { - margin: 0; - overflow: hidden; - color: var(--text); - font-size: 16px; - font-weight: 600; - line-height: 22px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.profile-card__title a { - color: inherit; - text-decoration: none; -} - -.profile-card__title a:hover { - color: var(--accent); -} - -.profile-card__meta { - margin: 0; - overflow: hidden; - color: var(--text-muted); - font-size: 13px; - line-height: 20px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.profile-promo { - display: grid; - width: 100%; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; - margin-inline: 0; - padding-block: 8px 40px; -} - -.profile-ad { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 6px; - width: 100%; - aspect-ratio: 592 / 169; - border: 1px solid var(--line-soft); - border-radius: 12px; - background: #232328; - text-align: center; -} - -.profile-ad__icon { - width: 24px; - height: 24px; - color: var(--text-muted); -} - -.profile-ad__title { - margin: 0; - color: var(--text-muted); - font-size: 16px; - font-weight: 600; - line-height: 22px; -} - -.profile-ad__mobile-size { - display: none; -} - -.profile-ad__meta { - margin: 0; - color: var(--text-dim); - font-size: 12px; - line-height: 18px; -} - -.profile-ad__badge { - position: absolute; - top: 8px; - right: 8px; - display: inline-flex; - align-items: center; - justify-content: center; - height: 20px; - padding-inline: 6px; - border-radius: 999px; - background: rgba(0, 0, 0, .6); - color: var(--text); - font-size: 11px; - font-weight: 500; - line-height: 14px; -} - -@media (min-width: 901px) { - .profile-page .site-footer__inner { - gap: 32px; - padding-bottom: 31px; - } - - .profile-page .site-footer__legal { - flex-direction: row; - justify-content: space-between; - gap: 24px; - } - - .profile-page .site-footer__legal p:last-child { - text-align: right; - } -} - -/* 账户列表使用上游总数进行服务端分页。 */ -.account-pagination { - display: flex; - align-items: center; - justify-content: center; - gap: 20px; - margin: 24px 0; - color: var(--text-muted); - font-size: 13px; -} - -.account-pagination a { - color: var(--text); - border: 1px solid var(--line); - border-radius: 20px; - padding: 7px 14px; -} diff --git a/app/static/css/purchased-mobile.css b/app/static/css/purchased-mobile.css deleted file mode 100644 index ca94e99..0000000 --- a/app/static/css/purchased-mobile.css +++ /dev/null @@ -1,152 +0,0 @@ -/* 已购剧集 H5:以 Figma 375px 画板为基准。 */ -@media (max-width: 768px) { - .purchased-main { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - max-width: none; - min-width: 0; - margin: 0; - padding: 12px var(--profile-gutter, 16px) 20px; - } - - .purchased-note { - margin: 0; - color: #7c7c86; - font-size: 12px; - font-weight: 400; - line-height: 18px; - } - - .purchased-list { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; - margin: 0; - padding: 0; - list-style: none; - } - - .purchased-item { - display: flex; - align-items: center; - gap: clamp(8px, 3.2vw, 14px); - width: 100%; - min-width: 0; - min-height: 89px; - padding: 12px 0; - border-bottom: 1px solid rgba(255, 255, 255, .07); - } - - .purchased-item__cover { - display: block; - flex: 0 0 clamp(41px, 12.8vw, 56px); - width: clamp(41px, 12.8vw, 56px); - height: clamp(55px, 17.066667vw, 75px); - overflow: hidden; - border-radius: 6px; - background: #141417; - } - - .purchased-item__cover img { - display: block; - width: 100%; - height: 100%; - object-fit: cover; - } - - .purchased-item__body { - display: flex; - flex: 1 1 0; - flex-direction: column; - align-items: flex-start; - gap: 5px; - min-width: 0; - } - - .purchased-item__title { - display: block; - max-width: 100%; - overflow: hidden; - margin: 0; - color: #f2f2f4; - font-size: clamp(13px, 4vw, 15px); - font-weight: 600; - line-height: 21px; - text-overflow: ellipsis; - white-space: nowrap; - } - - .purchased-item__info { - display: block; - max-width: 100%; - color: #7c7c86; - font-size: clamp(10px, 2.933333vw, 11px); - font-weight: 400; - line-height: 16px; - white-space: normal; - } - - .purchased-item__mobile-date { - display: inline; - white-space: nowrap; - } - - .purchased-item__date { - display: none; - } - - .purchased-item__progress { - display: block; - width: min(140px, 100%); - height: 3px; - overflow: hidden; - border-radius: 999px; - background: rgba(255, 255, 255, .12); - } - - .purchased-item__progress > span { - display: block; - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); - } - - .purchased-item__resume { - display: inline-flex; - flex: 0 0 clamp(67px, 20.8vw, 88px); - align-items: center; - justify-content: center; - gap: 6px; - width: clamp(67px, 20.8vw, 88px); - height: 32px; - padding: 0 8px; - border: 1px solid rgba(255, 255, 255, .24); - border-radius: 999px; - background: transparent; - color: #f2f2f4; - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; - } - - .purchased-item__resume svg, - .purchased-item__resume img { - display: block; - flex: 0 0 16px; - width: 16px; - height: 16px; - } - - .purchased-item__resume-desktop { display: none; } - .purchased-item__resume-mobile { display: inline; } -} - -@media (max-width: 359px) { - .purchased-page .profile-tabs__inner { - gap: 16px; - } -} diff --git a/app/static/css/purchased.css b/app/static/css/purchased.css deleted file mode 100644 index 9e2a344..0000000 --- a/app/static/css/purchased.css +++ /dev/null @@ -1,148 +0,0 @@ -/* 已购剧集:1440px 桌面稿,沿用个人中心公共页头与页签。 */ -.purchased-main { - display: flex; - flex-direction: column; - gap: 16px; - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; - padding: 24px 0 40px; -} - -.purchased-note { - margin: 0; - color: var(--text-dim); - font-size: 13px; - font-weight: 400; - line-height: 20px; -} - -.purchased-list { - min-width: 0; -} - -.purchased-item { - display: grid; - grid-template-columns: 56px minmax(0, 1fr) 120px 106px; - align-items: center; - gap: 16px; - min-width: 0; - min-height: 104px; - padding-block: 14px; - border-bottom: 1px solid var(--line-soft); -} - -.purchased-item__cover { - display: block; - width: 56px; - height: 75px; - overflow: hidden; - border-radius: 6px; - background: var(--bg-panel); -} - -.purchased-item__cover img { - display: block; - width: 100%; - height: 100%; - object-fit: cover; -} - -.purchased-item__body { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - min-width: 0; -} - -.purchased-item__title { - display: block; - max-width: 100%; - margin: 0; - overflow: hidden; - color: var(--text); - font-size: 16px; - font-weight: 600; - line-height: 22px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.purchased-item__info { - display: block; - max-width: 100%; - margin: 0; - overflow: hidden; - color: var(--text-muted); - font-size: 13px; - font-weight: 400; - line-height: 20px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.purchased-item__progress { - display: block; - width: 240px; - max-width: 100%; - height: 3px; - overflow: hidden; - border-radius: 999px; - background: var(--line); -} - -.purchased-item__progress > span { - display: block; - width: var(--progress, 100%); - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.purchased-item__date { - color: var(--text-dim); - font-size: 12px; - font-weight: 400; - line-height: 18px; - white-space: nowrap; -} - -.purchased-item__mobile-date, -.purchased-item__resume-mobile { - display: none; -} - -.purchased-item__resume { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - width: 106px; - height: 36px; - padding-inline: 12px; - border: 1px solid rgba(255, 255, 255, .24); - border-radius: 999px; - color: var(--text); - font-size: 14px; - font-weight: 500; - line-height: 20px; - white-space: nowrap; -} - -.purchased-item__resume img, -.purchased-item__resume svg { - display: block; - width: 16px; - height: 16px; - flex: 0 0 16px; -} - -.purchased-item__resume:hover { - border-color: rgba(255, 255, 255, .36); - color: var(--text); -} - -.purchased-item__resume:focus-visible { - outline: 2px solid var(--accent-hot); - outline-offset: 3px; -} diff --git a/app/static/css/recharge-mobile.css b/app/static/css/recharge-mobile.css deleted file mode 100644 index f199470..0000000 --- a/app/static/css/recharge-mobile.css +++ /dev/null @@ -1,117 +0,0 @@ -@media (max-width: 768px) { - .recharge-page { - --recharge-gutter: clamp(14px, 4.266667vw, 18px); - padding-top: 0; - } - - .recharge-page .site-header { display: none; } - .subpage-header { - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 56px; - padding: 0 var(--recharge-gutter); - border-bottom: 1px solid var(--line-soft); - background: var(--bg); - } - .subpage-header__button { display: flex; align-items: center; justify-content: center; width: 28px; height: 36px; } - .subpage-header__button img { width: 22px; height: 22px; } - .subpage-header__title { color: var(--text); font-size: 16px; font-weight: 600; line-height: 24px; } - - .recharge-main { width: 100%; margin: 0; padding: 16px 0 0; } - .recharge-breadcrumb, - .recharge-intro, - .recharge-balance__guide { display: none; } - - .recharge-balance { - width: auto; - height: 110px; - margin: 0 var(--recharge-gutter); - padding: 18px; - border-radius: 16px; - } - .recharge-balance__main { gap: 14px; } - .recharge-balance__coin { width: 72px; height: 72px; flex-basis: 72px; } - .recharge-balance__label { font-size: 11px; line-height: 16px; } - .recharge-balance__copy strong { margin-top: 2px; font-size: 28px; line-height: 34px; } - .recharge-balance__copy small { font-size: 13px; line-height: 20px; } - .recharge-balance__copy p { margin-top: 0; font-size: 11px; line-height: 16px; } - - .recharge-packages { margin-top: 20px; padding: 0 var(--recharge-gutter); } - .recharge-section-heading { gap: 10px; height: 22px; min-height: 22px; } - .recharge-section-heading h2, - .recharge-payment > h2 { font-size: 16px; line-height: 22px; } - .recharge-section-heading span { font-size: 12px; line-height: 18px; white-space: nowrap; } - .recharge-packages__grid { grid-template-columns: repeat(2,minmax(0,1fr)); gap: 10px; margin-top: 12px; } - .recharge-package { gap: 3px; height: 99px; padding: 12px; border-radius: 12px; } - .recharge-package__title { gap: 5px; font-size: 15px; line-height: 21px; white-space: nowrap; } - .recharge-package__title img { width: 14px; height: 14px; } - .recharge-package__hint { font-size: 11px; line-height: 16px; } - .recharge-package__price { font-size: 20px; line-height: 26px; } - .recharge-package__selected { top: 12px; right: 12px; width: 16px; height: 16px; } - .recharge-package__badge { top: -2px; right: 9px; height: 21px; padding-inline: 7px; font-size: 10px; } - .recharge-packages__note { display: none; } - - .recharge-payment { margin-top: 16px; padding: 0 var(--recharge-gutter); } - .recharge-payment__grid { grid-template-columns: repeat(2,minmax(0,1fr)); gap: 12px; margin-top: 12px; } - .recharge-method { gap: 10px; padding: 0 12px; } - .recharge-payment__checkout { flex-direction: column; gap: 11px; margin-top: 11px; } - .recharge-payment__checkout button { width: 100%; height: 48px; font-size: 16px; } - .recharge-payment__checkout p { font-size: 11px; line-height: 18px; text-align: center; } - .recharge-payment__status { text-align: center; } - - .recharge-history { margin-top: 22px; padding: 0 var(--recharge-gutter) 22px; } - .recharge-history__head { height: 26px; } - .recharge-history__head h2 { font-size: 18px; line-height: 26px; } - .recharge-history__filters, - .recharge-history__export { display: none; } - .recharge-history__more { display: flex; align-items: center; gap: 2px; margin-left: auto; padding: 0; border: 0; background: none; color: var(--text-muted); font-size: 12px; line-height: 18px; } - .recharge-history__more img { width: 14px; height: 14px; } - .recharge-history__table { height: auto; margin-top: 8px; padding: 0; border: 0; border-radius: 0; background: none; } - .recharge-history__columns { display: none; } - .recharge-history__row { display: grid; grid-template-columns: minmax(0,1fr) auto; grid-template-rows: 20px 16px; align-content: center; height: 64px; border-bottom: 1px solid var(--line-soft); font-size: 14px; line-height: 20px; } - .recharge-history__kind { grid-column: 1; grid-row: 1; } - .recharge-history__detail-line { display: flex; align-items: center; gap: 8px; grid-column: 1; grid-row: 2; min-width: 0; } - .recharge-history__description, - .recharge-history__time { color: var(--text-dim); font-size: 11px; line-height: 16px; white-space: nowrap; } - .recharge-history__description { overflow: hidden; text-overflow: ellipsis; } - .recharge-history__time { flex: 0 0 auto; } - .recharge-history__amount { align-self: center; grid-column: 2; grid-row: 1 / 3; font-size: 16px; line-height: 20px; } - .only-desktop { display: none; } - .only-mobile { display: inline; } - .recharge-pagination { display: none; } - - .recharge-page .site-footer { background: #141417; } - .recharge-page .site-footer__inner { padding: 32px 16px 27px; gap: 18px; } - .recharge-page .site-footer__brand { gap: 18px; } - .recharge-page .site-footer__links { flex-wrap: wrap; gap: 12px 16px; } - .recharge-page .footer-col { flex-wrap: nowrap; } - .recharge-page .footer-col:first-child, - .recharge-page .footer-col:nth-child(2) a:last-child, - .recharge-page .footer-col:nth-child(3) a:last-child { display: none; } - .recharge-page .footer-download { display: flex; flex-basis: 100%; justify-content: center; padding: 0; border: 0; background: none; } - .recharge-page .footer-download .qr-box, - .recharge-page .footer-download__copy strong, - .recharge-page .footer-download__copy p, - .recharge-page .footer-download__copy .icon { display: none; } - .recharge-page .footer-download .btn { height: 20px; padding: 0; color: var(--text-muted); background: none; font-size: 13px; line-height: 20px; } - .recharge-page .site-footer__rule { display: none; } - .recharge-page .site-footer__legal { flex-direction: column; align-items: center; gap: 0; } - .recharge-page .site-footer__legal p:first-child { order: 2; width: 100%; padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--line-soft); } - .recharge-page .site-footer__legal p:last-child { order: 1; } - .recharge-page .site-footer__adult, - .recharge-page .site-footer__rights { display: none; } - .recharge-page .site-footer__mobile-adult { display: inline; } -} - -@media (max-width: 359px) { - .recharge-balance { height: 110px; padding: 14px; } - .recharge-balance__main { gap: 10px; } - .recharge-balance__coin { width: 64px; height: 64px; flex-basis: 64px; } - .recharge-balance__copy p { line-height: 14px; } - .recharge-balance__note-full { display: none; } - .recharge-balance__note-short { display: inline; } - .recharge-package__title { font-size: 14px; } - .recharge-package__badge { top: -4px; height: 18px; padding-inline: 5px; font-size: 9px; } -} diff --git a/app/static/css/recharge-modal.css b/app/static/css/recharge-modal.css deleted file mode 100644 index ae6cabd..0000000 --- a/app/static/css/recharge-modal.css +++ /dev/null @@ -1,516 +0,0 @@ -.recharge-modal[hidden] { - display: none !important; -} - -.recharge-modal { - position: fixed; - inset: 0; - z-index: 320; - display: flex; - align-items: center; - justify-content: center; - padding: 24px; -} - -.recharge-modal__backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.7); -} - -.recharge-modal__panel { - position: relative; - z-index: 1; - display: flex; - flex-direction: column; - align-items: stretch; - gap: 16px; - width: 640px; - max-width: 100%; - max-height: calc(100dvh - 48px); - padding: 32px; - overflow-y: auto; - background: #141417; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 20px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55); - scrollbar-width: thin; -} - -.recharge-modal__handle { - display: none; -} - -.recharge-modal__head { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - min-height: 54px; -} - -.recharge-modal__heading { - min-width: 0; - display: flex; - flex-direction: column; - gap: 4px; -} - -.recharge-modal__heading h2, -.recharge-modal__subtitle { - margin: 0; -} - -.recharge-modal__heading h2 { - color: #f2f2f4; - font-size: 22px; - font-weight: 600; - line-height: 30px; -} - -.recharge-modal__subtitle { - color: #a1a1aa; - font-size: 13px; - font-weight: 400; - line-height: 20px; - white-space: nowrap; -} - -.recharge-modal__subtitle--mobile { - display: none; -} - -.recharge-modal__balance { - display: inline-flex; - align-items: center; - gap: 6px; - flex: none; - padding: 6px 12px; - color: #e9c977; - background: rgba(217, 169, 74, 0.14); - border-radius: 999px; -} - -.recharge-modal__balance strong { - font-family: Inter, var(--font); - font-size: 16px; - font-weight: 600; - line-height: 20px; -} - -.recharge-modal__coin { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 14px; - width: 14px; - height: 14px; - background: linear-gradient(135deg, #e9c977 15%, #d9a94a 85%); - border: 1px solid #e9c977; - border-radius: 50%; -} - -.recharge-modal__coin::before { - content: ""; - position: absolute; - inset: 2px; - border: 1px solid rgba(58, 42, 10, 0.45); - border-radius: 50%; -} - -.recharge-modal__coin img { - position: relative; - width: 8px; - height: 8px; -} - -.recharge-modal__coin--balance { - flex-basis: 16px; - width: 16px; - height: 16px; -} - -.recharge-modal__close { - position: absolute; - top: 16px; - right: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - background: rgba(255, 255, 255, 0.04); - border-radius: 50%; -} - -.recharge-modal__close img { - width: 18px; - height: 18px; -} - -.recharge-modal__packages { - display: grid; - grid-template-columns: repeat(3, 181px); - gap: 12px; -} - -.recharge-modal__package { - position: relative; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 4px; - width: 181px; - height: 99px; - padding: 12px; - text-align: left; - background: #141417; - border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 12px; -} - -.recharge-modal__package.is-selected { - background: rgba(217, 169, 74, 0.14); - border: 1.5px solid #d9a94a; -} - -.recharge-modal__package-top { - display: flex; - align-items: center; - justify-content: space-between; - gap: 4px; - width: 100%; - min-height: 21px; -} - -.recharge-modal__package-name { - display: inline-flex; - align-items: center; - gap: 6px; - white-space: nowrap; - color: #f2f2f4; - font-size: 15px; - font-weight: 600; - line-height: 21px; -} - -.recharge-modal__package.is-selected .recharge-modal__package-name, -.recharge-modal__package.is-selected .recharge-modal__package-price { - color: #e9c977; -} - -.recharge-modal__selected-icon { - display: none; - flex: none; - width: 14px; - height: 14px; -} - -.recharge-modal__package.is-selected .recharge-modal__selected-icon { - display: block; -} - -.recharge-modal__package-detail { - color: #7c7c86; - font-size: 12px; - font-weight: 400; - line-height: 18px; - white-space: nowrap; -} - -.recharge-modal__package-price { - color: #f2f2f4; - font-family: Inter, var(--font); - font-size: 20px; - font-weight: 600; - line-height: 26px; -} - -.recharge-modal__package-badge { - position: absolute; - top: -1px; - right: 10px; - display: inline-flex; - align-items: center; - justify-content: center; - height: 20px; - padding: 0 8px; - color: #141414; - background: linear-gradient(135deg, #e9c977 15%, #d9a94a 85%); - border-radius: 6px; - font-size: 11px; - font-weight: 500; - line-height: 14px; -} - -.recharge-modal__methods { - display: grid; - grid-template-columns: repeat(2, 278px); - gap: 12px; -} - -.recharge-modal__method { - display: flex; - align-items: center; - gap: 10px; - width: 278px; - height: 52px; - padding: 0 16px; - text-align: left; - background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 12px; -} - -.recharge-modal__method.is-selected { - background: rgba(236, 35, 127, 0.1); - border-color: rgba(236, 35, 127, 0.3); -} - -.recharge-modal__method-check { - display: inline-flex; - align-items: center; - justify-content: center; - flex: none; - width: 18px; - height: 18px; - border: 1px solid rgba(255, 255, 255, 0.24); - border-radius: 50%; -} - -.recharge-modal__method-check img { - display: none; - width: 12px; - height: 12px; -} - -.recharge-modal__method.is-selected .recharge-modal__method-check { - border: 0; - background: linear-gradient(135deg, #ff4a7b 15%, #ec237f 51%, #93226f 85%); -} - -.recharge-modal__method.is-selected .recharge-modal__method-check img { - display: block; -} - -.recharge-modal__method-copy { - display: flex; - flex-direction: column; - gap: 1px; - min-width: 0; - color: #f2f2f4; - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.recharge-modal__method-copy small { - color: #7c7c86; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.recharge-modal__pay { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 52px; - padding: 0 28px; - color: #fff; - background: linear-gradient(135deg, #ff4a7b 15%, #ec237f 51%, #93226f 85%); - border-radius: 999px; - font-size: 16px; - font-weight: 500; - line-height: 24px; -} - -.recharge-modal__status { - margin: -8px 0 0; - color: #a1a1aa; - font-size: 12px; - line-height: 18px; - text-align: center; -} - -.recharge-modal__status.is-ok { - color: #6ee7a8; -} - -.recharge-modal__empty { - margin: 0; - grid-column: 1 / -1; - color: rgba(255, 255, 255, 0.56); - font-size: 13px; - line-height: 20px; -} - -.recharge-modal__foot { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - color: #7c7c86; - font-size: 12px; - font-weight: 400; - line-height: 18px; - white-space: nowrap; -} - -.recharge-modal__foot a { - display: inline-flex; - align-items: center; - gap: 5px; - color: #a1a1aa; -} - -.recharge-modal__foot img { - width: 12px; - height: 12px; -} - -.recharge-modal__close:focus-visible, -.recharge-modal__package:focus-visible, -.recharge-modal__method:focus-visible, -.recharge-modal__pay:focus-visible, -.recharge-modal__foot a:focus-visible { - outline: 2px solid #ff4a7b; - outline-offset: 2px; -} - -@media (max-width: 768px) { - .recharge-modal { - align-items: flex-end; - justify-content: stretch; - padding: 0; - } - - .recharge-modal__panel { - width: 100%; - max-width: none; - max-height: 100dvh; - padding: 12px 20px 24px; - gap: 14px; - border: 0; - border-radius: 20px 20px 0 0; - box-shadow: none; - } - - .recharge-modal__handle { - display: flex; - justify-content: center; - align-items: center; - flex: none; - height: 4px; - } - - .recharge-modal__handle::before { - content: ""; - width: 36px; - height: 4px; - background: rgba(255, 255, 255, 0.24); - border-radius: 2px; - } - - .recharge-modal__head { - min-height: 46px; - } - - .recharge-modal__heading { - gap: 2px; - } - - .recharge-modal__heading h2 { - font-size: 18px; - line-height: 26px; - } - - .recharge-modal__subtitle { - font-size: 12px; - line-height: 18px; - } - - .recharge-modal__subtitle--desktop, - .recharge-modal__balance { - display: none; - } - - .recharge-modal__subtitle--mobile { - display: block; - } - - .recharge-modal__close { - position: static; - flex: none; - } - - .recharge-modal__packages { - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; - } - - .recharge-modal__package { - width: 100%; - } - - .recharge-modal__package-detail { - font-size: clamp(10px, 3.2vw, 12px); - } - - .recharge-modal__methods { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .recharge-modal__method { - width: 100%; - padding: 0 12px; - } - - .recharge-modal__method-copy small { - font-size: 11px; - line-height: 16px; - } - - .recharge-modal__pay { - height: 48px; - padding: 0 22px; - font-size: 15px; - line-height: 22px; - } - - .recharge-modal__foot { - font-size: 11px; - line-height: 16px; - } - - .recharge-modal__foot img { - width: 11px; - height: 11px; - } -} - -@media (max-width: 340px) { - .recharge-modal__panel { - padding-inline: 16px; - } - - .recharge-modal__package { - padding-inline: 9px; - } - - .recharge-modal__package-name { - font-size: 13px; - } - - .recharge-modal__package-badge { - top: -8px; - right: 4px; - } - - .recharge-modal__method { - gap: 7px; - padding-inline: 9px; - } -} diff --git a/app/static/css/recharge.css b/app/static/css/recharge.css deleted file mode 100644 index 20d52ec..0000000 --- a/app/static/css/recharge.css +++ /dev/null @@ -1,271 +0,0 @@ -.recharge-page { - min-height: 100vh; - padding-top: 72px; - background: var(--bg); -} - -.recharge-page [hidden] { display: none !important; } -.subpage-header { display: none; } - -.recharge-main { - width: min(calc(100% - 48px), var(--page-width)); - margin-inline: auto; - padding: 24px 0 41px; -} - -.recharge-breadcrumb { - display: flex; - align-items: center; - gap: 8px; - height: 20px; - color: var(--text-dim); - font-size: 13px; - line-height: 20px; -} - -.recharge-breadcrumb span:last-child { color: var(--text-muted); } -.recharge-breadcrumb a:hover { color: var(--text); } - -.recharge-intro { - display: flex; - flex-direction: column; - align-items: flex-start; - margin-top: 16px; -} - -.recharge-intro__eyebrow { - color: var(--accent-hot); - font-size: 13px; - font-weight: 600; - line-height: 18px; - letter-spacing: .14em; -} - -.recharge-intro__eyebrow::before { - display: inline-block; - width: 18px; - height: 2px; - margin: 0 10px 4px 0; - background: var(--accent); - content: ""; -} - -.recharge-intro h1 { - margin: 12px 0 0; - color: var(--text); - font-size: 28px; - font-weight: 600; - line-height: 36px; -} - -.recharge-intro p { - margin: 12px 0 0; - color: var(--text-muted); - font-size: 15px; - line-height: 24px; -} - -.recharge-balance { - display: flex; - align-items: center; - justify-content: space-between; - gap: 32px; - width: 100%; - height: 216px; - margin-top: 24px; - padding: 24px 32px; - border: 1px solid var(--line-soft); - border-radius: 20px; - background: linear-gradient(90deg, #2a0d22 0%, #150a17 55%, #141417 100%); -} - -.recharge-balance__main { - display: flex; - align-items: center; - gap: 24px; - min-width: 0; -} - -.recharge-balance__coin { - width: 120px; - height: 120px; - flex: 0 0 120px; - object-fit: contain; -} - -.recharge-balance__copy { min-width: 0; } -.recharge-balance__label, -.recharge-balance__guide h2 { - color: var(--text-dim); - font-size: 12px; - font-weight: 500; - line-height: 18px; -} - -.recharge-balance__copy strong { - display: block; - margin-top: 4px; - color: var(--gold); - font-family: Inter, var(--font); - font-size: 40px; - font-weight: 600; - line-height: 46px; -} - -.recharge-balance__copy small { - color: var(--text); - font-family: var(--font); - font-size: 15px; - font-weight: 600; - line-height: 22px; -} - -.recharge-balance__copy p { - margin: 3px 0 0; - color: var(--text-dim); - font-size: 12px; - line-height: 18px; - white-space: nowrap; -} - -.recharge-balance__note-short { display: none; } - -.recharge-balance__guide { - display: flex; - flex: 0 0 360px; - flex-direction: column; - gap: 9px; -} - -.recharge-balance__guide h2 { margin: 0; } -.recharge-balance__tip { display: flex; align-items: flex-start; gap: 8px; } -.recharge-balance__tip > img { width: 16px; height: 16px; flex: 0 0 16px; } -.recharge-balance__tip > div { display: flex; flex-direction: column; gap: 2px; } -.recharge-balance__tip strong { color: var(--text); font-size: 13px; font-weight: 600; line-height: 18px; } -.recharge-balance__tip span { color: var(--text-dim); font-size: 12px; line-height: 18px; } - -.recharge-packages { margin-top: 32px; } -.recharge-section-heading { display: flex; align-items: center; gap: 14px; min-height: 30px; } -.recharge-section-heading h2, -.recharge-payment > h2, -.recharge-history__head h2 { - margin: 0; - color: var(--text); - font-size: 20px; - font-weight: 600; - line-height: 26px; -} -.recharge-section-heading span { color: var(--text-dim); font-size: 13px; line-height: 20px; } - -.recharge-packages__grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 16px; - margin-top: 20px; -} - -.recharge-package { - position: relative; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 5px; - min-width: 0; - height: 124px; - padding: 18px 20px; - border: 1px solid var(--line-soft); - border-radius: 16px; - background: var(--bg-elevated); - color: var(--text); - text-align: left; - cursor: pointer; -} - -.recharge-package.is-selected { - border: 1.5px solid #d9a94a; - background: rgba(217, 169, 74, .14); -} - -.recharge-package__title { display: flex; align-items: center; gap: 6px; font-size: 18px; font-weight: 600; line-height: 26px; } -.recharge-package__title img { width: 16px; height: 16px; } -.recharge-package__hint { color: var(--text-dim); font-size: 12px; line-height: 18px; white-space: nowrap; } -.recharge-package__price { margin-top: auto; color: var(--text); font-family: Inter, var(--font); font-size: 24px; font-weight: 600; line-height: 30px; } -.recharge-package.is-selected .recharge-package__title, -.recharge-package.is-selected .recharge-package__price { color: var(--gold); } -.recharge-package__selected { position: absolute; top: 18px; right: 20px; width: 18px; height: 18px; display: none; } -.recharge-package.is-selected .recharge-package__selected { display: block; } -.recharge-package__badge { position: absolute; top: -2px; right: 10px; display: flex; align-items: center; justify-content: center; height: 21px; padding: 0 8px; border-radius: 5px; background: linear-gradient(135deg, #ffe7a0, #e9c977); color: #382506; font-size: 11px; font-weight: 600; line-height: 16px; } - -.recharge-packages__note { display: flex; align-items: center; gap: 8px; min-height: 24px; margin-top: 20px; } -.recharge-packages__note span { display: inline-flex; align-items: center; height: 24px; padding: 0 10px; border: 1px solid rgba(236,35,127,.34); border-radius: 999px; background: rgba(236,35,127,.08); color: var(--accent-hot); font-size: 12px; font-weight: 500; white-space: nowrap; } -.recharge-packages__note p { margin: 0; color: var(--text-dim); font-size: 12px; line-height: 18px; } -.recharge-data-empty { margin: 16px 0 0; color: var(--text-dim); font-size: 13px; line-height: 20px; } - -.recharge-payment { margin-top: 28px; } -.recharge-payment__grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 12px; margin-top: 20px; } -.recharge-method { display: flex; align-items: center; gap: 10px; height: 52px; min-width: 0; padding: 0 14px; border: 1px solid var(--line); border-radius: 12px; background: #242427; color: var(--text); text-align: left; cursor: pointer; } -.recharge-method.is-selected { border-color: rgba(236,35,127,.45); background: rgba(236,35,127,.12); } -.recharge-method__radio { display: flex; align-items: center; justify-content: center; width: 18px; height: 18px; flex: 0 0 18px; border: 1px solid #68686d; border-radius: 50%; } -.recharge-method__radio img { display: none; width: 12px; height: 12px; } -.recharge-method.is-selected .recharge-method__radio { border-color: transparent; background: linear-gradient(135deg,#ff4a7b,#ec237f); } -.recharge-method.is-selected .recharge-method__radio img { display: block; } -.recharge-method > span:last-child { display: flex; flex-direction: column; } -.recharge-method strong { font-size: 14px; font-weight: 500; line-height: 20px; } -.recharge-method small { color: var(--text-dim); font-size: 12px; line-height: 18px; } - -.recharge-payment__checkout { display: flex; align-items: center; gap: 16px; margin-top: 28px; } -.recharge-payment__checkout button { display: flex; align-items: center; justify-content: center; width: 320px; height: 56px; border: 0; border-radius: 999px; background: linear-gradient(105deg,#ff4a7b 0%,#ec237f 52%,#93226f 100%); color: #fff; font-size: 16px; font-weight: 600; line-height: 24px; cursor: pointer; } -.recharge-payment__checkout button:disabled { cursor: not-allowed; opacity: .5; } -.recharge-payment__checkout p { margin: 0; color: var(--text-dim); font-size: 12px; line-height: 18px; } -.recharge-payment__status { margin: 8px 0 0; color: var(--text-muted); font-size: 12px; line-height: 18px; } -.recharge-payment__status.is-success { color: #39c98a; } - -.recharge-history { margin-top: 40px; scroll-margin-top: calc(var(--site-header-height, 72px) + 16px); } -.recharge-history__head { display: flex; align-items: center; gap: 14px; height: 30px; } -.recharge-history__filters { display: flex; align-items: center; gap: 18px; align-self: stretch; } -.recharge-history__filters button { position: relative; padding: 0; border: 0; background: none; color: var(--text-dim); font-size: 13px; font-weight: 500; cursor: pointer; } -.recharge-history__filters button.is-active { color: var(--text); } -.recharge-history__filters button.is-active::after { position: absolute; right: 0; bottom: -7px; left: 0; height: 2px; background: var(--accent); content: ""; } -.recharge-history__export { display: flex; align-items: center; gap: 3px; margin-left: auto; padding: 0; border: 0; background: none; color: var(--text-muted); font-size: 13px; cursor: pointer; } -.recharge-history__export img { width: 16px; height: 16px; } -.recharge-history__more { display: none; } -.recharge-history__table { height: 310px; margin-top: 16px; padding: 4px 24px; border: 1px solid var(--line-soft); border-radius: 16px; background: var(--bg-elevated); } -.recharge-history__columns, -.recharge-history__row { display: grid; grid-template-columns: 200px minmax(0,1fr) 200px 120px; align-items: center; } -.recharge-history__columns { height: 45px; border-bottom: 1px solid var(--line-soft); color: var(--text-dim); font-size: 12px; line-height: 18px; } -.recharge-history__columns span:last-child, -.recharge-history__amount { text-align: right; } -.recharge-history__row { height: 51px; border-bottom: 1px solid var(--line-soft); color: var(--text-muted); font-size: 13px; line-height: 20px; } -.recharge-history__row:last-child { border-bottom: 0; } -.recharge-history__kind { color: var(--text); } -.recharge-history__detail-line { display: contents; } -.recharge-history__time { color: var(--text-dim); } -.recharge-history__amount { color: #f35b69; font-family: Inter, var(--font); font-size: 15px; font-weight: 600; } -.recharge-history__amount.is-credit { color: #39c98a; } -.recharge-history__empty { margin: 0; padding: 36px 0; color: var(--text-dim); font-size: 13px; text-align: center; } -.only-mobile { display: none; } - -.recharge-pagination, -.recharge-pagination [data-recharge-pages] { display: flex; align-items: center; justify-content: center; gap: 8px; } -.recharge-pagination { margin-top: 16px; } -.recharge-pagination button { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; padding: 0; border: 1px solid var(--line); border-radius: 8px; background: var(--bg-elevated); color: var(--text-muted); font-family: Inter, var(--font); font-size: 16px; font-weight: 600; cursor: pointer; } -.recharge-pagination button.is-active { border-color: transparent; background: linear-gradient(135deg,#ff4a7b,#ec237f 52%,#93226f); color: #fff; } -.recharge-pagination button:disabled { opacity: .45; cursor: default; } -.recharge-pagination button img { width: 16px; height: 16px; } -.recharge-pagination > button:first-child img { transform: rotate(180deg); } -.recharge-pagination__total { color: var(--text-dim); font-size: 12px; line-height: 18px; } - -.recharge-page button:focus-visible, -.recharge-page a:focus-visible { outline: 2px solid var(--accent-hot); outline-offset: 3px; } - -@media (max-width: 1080px) and (min-width: 769px) { - .recharge-balance__guide { flex-basis: 300px; } - .recharge-history__columns, - .recharge-history__row { grid-template-columns: 140px minmax(0,1fr) 175px 90px; } -} - -@media (min-width: 901px) { - .recharge-page .site-footer__inner { gap: 32px; padding-bottom: 32px; } - .recharge-page .site-footer__legal { flex-direction: row; justify-content: space-between; gap: 24px; } - .recharge-page .site-footer__legal p:last-child { text-align: right; } -} diff --git a/app/static/css/register.css b/app/static/css/register.css deleted file mode 100644 index 93f04f1..0000000 --- a/app/static/css/register.css +++ /dev/null @@ -1,255 +0,0 @@ -/* H5 注册页 */ - -body.page-register { - min-height: 100vh; - background: var(--bg); -} - -.page-register .page-subheader { - display: flex; - border-bottom: 0; -} - -.register-page { - min-height: 100vh; - padding: 56px 0 32px; - box-sizing: border-box; -} - -.register-page__inner { - width: 100%; - max-width: 440px; - margin: 0 auto; - padding: 24px var(--h5-gutter); - display: flex; - flex-direction: column; - align-items: center; - gap: 24px; - box-sizing: border-box; -} - -.register-page__brand { - display: flex; - flex-direction: column; - align-items: center; - gap: 8px; -} - -.register-page__logo { - width: 64px; - height: 64px; - border-radius: 12px; - object-fit: cover; - box-shadow: 0 8px 24px rgba(236, 35, 127, 0.4); -} - -.register-page__content { - align-self: stretch; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 18px; -} - -.register-page__intro { - align-self: stretch; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; -} - -.register-page__title { - margin: 0; - color: var(--text); - font-size: 28px; - font-weight: 600; - line-height: 38px; -} - -.register-page__desc { - margin: 0; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.register-page__form { - align-self: stretch; - display: flex; - flex-direction: column; - gap: 16px; -} - -.register-page__field { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; -} - -.register-page__label { - color: var(--text-muted); - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.register-page__control { - align-self: stretch; - height: 48px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 10px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid transparent; - border-radius: 12px; - outline: 1px solid transparent; - outline-offset: -1px; -} - -.register-page__control:focus-within { - outline-color: rgba(236, 35, 127, 0.45); -} - -.register-page__field.is-error .register-page__control { - outline-color: #ff5a5f; -} - -.register-page__error { - display: inline-flex; - align-items: center; - gap: 6px; - color: #ff5a5f; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.register-page__error[hidden] { - display: none !important; -} - -.register-page__error-icon { - width: 14px; - height: 14px; - flex-shrink: 0; -} - -.register-page__control > .icon { - width: 18px; - height: 18px; - flex-shrink: 0; -} - -.register-page__input { - flex: 1 1 0; - min-width: 0; - height: 100%; - padding: 0; - border: 0; - outline: none; - background: transparent; - color: var(--text); - font: inherit; - font-size: 15px; - line-height: 24px; -} - -.register-page__input::placeholder { - color: var(--text-dim); -} - -.register-page__toggle { - width: 18px; - height: 18px; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-dim); -} - -.register-page__toggle .icon { - width: 18px; - height: 18px; -} - -.register-page__agree { - display: flex; - align-items: flex-start; - gap: 10px; - color: var(--text-muted); - font-size: 13px; - line-height: 20px; - cursor: pointer; -} - -.register-page__agree a { - color: inherit; - text-decoration: underline; - text-underline-offset: 2px; -} - -.register-page__checkbox { - position: absolute; - opacity: 0; - width: 1px; - height: 1px; - pointer-events: none; -} - -.register-page__check { - width: 18px; - height: 18px; - flex-shrink: 0; - margin-top: 1px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 4px; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.24); -} - -.register-page__checkbox:checked + .register-page__check { - border-color: transparent; - background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.register-page__checkbox:checked + .register-page__check::after { - content: ""; - width: 10px; - height: 6px; - border-left: 1.5px solid #fff; - border-bottom: 1.5px solid #fff; - transform: translateY(-1px) rotate(-45deg); -} - -.register-page__submit { - align-self: stretch; - height: 52px; - padding: 0 28px; - font-size: 16px; - line-height: 24px; -} - -.register-page__switch { - margin: 0; - align-self: stretch; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.register-page__switch a { - color: #ff4a7b; - font-size: 14px; - font-weight: 500; - line-height: 20px; -} diff --git a/app/static/css/registerPop.css b/app/static/css/registerPop.css deleted file mode 100644 index f18d276..0000000 --- a/app/static/css/registerPop.css +++ /dev/null @@ -1,304 +0,0 @@ -/* PC 注册弹窗 */ - -.register-pop { - position: fixed; - inset: 0; - z-index: 290; - display: none; - align-items: center; - justify-content: center; - padding: 24px; -} - -.register-pop.is-open { - display: flex; -} - -.register-pop__mask { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.7); -} - -.register-pop__panel { - position: relative; - z-index: 1; - width: 440px; - max-width: 100%; - padding: 32px; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 20px; - background: #141417; - border: 1px solid var(--line); - border-radius: var(--radius-xl); - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.55); - overflow: hidden; -} - -.register-pop__close { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.04); - border-radius: 999px; -} - -.register-pop__close .icon { - width: 18px; - height: 18px; -} - -.register-pop__intro { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; - padding-right: 40px; -} - -.register-pop__title { - margin: 0; - color: var(--text); - font-size: 28px; - font-weight: 600; - line-height: 38px; -} - -.register-pop__desc { - margin: 0; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.register-pop__form { - align-self: stretch; - display: flex; - flex-direction: column; - gap: 16px; -} - -.register-pop__field { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 8px; -} - -.register-pop__label { - color: var(--text-muted); - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -.register-pop__control { - align-self: stretch; - height: 48px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 10px; - background: rgba(255, 255, 255, 0.06); - border: 1px solid transparent; - border-radius: 12px; - outline: 1px solid transparent; - outline-offset: -1px; -} - -.register-pop__control:focus-within { - outline-color: rgba(236, 35, 127, 0.45); -} - -.register-pop__field.is-error .register-pop__control { - outline-color: #ff5a5f; -} - -.register-pop__error { - display: inline-flex; - align-items: center; - gap: 6px; - color: #ff5a5f; - font-size: 12px; - font-weight: 400; - line-height: 18px; -} - -.register-pop__error[hidden] { - display: none !important; -} - -.register-pop__error-icon { - width: 14px; - height: 14px; - flex-shrink: 0; -} - -.register-pop__control > .icon { - width: 18px; - height: 18px; - flex-shrink: 0; -} - -.register-pop__input { - flex: 1 1 0; - min-width: 0; - height: 100%; - padding: 0; - border: 0; - outline: none; - background: transparent; - color: var(--text); - font: inherit; - font-size: 15px; - line-height: 24px; -} - -.register-pop__input::placeholder { - color: var(--text-dim); -} - -.register-pop__toggle { - width: 18px; - height: 18px; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--text-dim); -} - -.register-pop__toggle .icon { - width: 18px; - height: 18px; -} - -.register-pop__agree { - display: flex; - align-items: flex-start; - gap: 10px; - color: var(--text-muted); - font-size: 13px; - line-height: 20px; - cursor: pointer; -} - -.register-pop__agree a { - color: inherit; - text-decoration: underline; - text-underline-offset: 2px; -} - -.register-pop__checkbox { - position: absolute; - opacity: 0; - width: 1px; - height: 1px; - pointer-events: none; -} - -.register-pop__check { - width: 18px; - height: 18px; - flex-shrink: 0; - margin-top: 1px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 4px; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.24); -} - -.register-pop__checkbox:checked + .register-pop__check { - border-color: transparent; - background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%); -} - -.register-pop__checkbox:checked + .register-pop__check::after { - content: ""; - width: 10px; - height: 6px; - border-left: 1.5px solid #fff; - border-bottom: 1.5px solid #fff; - transform: translateY(-1px) rotate(-45deg); -} - -.register-pop__submit { - align-self: stretch; - height: 52px; - padding: 0 28px; - font-size: 16px; - line-height: 24px; -} - -.register-pop__submit.is-loading, -.register-pop__submit:disabled { - opacity: 0.72; - cursor: wait; -} - -.register-pop__tip { - margin: 0; - align-self: stretch; - padding: 8px 12px; - border-radius: 8px; - font-size: 13px; - line-height: 20px; - background: rgba(255, 74, 123, 0.1); - color: #ff4a7b; -} - -.register-pop__tip.is-success { - background: rgba(30, 120, 70, 0.12); - color: #1e7846; -} - -.register-pop__tip[hidden] { - display: none !important; -} - -.register-pop__switch { - margin: 0; - align-self: stretch; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - color: var(--text-muted); - font-size: 14px; - line-height: 22px; -} - -.register-pop__switch a { - color: #ff4a7b; - font-size: 14px; - font-weight: 500; - line-height: 20px; -} - -@media (max-width: 768px) { - .register-pop { - padding: 16px; - } - - .register-pop__panel { - max-height: calc(100vh - 32px); - max-height: calc(100dvh - 32px); - padding: 24px 20px; - border-radius: 18px; - overflow-y: auto; - } - - .register-pop__title { - font-size: 24px; - line-height: 32px; - } -} diff --git a/app/static/css/setting.css b/app/static/css/setting.css deleted file mode 100644 index 0726b40..0000000 --- a/app/static/css/setting.css +++ /dev/null @@ -1,65 +0,0 @@ -.setting-main { width: min(calc(100% - 48px), var(--page-width)); margin: 0 auto; padding: 24px 0 40px; } -.setting-layout { max-width: 900px; margin: 0 auto; } -.setting-card { display: flex; flex-direction: column; gap: 24px; min-width: 0; padding: 32px; border: 1px solid var(--line-soft); border-radius: 16px; background: #141417; } -.setting-section { display: flex; flex-direction: column; gap: 24px; scroll-margin-top: 90px; } -.setting-section__title { margin: 0; color: var(--text); font-size: 18px; font-weight: 600; line-height: 26px; } -.setting-avatar { display: flex; align-items: center; gap: 16px; } -.setting-avatar__image { flex: 0 0 72px; width: 72px; height: 72px; border-radius: 50%; object-fit: cover; } -.setting-avatar__button { display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 36px; padding: 0 12px; border: 1px solid rgba(255,255,255,.24); border-radius: 999px; color: var(--text); font-size: 14px; cursor: pointer; } -.setting-avatar__button img { width: 16px; height: 16px; } -.setting-avatar__file { position: absolute; width: 1px; height: 1px; opacity: 0; } -.setting-avatar__hint { margin: 8px 0 0; color: var(--text-dim); font-size: 12px; line-height: 18px; } -.setting-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px; } -.setting-field { display: flex; flex-direction: column; gap: 8px; min-width: 0; color: var(--text-muted); font-size: 14px; font-weight: 500; line-height: 20px; } -.setting-field input, .setting-field textarea, .setting-field__action { width: 100%; min-width: 0; min-height: 48px; padding: 12px 16px; border: 1px solid rgba(255,255,255,.12); border-radius: 12px; outline: none; background: rgba(255,255,255,.06); color: var(--text); font: 400 15px/24px var(--font); } -.setting-field textarea { min-height: 96px; resize: vertical; } -.setting-field input:focus, .setting-field textarea:focus { border-color: #ec237f; } -.setting-field input[readonly] { color: var(--text-muted); cursor: default; } -.setting-field small { color: var(--text-dim); font-size: 12px; font-weight: 400; line-height: 18px; } -.setting-field__action { color: #ff4a7b; text-align: right; cursor: pointer; } -.setting-field__value-action { position: relative; min-width: 0; } -.setting-field__value-action input { padding-right: 72px; } -.setting-field__inline-action { position: absolute; top: 50%; right: 16px; color: #ff4a7b; font-size: 14px; font-weight: 500; line-height: 20px; transform: translateY(-50%); cursor: pointer; } -.setting-field--intro { max-width: 100%; } -.setting-actions { display: flex; align-items: center; gap: 12px; padding-top: 24px; border-top: 1px solid var(--line-soft); } -.setting-actions button { min-height: 48px; padding: 0 22px; border-radius: 999px; color: #fff; font-size: 15px; font-weight: 500; cursor: pointer; } -.setting-actions__save { border: 0; background: linear-gradient(135deg, #ff4a7b 15%, #ec237f 51%, #93226f 85%); } -.setting-actions__logout { border: 1px solid rgba(255,255,255,.24); background: transparent; } -.setting-actions__unavailable { margin-left: auto; color: var(--text-dim); font-size: 13px; } -.setting-status { margin: -8px 0 0; min-height: 20px; color: #ff858b; font-size: 13px; line-height: 20px; } -.setting-status.is-success { color: #84d9a1; } -.setting-login-state { padding: 80px 16px; text-align: center; color: var(--text-muted); } -.setting-login-state h2 { color: var(--text); } -.setting-login-state a { color: #ff4a7b; } - -.setting-password-pop { position: fixed; inset: 0; z-index: 295; display: flex; align-items: center; justify-content: center; padding: 24px; } -.setting-password-pop[hidden] { display: none !important; } -.setting-password-pop__mask { position: absolute; inset: 0; width: 100%; height: 100%; background: rgba(0,0,0,.72); cursor: default; } -.setting-password-pop__panel { position: relative; z-index: 1; width: 440px; max-width: 100%; padding: 32px; border: 1px solid var(--line); border-radius: var(--radius-xl); background: #141417; box-shadow: 0 24px 64px rgba(0,0,0,.55); } -.setting-password-pop__close { position: absolute; top: 16px; right: 16px; display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; background: rgba(255,255,255,.04); } -.setting-password-pop__close .icon { width: 18px; height: 18px; } -.setting-password-pop__intro { padding-right: 40px; } -.setting-password-pop__intro h2 { margin: 0; color: var(--text); font-size: 28px; font-weight: 600; line-height: 38px; } -.setting-password-pop__intro p { margin: 6px 0 0; color: var(--text-muted); font-size: 14px; line-height: 22px; } -.setting-password-pop__form { display: flex; flex-direction: column; gap: 16px; margin-top: 20px; } -.setting-password-pop__status { min-height: 18px; margin: -4px 0 0; color: #ff858b; font-size: 12px; line-height: 18px; } -.setting-password-pop__status.is-success { color: #84d9a1; } -.setting-password-pop__submit { width: 100%; min-height: 52px; border: 0; border-radius: 999px; color: #fff; font-size: 16px; font-weight: 600; cursor: pointer; } -.setting-password-pop__submit:disabled { opacity: .72; cursor: wait; } - -@media (max-width: 768px) { - .setting-main { width: 100%; padding: 0 16px 24px; } - .setting-layout { max-width: none; } - .setting-card { gap: 16px; padding: 16px 0 0; border: 0; border-radius: 0; background: transparent; } - .setting-section { gap: 16px; } - .setting-section__title { display: none; } - .setting-avatar__image { flex-basis: 56px; width: 56px; height: 56px; } - .setting-fields { grid-template-columns: minmax(0, 1fr); gap: 16px; } - .setting-actions { flex-direction: column; align-items: stretch; gap: 16px; padding-top: 16px; } - .setting-actions button { width: 100%; } - .setting-actions__unavailable { margin: 0; text-align: center; } - .setting-status { text-align: center; } - .setting-password-pop { padding: 16px; } - .setting-password-pop__panel { max-height: calc(100vh - 32px); max-height: calc(100dvh - 32px); padding: 24px 20px; border-radius: 18px; overflow-y: auto; } - .setting-password-pop__intro h2 { font-size: 24px; line-height: 32px; } -} diff --git a/app/static/images/check.svg b/app/static/images/check.svg deleted file mode 100644 index 9d95a45..0000000 --- a/app/static/images/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/eye-off.svg b/app/static/images/eye-off.svg deleted file mode 100644 index 3ca0b3e..0000000 --- a/app/static/images/eye-off.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/eye.svg b/app/static/images/eye.svg deleted file mode 100644 index 26466fe..0000000 --- a/app/static/images/eye.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/favorites-empty/arrow-right.svg b/app/static/images/favorites-empty/arrow-right.svg deleted file mode 100644 index bd73f72..0000000 --- a/app/static/images/favorites-empty/arrow-right.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/favorites-empty/bookmark.svg b/app/static/images/favorites-empty/bookmark.svg deleted file mode 100644 index 632230f..0000000 --- a/app/static/images/favorites-empty/bookmark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/favorites/sliders.svg b/app/static/images/favorites/sliders.svg deleted file mode 100644 index ac662dd..0000000 --- a/app/static/images/favorites/sliders.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/app/static/images/gift-dim.svg b/app/static/images/gift-dim.svg deleted file mode 100644 index cb291ac..0000000 --- a/app/static/images/gift-dim.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/static/images/logout.svg b/app/static/images/logout.svg deleted file mode 100644 index 8e02ff9..0000000 --- a/app/static/images/logout.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/static/images/menu_gkls.svg b/app/static/images/menu_gkls.svg deleted file mode 100644 index e604c3f..0000000 --- a/app/static/images/menu_gkls.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/menu_grzx.svg b/app/static/images/menu_grzx.svg deleted file mode 100644 index 1e86399..0000000 --- a/app/static/images/menu_grzx.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/menu_jbcz.svg b/app/static/images/menu_jbcz.svg deleted file mode 100644 index ed3f3e3..0000000 --- a/app/static/images/menu_jbcz.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/menu_sc.svg b/app/static/images/menu_sc.svg deleted file mode 100644 index cd882fd..0000000 --- a/app/static/images/menu_sc.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/purchased-empty/bag-play.svg b/app/static/images/purchased-empty/bag-play.svg deleted file mode 100644 index 55f6c72..0000000 --- a/app/static/images/purchased-empty/bag-play.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/static/images/recharge/award.svg b/app/static/images/recharge/award.svg deleted file mode 100644 index fd9ba54..0000000 --- a/app/static/images/recharge/award.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/recharge/check-circle.svg b/app/static/images/recharge/check-circle.svg deleted file mode 100644 index 1205ddc..0000000 --- a/app/static/images/recharge/check-circle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/recharge/check.svg b/app/static/images/recharge/check.svg deleted file mode 100644 index 41309c2..0000000 --- a/app/static/images/recharge/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/recharge/close.svg b/app/static/images/recharge/close.svg deleted file mode 100644 index 7b412a5..0000000 --- a/app/static/images/recharge/close.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/recharge/coin-star.svg b/app/static/images/recharge/coin-star.svg deleted file mode 100644 index ac08866..0000000 --- a/app/static/images/recharge/coin-star.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/recharge/coin3d.png b/app/static/images/recharge/coin3d.png deleted file mode 100644 index 87ff947..0000000 Binary files a/app/static/images/recharge/coin3d.png and /dev/null differ diff --git a/app/static/images/recharge/download.svg b/app/static/images/recharge/download.svg deleted file mode 100644 index d3487e4..0000000 --- a/app/static/images/recharge/download.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/static/images/recharge/lock.svg b/app/static/images/recharge/lock.svg deleted file mode 100644 index edf11ea..0000000 --- a/app/static/images/recharge/lock.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/images/recharge/zap.svg b/app/static/images/recharge/zap.svg deleted file mode 100644 index 99e9793..0000000 --- a/app/static/images/recharge/zap.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/static/images/user.svg b/app/static/images/user.svg deleted file mode 100644 index 3b5b54d..0000000 --- a/app/static/images/user.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/static/js/common/auth.js b/app/static/js/common/auth.js deleted file mode 100644 index 77b4b08..0000000 --- a/app/static/js/common/auth.js +++ /dev/null @@ -1,515 +0,0 @@ -/** - * 认证相关前端封装:注册 / 登录 / 游客登录走本站 BFF,token 等写入 cookie。 - * 未注册用户自动游客登录(POST /api/auth/guest → 上游 /api/app/auth/guest)。 - */ -(function (global) { - var UUID_KEY = "uuid"; - var DEV_ID_KEY = "guest_dev_id"; - var CHANNEL_KEY = "channel"; - var GUEST_LOGOUT_FLAG_KEY = "guest_logout_manual"; - var guestLoginPromise = null; - var volatileDevID = ""; - /** 上游约定:token 失效需重新登录 */ - var TOKEN_EXPIRED_CODE = 1000; - var tokenRecoveryPromise = null; - - function toast(message, type) { - if (typeof global.showToast === "function") { - global.showToast(message, type || "info"); - return; - } - // 调试兜底:无 toast 组件时用简易提示 - try { - var tip = String(message || "").trim(); - if (!tip) return; - var el = document.getElementById("auth-debug-toast"); - if (!el) { - el = document.createElement("div"); - el.id = "auth-debug-toast"; - el.setAttribute("role", "status"); - el.style.cssText = - "position:fixed;left:50%;bottom:48px;transform:translateX(-50%);z-index:99999;" + - "max-width:86vw;padding:10px 16px;border-radius:8px;font-size:14px;line-height:1.4;" + - "color:#fff;background:rgba(20,20,20,.88);box-shadow:0 8px 24px rgba(0,0,0,.25);"; - document.body.appendChild(el); - } - el.textContent = tip; - el.style.background = - type === "error" - ? "rgba(180,40,40,.92)" - : type === "success" - ? "rgba(30,120,70,.92)" - : "rgba(20,20,20,.88)"; - el.style.display = "block"; - clearTimeout(el._hideTimer); - el._hideTimer = setTimeout(function () { - el.style.display = "none"; - }, 2600); - } catch (e) {} - } - - function getInviteCode() { - try { - var params = new URLSearchParams(global.location.search || ""); - return ( - params.get("inviteCode") || - params.get("invite") || - params.get("code") || - "" - ).trim(); - } catch (e) { - return ""; - } - } - - function hasSessionToken() { - return !!(global.CookieUtil && global.CookieUtil.getToken && global.CookieUtil.getToken()); - } - - // 与 91porn-h5-client generateUid 保持一致:16 位十六进制随机串 + 毫秒时间戳。 - function generateUUID() { - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - var uuid = []; - for (var i = 0; i < 16; i++) { - uuid[i] = chars.charAt(Math.floor(Math.random() * 16)); - } - return uuid.join("") + Date.now(); - } - - function syncDevID(devID) { - if (!devID) return; - try { - localStorage.setItem(UUID_KEY, devID); - // 保留旧 key,兼容已上线页面和服务端游客 cookie。 - localStorage.setItem(DEV_ID_KEY, devID); - } catch (e) {} - try { - if (global.CookieUtil && typeof global.CookieUtil.set === "function") { - global.CookieUtil.set(DEV_ID_KEY, devID, { days: 365 }); - } - } catch (e2) {} - } - - function getOrCreateDevID() { - var devID = ""; - try { - devID = (localStorage.getItem(UUID_KEY) || "").trim(); - } catch (e) {} - if (devID) { - syncDevID(devID); - return devID; - } - - // 首屏 SSR 可能已生成同规则 UUID 并写入 cookie,优先复用以保持游客身份一致。 - var fromCookie = - global.CookieUtil && typeof global.CookieUtil.get === "function" - ? global.CookieUtil.get(DEV_ID_KEY) - : ""; - if (fromCookie) { - syncDevID(fromCookie); - return fromCookie; - } - - if (!volatileDevID) { - volatileDevID = generateUUID(); - } - syncDevID(volatileDevID); - return volatileDevID; - } - - function getChannel() { - if ( - global.ChannelAttribution && - typeof global.ChannelAttribution.get === "function" - ) { - return global.ChannelAttribution.get(); - } - - var channel = ""; - try { - channel = (localStorage.getItem(CHANNEL_KEY) || "").trim(); - if (channel) { - if (global.CookieUtil && typeof global.CookieUtil.set === "function") { - global.CookieUtil.set(CHANNEL_KEY, channel, { days: 365 }); - } - return channel; - } - } catch (e) {} - try { - channel = - global.CookieUtil && typeof global.CookieUtil.get === "function" - ? String(global.CookieUtil.get(CHANNEL_KEY) || "").trim() - : ""; - if (channel) { - try { - localStorage.setItem(CHANNEL_KEY, channel); - } catch (e2) {} - return channel; - } - } catch (e3) {} - try { - var params = new URLSearchParams(global.location.search || ""); - channel = (params.get("dc") || params.get("ch") || params.get("pc") || "").trim(); - if (channel) { - localStorage.setItem(CHANNEL_KEY, channel); - if (global.CookieUtil && typeof global.CookieUtil.set === "function") { - global.CookieUtil.set(CHANNEL_KEY, channel, { days: 365 }); - } - } - } catch (e4) {} - return channel; - } - - function clearGuestLogoutFlag() { - try { - sessionStorage.removeItem(GUEST_LOGOUT_FLAG_KEY); - } catch (e) {} - try { - if (global.CookieUtil && typeof global.CookieUtil.remove === "function") { - global.CookieUtil.remove("guest_skip_auto"); - } - } catch (e2) {} - } - - function markGuestLogout() { - try { - sessionStorage.setItem(GUEST_LOGOUT_FLAG_KEY, "1"); - } catch (e) {} - try { - if (global.CookieUtil && typeof global.CookieUtil.set === "function") { - // 不设 days → session cookie,配合 SSR 跳过本次自动游客 - global.CookieUtil.set("guest_skip_auto", "1"); - } - } catch (e2) {} - } - - function isGuestLogoutMarked() { - try { - if (sessionStorage.getItem(GUEST_LOGOUT_FLAG_KEY) === "1") return true; - } catch (e) {} - try { - if ( - global.CookieUtil && - typeof global.CookieUtil.get === "function" && - global.CookieUtil.get("guest_skip_auto") === "1" - ) { - return true; - } - } catch (e2) {} - return false; - } - - function syncTrackerUid(user) { - if (!user || !global.tracker || typeof global.tracker.setUid !== "function") return; - var uid = user.userId != null ? user.userId : user.uid != null ? user.uid : user.id; - if (uid == null || String(uid) === "") return; - global.tracker.setUid(String(uid)); - } - - function applyAuthSession(res, failTip) { - if (!res || res.code !== 200 || !res.data) { - var err = new Error((res && (res.tip || res.msg)) || failTip || "请求失败"); - err.code = res && res.code; - err.data = res; - throw err; - } - if (global.CookieUtil && typeof global.CookieUtil.setAuthSession === "function") { - global.CookieUtil.setAuthSession(res.data); - } - clearGuestLogoutFlag(); - syncTrackerUid(res.data); - try { - document.dispatchEvent( - new CustomEvent("auth:login", { detail: { user: res.data } }) - ); - } catch (e) {} - return res; - } - - function register(payload) { - var channel = getChannel(); - var body = { - accountNo: (payload.accountNo || "").trim(), - password: payload.password || "", - devID: getOrCreateDevID(), - channel: channel, - }; - var invite = (payload.agentInviteCode || getInviteCode() || "").trim(); - if (invite) body.agentInviteCode = invite; - - // 游客升级注册:携带当前游客 token,上游按 Authorization 绑定原游客账号。 - var options = {}; - var token = - global.CookieUtil && typeof global.CookieUtil.getToken === "function" - ? global.CookieUtil.getToken() - : ""; - var registered = - global.CookieUtil && typeof global.CookieUtil.isRegistered === "function" - ? global.CookieUtil.isRegistered() - : false; - if (token && !registered) { - options.headers = { Authorization: token }; - } - - return global.HttpRequest.post("/api/auth/register", body, options).then(function (res) { - return applyAuthSession(res, "注册失败"); - }); - } - - function login(payload) { - var body = { - account: (payload.account || "").trim(), - password: payload.password || "", - devID: getOrCreateDevID(), - channel: getChannel(), - }; - return global.HttpRequest.post("/api/auth/login", body).then(function (res) { - return applyAuthSession(res, "登录失败"); - }); - } - - function resetPassword(payload) { - return global.HttpRequest.post("/api/auth/resetPassword", { - accountNo: (payload.accountNo || "").trim(), - email: (payload.email || "").trim(), - newPassword: payload.newPassword || "", - }); - } - - function guestLogin() { - var devId = getOrCreateDevID(); - return global.HttpRequest.post("/api/auth/guest", { - devID: devId, - deviceId: devId, - channel: getChannel(), - mac: "", - }).then(function (res) { - return applyAuthSession(res, "游客登录失败"); - }); - } - - function isAuthEndpoint(url) { - return /\/api\/auth\/(guest|login|register|resetPassword)(\?|$)/.test(String(url || "")); - } - - function isSafeRedirect(path) { - return typeof path === "string" && path.charAt(0) === "/" && path.charAt(1) !== "/"; - } - - /** PC 打开登录弹窗;H5 跳转登录页 */ - function promptRegisteredRelogin() { - toast("登录已过期,请重新登录", "warning"); - var isMobile = - global.matchMedia && global.matchMedia("(max-width: 768px)").matches; - if (!isMobile && typeof global.openLoginPop === "function") { - try { - global.openLoginPop(); - return; - } catch (e) {} - } - if (typeof global.openLoginDialog === "function") { - try { - var opened = global.openLoginDialog(); - if (opened && typeof opened.then === "function") { - opened.catch(function () { - global.location.href = "/login"; - }); - return; - } - return; - } catch (e2) {} - } - global.location.href = "/login"; - } - - /** - * 接口返回 code=1000(token 失效)时的统一处理。 - * - 游客:清会话后走游客登录,成功后可重试原请求 - * - 注册账号:清会话并打开登录弹窗 / 跳转登录页 - * @returns {Promise<{ok:boolean, guest?:boolean, needLogin?:boolean, res?:any}>} - */ - function handleTokenExpired() { - if (tokenRecoveryPromise) { - return tokenRecoveryPromise; - } - - var wasRegistered = - global.CookieUtil && typeof global.CookieUtil.isRegistered === "function" - ? global.CookieUtil.isRegistered() - : false; - - if (global.CookieUtil && typeof global.CookieUtil.clearAuth === "function") { - global.CookieUtil.clearAuth(); - } - try { - document.dispatchEvent( - new CustomEvent("auth:logout", { detail: { reason: "token-expired" } }) - ); - } catch (e) {} - - if (wasRegistered) { - var registeredJob = Promise.resolve().then(function () { - promptRegisteredRelogin(); - return { ok: false, needLogin: true }; - }); - tokenRecoveryPromise = registeredJob; - registeredJob.finally(function () { - setTimeout(function () { - if (tokenRecoveryPromise === registeredJob) { - tokenRecoveryPromise = null; - } - }, 1200); - }); - return registeredJob; - } - - clearGuestLogoutFlag(); - var guestJob = guestLogin() - .then(function (res) { - return { ok: true, guest: true, res: res }; - }) - .catch(function (err) { - toast("会话已失效,请刷新页面重试", "error"); - throw err; - }) - .finally(function () { - if (tokenRecoveryPromise === guestJob) { - tokenRecoveryPromise = null; - } - }); - tokenRecoveryPromise = guestJob; - return guestJob; - } - - /** - * 无 token(未自己登录)时自动游客登录。 - * SSR 也会在首屏补游客;此处作客户端兜底。 - * 手动退出后跳过本次;清掉标记后,下次进页再自动游客。 - * BFF: POST /api/auth/guest → 上游 POST /api/app/auth/guest - */ - function ensureGuestSession() { - if (hasSessionToken()) { - return Promise.resolve({ ok: true, skipped: true }); - } - if (isGuestLogoutMarked()) { - // 本次因手动退出跳过;清标记,下次进页恢复自动游客 - clearGuestLogoutFlag(); - return Promise.resolve({ ok: true, skipped: true, reason: "manual-logout" }); - } - if (!global.HttpRequest) { - return Promise.resolve({ ok: false, skipped: true, reason: "no-http" }); - } - if (!guestLoginPromise) { - guestLoginPromise = guestLogin() - .then(function (res) { - return { ok: true, res: res }; - }) - .catch(function (err) { - return { ok: false, error: err }; - }) - .finally(function () { - guestLoginPromise = null; - }); - } - return guestLoginPromise; - } - - function uploadImage(file) { - var fd = new FormData(); - fd.append("file", file); - return global.HttpRequest.post("/api/upload/image", fd).then(function (res) { - if (!res || res.code !== 200 || !res.data || !res.data.url) { - var err = new Error((res && (res.tip || res.msg)) || "上传失败"); - err.code = res && res.code; - err.data = res; - throw err; - } - return res.data; - }); - } - - function updateProfile(payload) { - var body = {}; - if (payload.nickName != null && String(payload.nickName).trim() !== "") { - body.nickName = String(payload.nickName).trim(); - } - if (payload.introduction != null) { - body.introduction = String(payload.introduction).trim(); - } - if (payload.avatar) body.avatar = payload.avatar; - if (payload.oldPassword) body.oldPassword = payload.oldPassword; - if (payload.newPassword) body.newPassword = payload.newPassword; - if (typeof payload.privacyMode === "boolean") body.privacyMode = payload.privacyMode; - - return global.HttpRequest.post("/api/auth/profile", body).then(function (res) { - if (!res || res.code !== 200) { - var err = new Error((res && (res.tip || res.msg)) || "保存失败"); - err.code = res && res.code; - err.data = res; - throw err; - } - if (res.data && global.CookieUtil && typeof global.CookieUtil.setAuthSession === "function") { - global.CookieUtil.setAuthSession(res.data); - } - if (res.data) syncTrackerUid(res.data); - return res; - }); - } - - function isValidEmail(email) { - return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || "").trim()); - } - - /** 登录凭证:账号 4–16 位,或合法邮箱 */ - function isValidLoginAccount(account) { - var v = String(account || "").trim(); - if (!v) return false; - if (v.indexOf("@") >= 0) return isValidEmail(v); - return v.length >= 4 && v.length <= 16; - } - - function logout() { - markGuestLogout(); - if (global.CookieUtil && typeof global.CookieUtil.clearAuth === "function") { - global.CookieUtil.clearAuth(); - } - try { - document.dispatchEvent(new CustomEvent("auth:logout", { detail: { reason: "manual" } })); - } catch (e) {} - if (!global.HttpRequest) { - return Promise.resolve({ code: 200 }); - } - return global.HttpRequest.post("/api/auth/logout", {}).catch(function () { - return { code: 200 }; - }); - } - - global.AuthAPI = { - TOKEN_EXPIRED_CODE: TOKEN_EXPIRED_CODE, - register: register, - login: login, - logout: logout, - resetPassword: resetPassword, - guestLogin: guestLogin, - ensureGuestSession: ensureGuestSession, - handleTokenExpired: handleTokenExpired, - isAuthEndpoint: isAuthEndpoint, - markGuestLogout: markGuestLogout, - hasSessionToken: hasSessionToken, - uploadImage: uploadImage, - updateProfile: updateProfile, - getInviteCode: getInviteCode, - getOrCreateDevID: getOrCreateDevID, - getChannel: getChannel, - isValidEmail: isValidEmail, - isValidLoginAccount: isValidLoginAccount, - toast: toast, - }; - - // 每次页面初始化都准备好持久设备 UUID,并捕获首次渠道。 - getOrCreateDevID(); - getChannel(); - - // 首屏无 token → 自动游客登录 - ensureGuestSession(); -})(typeof window !== "undefined" ? window : this); diff --git a/app/static/js/common/channel.js b/app/static/js/common/channel.js deleted file mode 100644 index 23eb1bf..0000000 --- a/app/static/js/common/channel.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 全站渠道首次归因。 - * 与落地页约定一致,按 dc、ch、pc 顺序识别,并同步到 localStorage 与 cookie。 - */ -(function (global) { - "use strict"; - - var STORAGE_KEY = "channel"; - var QUERY_KEYS = ["dc", "ch", "pc"]; - - function clean(value) { - return String(value == null ? "" : value).trim(); - } - - function readStorage() { - try { - return clean(global.localStorage.getItem(STORAGE_KEY)); - } catch (e) { - return ""; - } - } - - function writeStorage(channel) { - try { - global.localStorage.setItem(STORAGE_KEY, channel); - } catch (e) {} - } - - function readCookie() { - try { - var match = document.cookie.match(/(?:^|;\s*)channel=([^;]*)/); - return match ? clean(decodeURIComponent(match[1])) : ""; - } catch (e) { - return ""; - } - } - - function writeCookie(channel) { - try { - var secure = global.location && global.location.protocol === "https:" ? "; Secure" : ""; - document.cookie = - STORAGE_KEY + - "=" + - encodeURIComponent(channel) + - "; Path=/; Max-Age=31536000; SameSite=Lax" + - secure; - } catch (e) {} - } - - function readQuery() { - try { - var params = new URLSearchParams(global.location.search || ""); - for (var i = 0; i < QUERY_KEYS.length; i += 1) { - var channel = clean(params.get(QUERY_KEYS[i])); - if (channel) return channel; - } - } catch (e) {} - return ""; - } - - function syncTracker(channel) { - try { - if (global.tracker && typeof global.tracker.setChannel === "function") { - global.tracker.setChannel(channel); - } - } catch (e) {} - } - - function capture() { - // localStorage 与服务端 cookie 都属于已归因结果,必须先于当前 URL。 - var channel = readStorage() || readCookie() || readQuery(); - if (!channel) return ""; - - writeStorage(channel); - writeCookie(channel); - global.__CHANNEL_CODE__ = channel; - syncTracker(channel); - return channel; - } - - global.ChannelAttribution = { - get: capture, - capture: capture, - }; - - var channel = capture(); - try { - document.dispatchEvent( - new CustomEvent("channel:ready", { detail: { channel: channel } }) - ); - } catch (e) {} -})(window); diff --git a/app/static/js/common/cookie.js b/app/static/js/common/cookie.js deleted file mode 100644 index 053cd93..0000000 --- a/app/static/js/common/cookie.js +++ /dev/null @@ -1,204 +0,0 @@ -/** - * Cookie 读写(登录 token / sid 等统一落 cookie) - */ -(function (global) { - function encode(value) { - return encodeURIComponent(String(value == null ? "" : value)); - } - - function decode(value) { - try { - // 兼容服务端历史 QueryEscape:空格被编成 "+",decodeURIComponent 不会还原 - return decodeURIComponent(String(value || "").replace(/\+/g, "%20")); - } catch (e) { - return value; - } - } - - function getCookie(name) { - if (!name) return ""; - var key = encodeURIComponent(name) + "="; - var parts = String(document.cookie || "").split(";"); - for (var i = 0; i < parts.length; i++) { - var part = parts[i].replace(/^\s+/, ""); - if (part.indexOf(key) === 0) { - return decode(part.slice(key.length)); - } - } - return ""; - } - - /** - * @param {string} name - * @param {string} value - * @param {{ days?: number, path?: string, sameSite?: string, secure?: boolean }} [options] - */ - function setCookie(name, value, options) { - options = options || {}; - var days = options.days; - var path = options.path || "/"; - var sameSite = options.sameSite || "Lax"; - var parts = [ - encodeURIComponent(name) + "=" + encode(value), - "path=" + path, - "SameSite=" + sameSite, - ]; - if (typeof days === "number") { - var expires = new Date(); - expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); - parts.push("expires=" + expires.toUTCString()); - } - if (options.secure || (global.location && global.location.protocol === "https:")) { - parts.push("Secure"); - } - document.cookie = parts.join("; "); - } - - function removeCookie(name, path) { - setCookie(name, "", { days: -1, path: path || "/" }); - } - - var AUTH = { - TOKEN: "token", - SID: "sid", - S_TOKEN: "_s_token_", - USER_ID: "userId", - ACCOUNT_NO: "accountNo", - NICK_NAME: "nickName", - INTRODUCTION: "introduction", - AVATAR: "avatar", - EMAIL: "email", - INVITE_CODE: "inviteCode", - RECHARGE: "recharge", - VIP_TYPE: "vipType", - VIP_EXPIRE: "currentVipExpire", - SHOW_CARD_ID: "showCardId", - SHOW_CARD_NAME: "showCardName", - USER_TYPE: "userType", - IS_REGISTER: "isRegister", - }; - - var AUTH_KEYS = [ - AUTH.TOKEN, - AUTH.USER_ID, - AUTH.ACCOUNT_NO, - AUTH.NICK_NAME, - AUTH.INTRODUCTION, - AUTH.AVATAR, - AUTH.EMAIL, - AUTH.INVITE_CODE, - AUTH.RECHARGE, - AUTH.VIP_TYPE, - AUTH.VIP_EXPIRE, - AUTH.SHOW_CARD_ID, - AUTH.SHOW_CARD_NAME, - AUTH.USER_TYPE, - AUTH.IS_REGISTER, - "_token_", - AUTH.SID, - AUTH.S_TOKEN, - ]; - - function getToken() { - return getCookie(AUTH.TOKEN) || getCookie("_token_") || ""; - } - - function setToken(token, days) { - setCookie(AUTH.TOKEN, token || "", { days: typeof days === "number" ? days : 7 }); - } - - function getSid() { - return getCookie(AUTH.SID) || ""; - } - - function setSid(sid, days) { - setCookie(AUTH.SID, sid || "", { days: typeof days === "number" ? days : 7 }); - } - - function setAuthSession(user, days) { - if (!user) return; - var d = typeof days === "number" ? days : 7; - if (user.token) setToken(user.token, d); - if (user.userId != null) setCookie(AUTH.USER_ID, String(user.userId), { days: d }); - if (user.accountNo) setCookie(AUTH.ACCOUNT_NO, user.accountNo, { days: d }); - if (user.nickName) setCookie(AUTH.NICK_NAME, user.nickName, { days: d }); - if (user.introduction != null) setCookie(AUTH.INTRODUCTION, user.introduction || "", { days: d }); - if (user.avatar != null) setCookie(AUTH.AVATAR, user.avatar || "", { days: d }); - if (user.email != null) setCookie(AUTH.EMAIL, user.email || "", { days: d }); - if (user.inviteCode != null) setCookie(AUTH.INVITE_CODE, user.inviteCode || "", { days: d }); - // 仅在响应显式带 recharge 时写入(含 0),避免局部更新把余额抹成 0 - if (Object.prototype.hasOwnProperty.call(user, "recharge")) { - setCookie(AUTH.RECHARGE, String(Number(user.recharge) || 0), { days: d }); - } - if (user.vipType != null) setCookie(AUTH.VIP_TYPE, String(user.vipType), { days: d }); - if (user.currentVipExpire != null) { - setCookie(AUTH.VIP_EXPIRE, user.currentVipExpire || "", { days: d }); - } - if (Object.prototype.hasOwnProperty.call(user, "showCardId")) { - setCookie(AUTH.SHOW_CARD_ID, String(user.showCardId != null ? user.showCardId : ""), { days: d }); - } - if (Object.prototype.hasOwnProperty.call(user, "showCardName")) { - setCookie(AUTH.SHOW_CARD_NAME, user.showCardName || "", { days: d }); - } - if (user.type != null) setCookie(AUTH.USER_TYPE, String(user.type), { days: d }); - if (Object.prototype.hasOwnProperty.call(user, "isRegister") || user.type != null) { - var registered = !!(user.isRegister || Number(user.type) === 1); - setCookie(AUTH.IS_REGISTER, registered ? "1" : "0", { days: d }); - } - } - - function getAuthSession() { - var token = getToken(); - if (!token) return null; - return { - token: token, - userId: getCookie(AUTH.USER_ID), - accountNo: getCookie(AUTH.ACCOUNT_NO), - nickName: getCookie(AUTH.NICK_NAME), - introduction: getCookie(AUTH.INTRODUCTION), - avatar: getCookie(AUTH.AVATAR), - email: getCookie(AUTH.EMAIL), - inviteCode: getCookie(AUTH.INVITE_CODE), - recharge: getCookie(AUTH.RECHARGE), - vipType: getCookie(AUTH.VIP_TYPE), - currentVipExpire: getCookie(AUTH.VIP_EXPIRE), - showCardId: getCookie(AUTH.SHOW_CARD_ID), - showCardName: getCookie(AUTH.SHOW_CARD_NAME), - type: getCookie(AUTH.USER_TYPE), - isRegister: getCookie(AUTH.IS_REGISTER) === "1", - }; - } - - function clearAuth() { - for (var i = 0; i < AUTH_KEYS.length; i++) { - removeCookie(AUTH_KEYS[i]); - } - } - - function isLoggedIn() { - return !!getToken(); - } - - /** 是否正式注册账号(非游客) */ - function isRegistered() { - if (!isLoggedIn()) return false; - if (getCookie(AUTH.IS_REGISTER) === "1") return true; - return Number(getCookie(AUTH.USER_TYPE)) === 1; - } - - global.CookieUtil = { - get: getCookie, - set: setCookie, - remove: removeCookie, - AUTH: AUTH, - getToken: getToken, - setToken: setToken, - getSid: getSid, - setSid: setSid, - setAuthSession: setAuthSession, - getAuthSession: getAuthSession, - clearAuth: clearAuth, - isLoggedIn: isLoggedIn, - isRegistered: isRegistered, - }; -})(typeof window !== "undefined" ? window : this); diff --git a/app/static/js/common/request.js b/app/static/js/common/request.js index fa5c681..73f3517 100644 --- a/app/static/js/common/request.js +++ b/app/static/js/common/request.js @@ -1,160 +1,10 @@ /** * 前端请求封装。 - * - 登录态走 cookie(credentials: include) * - 浏览器无法改写 User-Agent;X-Real-IP 由服务端反代上游时注入 - * - 同源业务请求携带 SDK 设备字段;短剧服务端事件额外携带完整公共字段快照 - * - token 重试复用同一份 SDK Header、埋点上下文和 request_id - * - request_id 只用于请求关联;最终 event_id 由服务端按每条事件的最终字段生成 * - 直连上游时若需自定义头,请走本站 Go 代理接口 */ (function (global) { var DEFAULT_TIMEOUT_MS = 30000; - var TRACKER_CONTEXT_TIMEOUT_MS = 1500; - var DRAMA_SERVER_EVENT_ROUTES = { - "/api/playlet/chapter/like/set": "drama_like", - "/api/playlet/like/set": "drama_like", - "/api/playlet/chapter/collect/set": "drama_collect", - "/api/playlet/collect/set": "drama_collect", - "/api/comment/send": "drama_comment", - "/api/playlet/chapter/unlock": "drama_purchase", - }; - - function randomRequestId() { - var bytes = new Uint8Array(16); - if (global.crypto && typeof global.crypto.getRandomValues === "function") { - global.crypto.getRandomValues(bytes); - return Array.prototype.map.call(bytes, function (value) { - return (value + 256).toString(16).slice(-2); - }).join(""); - } - var fallback = (Date.now().toString(16) + Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)).slice(0, 32); - while (fallback.length < 32) fallback += "0"; - return fallback; - } - - function cleanHeaderPart(value) { - return String(value == null ? "" : value).replace(/[^\x20-\x7E]|[;=]/g, " ").trim(); - } - - function sameOrigin(url) { - try { - return new URL(url, global.location.href).origin === global.location.origin; - } catch (error) { - return true; - } - } - - function dramaServerEvent(url, method, options) { - if (method === "GET" || method === "HEAD" || !sameOrigin(url)) return ""; - if (options.trackerEvent === false) return ""; - if (typeof options.trackerEvent === "string") { - var explicit = options.trackerEvent.trim(); - return /^(drama_like|drama_comment|drama_collect|drama_purchase)$/.test(explicit) - ? explicit - : ""; - } - try { - return DRAMA_SERVER_EVENT_ROUTES[new URL(url, global.location.href).pathname] || ""; - } catch (error) { - return ""; - } - } - - function commonFields(options) { - if (options._trackerCommonFieldsPromise) return options._trackerCommonFieldsPromise; - var getter = global.tracker && typeof global.tracker.getCommonFields === "function" - ? global.tracker.getCommonFields - : global.WebSDK && typeof global.WebSDK.getCommonFields === "function" - ? global.WebSDK.getCommonFields.bind(global.WebSDK) - : null; - options._trackerCommonFieldsPromise = getter - ? Promise.resolve().then(function () { return getter(); }).catch(function () { return {}; }) - : Promise.resolve({}); - return options._trackerCommonFieldsPromise; - } - - function currentDramaContext(options) { - var context; - if (options && options.trackerContext && typeof options.trackerContext === "object") { - context = options.trackerContext; - } else if (global.tracker && typeof global.tracker.getDramaContext === "function") { - context = global.tracker.getDramaContext() || {}; - } else { - context = {}; - } - return { - media_id: String(context.media_id == null ? "" : context.media_id).trim(), - drama_id: String(context.drama_id == null ? "" : context.drama_id).trim(), - episode: Math.max(1, parseInt(context.episode, 10) || 1), - video_id: String(context.video_id == null ? "" : context.video_id).trim(), - recommend_trace_id: String(context.recommend_trace_id == null ? "" : context.recommend_trace_id).trim(), - }; - } - - function base64URLJSON(value) { - try { - var json = JSON.stringify(value); - var bytes = encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, function (_, hex) { - return String.fromCharCode(parseInt(hex, 16)); - }); - return global.btoa(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); - } catch (error) { - return ""; - } - } - - function trackerHeaders(url, method, options) { - var eventName = dramaServerEvent(url, method, options); - if (!eventName) return Promise.resolve({}); - if (!options._trackerContextPromise) { - if (!options._trackerRequestId) options._trackerRequestId = randomRequestId(); - var drama = currentDramaContext(options); - if (!drama || !String(drama.drama_id || "").trim()) { - options._trackerContextPromise = Promise.resolve({}); - } else { - options._trackerContextPromise = commonFields(options).then(function (fields) { - var packedContext = base64URLJSON({ - v: 1, - request_id: options._trackerRequestId, - event_name: eventName, - common: fields, - drama: drama, - }); - if (!packedContext) return {}; - var result = { - "X-Tracker-Request-Id": options._trackerRequestId, - "X-Tracker-Event-Name": eventName, - "X-DC-Request-Context": packedContext, - }; - result["X-User-Agent"] = [ - "DevID=" + cleanHeaderPart(fields.device_id), - "DevType=" + cleanHeaderPart(fields.device || "OTHER"), - "SysType=" + (fields.device === "Android" ? "h5_android" : fields.device === "iOS" ? "h5_ios" : "h5_pc"), - "Ver=" + cleanHeaderPart(fields.app_version || "1.0.0"), - "BuildID=web", - "device_brand=" + cleanHeaderPart(fields.device_brand), - "device_model=" + cleanHeaderPart(fields.device_model), - "sid=" + cleanHeaderPart(fields.sid), - ].join(";"); - return result; - }); - } - } - return new Promise(function (resolve) { - var settled = false; - var timer = global.setTimeout(function () { - if (settled) return; - settled = true; - resolve({}); - }, TRACKER_CONTEXT_TIMEOUT_MS); - options._trackerContextPromise.then(function (headers) { - if (settled) return; - settled = true; - global.clearTimeout(timer); - resolve(headers); - }); - }); - } function buildQuery(params) { if (!params) return ""; @@ -175,8 +25,6 @@ * params?: Record, * headers?: Record, * timeout?: number, - * trackerEvent?: false|string, - * trackerContext?: Record, * }} [options] */ function request(url, options) { @@ -206,52 +54,20 @@ var timeoutMs = typeof options.timeout === "number" ? options.timeout : DEFAULT_TIMEOUT_MS; - function sdkCommonHeader() { - if (headers["X-SDK-Common"] || options._skipSDKCommon || !sameOrigin(finalUrl)) { - return Promise.resolve(""); - } - if (!options._sdkCommonHeaderPromise) { - options._sdkCommonHeaderPromise = Promise.resolve() - .then(function () { - if ( - global.tracker && - typeof global.tracker.getSDKCommonHeader === "function" - ) { - return global.tracker.getSDKCommonHeader(); - } - return ""; - }) - .catch(function () { return ""; }); - } - return options._sdkCommonHeaderPromise; + var fetchOpts = { + method: method, + headers: headers, + credentials: "include", + signal: controller ? controller.signal : undefined, + }; + if (body != null) fetchOpts.body = body; + if (controller && timeoutMs > 0) { + timer = setTimeout(function () { + controller.abort(); + }, timeoutMs); } - return Promise.all([ - trackerHeaders(finalUrl, method, options).catch(function () { return {}; }), - sdkCommonHeader(), - ]) - .then(function (prepared) { - var tracking = prepared[0] || {}; - Object.keys(tracking).forEach(function (key) { - if (headers[key] == null) headers[key] = tracking[key]; - }); - if (!headers["X-SDK-Common"] && prepared[1]) { - headers["X-SDK-Common"] = prepared[1]; - } - var fetchOpts = { - method: method, - headers: headers, - credentials: "include", - signal: controller ? controller.signal : undefined, - }; - if (body != null) fetchOpts.body = body; - if (controller && timeoutMs > 0) { - timer = setTimeout(function () { - controller.abort(); - }, timeoutMs); - } - return fetch(finalUrl, fetchOpts); - }) + return fetch(finalUrl, fetchOpts) .then(function (res) { var contentType = res.headers.get("content-type") || ""; var parse = contentType.indexOf("application/json") >= 0 @@ -270,49 +86,11 @@ return data; }); }) - .then(function (data) { - return handleTokenExpiredResponse(url, options, data); - }) .finally(function () { if (timer) clearTimeout(timer); }); } - /** - * 业务码 1000:token 失效。 - * 游客 → 重新游客登录并重试一次;注册账号 → 打开登录弹窗/跳转登录页。 - */ - function handleTokenExpiredResponse(url, options, data) { - options = options || {}; - var expiredCode = - (global.AuthAPI && global.AuthAPI.TOKEN_EXPIRED_CODE) || 1000; - if (!data || Number(data.code) !== Number(expiredCode)) { - return data; - } - if (options._skipTokenRecovery || options._tokenRetried) { - return data; - } - if ( - global.AuthAPI && - typeof global.AuthAPI.isAuthEndpoint === "function" && - global.AuthAPI.isAuthEndpoint(url) - ) { - return data; - } - if (!global.AuthAPI || typeof global.AuthAPI.handleTokenExpired !== "function") { - return data; - } - return global.AuthAPI.handleTokenExpired().then(function (result) { - if (result && result.ok && result.guest) { - return request( - url, - Object.assign({}, options, { _tokenRetried: true }) - ); - } - return data; - }); - } - global.HttpRequest = { request: request, get: function (url, options) { diff --git a/app/static/js/favorites-manage.js b/app/static/js/favorites-manage.js deleted file mode 100644 index ef9bd17..0000000 --- a/app/static/js/favorites-manage.js +++ /dev/null @@ -1,56 +0,0 @@ -(() => { - const root = document.querySelector('[data-favorites]'); - const manage = root?.querySelector('[data-favorites-manage]'); - if (!manage) return; - - manage.addEventListener('click', () => { - const active = document.body.classList.toggle('is-managing'); - manage.setAttribute('aria-pressed', String(active)); - manage.querySelector('span').textContent = active ? '完成' : '管理'; - }); - - root.querySelectorAll('[data-favorites-remove]').forEach(button => { - button.addEventListener('click', async (event) => { - event.preventDefault(); - event.stopPropagation(); - if (button.disabled) return; - button.disabled = true; - const kind = button.dataset.kind || 'short'; - const id = Number(button.dataset.id); - let url = '/api/playlet/collect/set'; - let body = {id, isCollect: false}; - if (kind === 'chapter') { - url = '/api/playlet/chapter/collect/set'; - body = {chapterId: id, isCollect: false}; - } else if (kind === 'series') { - url = '/api/video/collect/set'; - body = {id, isCollect: false}; - } - const trackerContext = kind === 'series' ? null : { - media_id: '', - drama_id: String(button.dataset.dramaId || ''), - drama_title: String(button.dataset.dramaTitle || ''), - episode: Math.max(1, Number(button.dataset.episode) || 1), - total_episode: Math.max(1, Number(button.dataset.totalEpisode) || 1), - video_id: String(button.dataset.videoId || ''), - drama_content_type: 'short_drama', - recommend_trace_id: '', - }; - try { - if (!window.HttpRequest || typeof window.HttpRequest.post !== 'function') { - throw new Error('请求服务未就绪'); - } - const result = await window.HttpRequest.post( - url, - body, - trackerContext ? {trackerContext} : undefined, - ); - if (!result || Number(result.code) !== 200) throw new Error((result && (result.tip || result.msg)) || '取消收藏失败'); - window.location.reload(); - } catch (_) { - button.disabled = false; - window.alert('操作失败,请稍后重试'); - } - }); - }); -})(); diff --git a/app/static/js/favorites.js b/app/static/js/favorites.js deleted file mode 100644 index d567331..0000000 --- a/app/static/js/favorites.js +++ /dev/null @@ -1,111 +0,0 @@ -(() => { - const root = document.querySelector('[data-favorites]'); - if (!root) return; - - const cards = Array.from(root.querySelectorAll('[data-favorites-card]')); - const filters = Array.from(root.querySelectorAll('[data-favorites-filter]')); - const manage = root.querySelector('[data-favorites-manage]'); - const manageLabel = manage ? manage.querySelector('span') : null; - const toolbar = root.querySelector('[data-favorites-toolbar]'); - const grid = root.querySelector('[data-favorites-grid]'); - const empty = root.querySelector('[data-favorites-empty]'); - const filterEmpty = root.querySelector('[data-favorites-filter-empty]'); - const recommendations = root.querySelector('[data-favorites-recommendations]'); - const loadMore = root.querySelector('[data-favorites-more]'); - const pagination = root.querySelector('[data-favorites-pagination]'); - const pages = root.querySelector('[data-favorites-pages]'); - const previous = root.querySelector('[data-favorites-prev]'); - const next = root.querySelector('[data-favorites-next]'); - const count = root.querySelector('[data-favorites-count]'); - const mobile = window.matchMedia('(max-width: 768px)'); - let activeFilter = 'all'; - let currentPage = 1; - let mobileVisible = 9; - - function matches(card) { - if (card.dataset.removed === 'true') return false; - if (activeFilter === 'following') return card.dataset.following === 'true'; - if (activeFilter === 'complete') return card.dataset.complete === 'true'; - return activeFilter === 'all' || card.dataset.kind === activeFilter; - } - - function render() { - const hasSavedWorks = cards.some(card => card.dataset.removed !== 'true'); - const collectionEmpty = !hasSavedWorks; - const filtered = cards.filter(matches); - const pageCount = Math.ceil(filtered.length / 12); - currentPage = Math.min(currentPage, Math.max(1, pageCount)); - const start = mobile.matches ? 0 : (currentPage - 1) * 12; - const end = mobile.matches ? mobileVisible : start + 12; - - cards.forEach(card => { card.hidden = true; }); - filtered.slice(start, end).forEach(card => { card.hidden = false; }); - root.classList.toggle('is-empty', collectionEmpty); - toolbar.hidden = collectionEmpty; - grid.hidden = collectionEmpty; - empty.hidden = !collectionEmpty; - if (recommendations) recommendations.hidden = !collectionEmpty; - filterEmpty.hidden = collectionEmpty || filtered.length !== 0; - if (collectionEmpty) { - document.body.classList.remove('is-managing'); - if (manage) manage.setAttribute('aria-pressed', 'false'); - if (manageLabel) manageLabel.textContent = '管理'; - } - loadMore.hidden = filtered.length === 0 || mobileVisible >= filtered.length; - pagination.hidden = filtered.length === 0; - count.textContent = `共 ${filtered.length} 部`; - previous.disabled = currentPage <= 1; - next.disabled = currentPage >= pageCount; - - pages.replaceChildren(); - for (let page = 1; page <= pageCount; page++) { - const button = document.createElement('button'); - button.className = `favorites-pagination__button${page === currentPage ? ' is-active' : ''}`; - button.type = 'button'; - button.textContent = String(page); - button.dataset.favoritesPage = String(page); - if (page === currentPage) button.setAttribute('aria-current', 'page'); - button.addEventListener('click', () => { - currentPage = page; - render(); - }); - pages.append(button); - } - } - - filters.forEach(filter => filter.addEventListener('click', () => { - activeFilter = filter.dataset.favoritesFilter; - currentPage = 1; - mobileVisible = 9; - filters.forEach(option => { - const selected = option === filter; - option.classList.toggle('is-active', selected); - option.setAttribute('aria-pressed', String(selected)); - }); - render(); - })); - - if (manage) manage.addEventListener('click', () => { - const managing = document.body.classList.toggle('is-managing'); - manage.setAttribute('aria-pressed', String(managing)); - manageLabel.textContent = managing ? '完成' : '管理'; - }); - - root.querySelectorAll('[data-favorites-remove]').forEach(button => button.addEventListener('click', () => { - button.closest('[data-favorites-card]').dataset.removed = 'true'; - render(); - })); - - loadMore.addEventListener('click', () => { - mobileVisible += 9; - render(); - }); - previous.addEventListener('click', () => { - if (currentPage > 1) { currentPage--; render(); } - }); - next.addEventListener('click', () => { - if (currentPage < Math.ceil(cards.filter(matches).length / 12)) { currentPage++; render(); } - }); - mobile.addEventListener('change', render); - render(); -})(); diff --git a/app/static/js/header.js b/app/static/js/header.js index 1f6043f..312de27 100644 --- a/app/static/js/header.js +++ b/app/static/js/header.js @@ -2,7 +2,6 @@ var header = document.querySelector(".site-header"); var drawer = document.querySelector("[data-drawer]"); var openBtn = document.querySelector("[data-drawer-open]"); - var userMenu = document.querySelector("[data-user-menu]"); if (header) { var threshold = 48; @@ -30,44 +29,6 @@ if ("ResizeObserver" in window) new ResizeObserver(measure).observe(header); } - // 用户下拉菜单 - if (userMenu) { - var toggle = userMenu.querySelector("[data-user-menu-toggle]"); - - function openUserMenu() { - userMenu.classList.add("is-open"); - if (toggle) toggle.setAttribute("aria-expanded", "true"); - } - - function closeUserMenu() { - userMenu.classList.remove("is-open"); - if (toggle) toggle.setAttribute("aria-expanded", "false"); - } - - function toggleUserMenu(e) { - e.stopPropagation(); - if (userMenu.classList.contains("is-open")) { - closeUserMenu(); - } else { - openUserMenu(); - } - } - - if (toggle) { - toggle.addEventListener("click", toggleUserMenu); - } - - document.addEventListener("click", function (e) { - if (!userMenu.contains(e.target)) { - closeUserMenu(); - } - }); - - document.addEventListener("keydown", function (e) { - if (e.key === "Escape") closeUserMenu(); - }); - } - // 左侧抽屉 if (drawer && openBtn) { var closeEls = drawer.querySelectorAll("[data-drawer-close]"); @@ -99,35 +60,6 @@ }); } - function openAccountSwitch(event) { - if (event) event.preventDefault(); - - if (userMenu) { - userMenu.classList.remove("is-open"); - var userToggle = userMenu.querySelector("[data-user-menu-toggle]"); - if (userToggle) userToggle.setAttribute("aria-expanded", "false"); - } - if (drawer) { - drawer.classList.remove("is-open"); - document.body.classList.remove("drawer-open"); - if (openBtn) openBtn.setAttribute("aria-expanded", "false"); - } - - if (typeof window.openAccountSwitchLogin === "function") { - window.openAccountSwitchLogin(); - return; - } - if (typeof window.openLoginPop === "function") { - window.openLoginPop(); - return; - } - window.location.href = "/login"; - } - - document.querySelectorAll("[data-account-switch]").forEach(function (btn) { - btn.addEventListener("click", openAccountSwitch); - }); - // 搜索表单:走路径 /search/{q},不用 query;同时写入本地搜索历史 var SEARCH_HISTORY_KEY = "hgdj_search_history"; var SEARCH_HISTORY_MAX = 10; @@ -526,108 +458,4 @@ closeDropdown(); }); }); - - var PLACEHOLDER = "/static/images/placeholder.png?v=2"; - var DEFAULT_AVATAR = "/static/images/profile/avatar.png"; - - function usableAvatar(url) { - var u = String(url || "").trim(); - if (!u) return ""; - if (/^https?:\/\//i.test(u) || u.indexOf("/static/") === 0) return u; - return ""; - } - - function setAvatarImg(img, url) { - if (!img) return; - var src = usableAvatar(url); - img.removeAttribute("data-decrypted"); - img.removeAttribute("data-decrypt-failed"); - if (src && typeof window.shouldDecryptImage === "function" && window.shouldDecryptImage(src)) { - img.classList.add("lazy"); - img.src = PLACEHOLDER; - img.setAttribute("data-src", src); - img.setAttribute("data-lazy-eager", "1"); - } else { - img.classList.remove("lazy"); - img.removeAttribute("data-src"); - img.removeAttribute("data-lazy-eager"); - img.src = src || DEFAULT_AVATAR; - } - } - - function refreshAvatarDecrypt(root) { - var scope = root || document; - if (typeof window.initLazyImages === "function") { - window.initLazyImages(scope, { eager: true }); - return; - } - if (typeof window.loadLazyNode !== "function") return; - scope - .querySelectorAll( - "img.user-chip__avatar.lazy[data-src], img.user-menu__avatar.lazy[data-src], img.drawer__avatar.lazy[data-src], img.play-composer__avatar-img.lazy[data-src]" - ) - .forEach(function (img) { - window.loadLazyNode(img); - }); - } - - function formatRecharge(raw) { - var n = Math.floor(Number(raw)); - if (!isFinite(n) || n < 0) n = 0; - var s = String(n); - if (s.length <= 3) return s; - var out = []; - var rem = s.length % 3; - if (rem) out.push(s.slice(0, rem)); - for (var i = rem; i < s.length; i += 3) out.push(s.slice(i, i + 3)); - return out.join(","); - } - - function syncHeaderUser() { - var session = - window.CookieUtil && typeof CookieUtil.getAuthSession === "function" - ? CookieUtil.getAuthSession() - : null; - if (!session) return; - - var avatar = usableAvatar(session.avatar); - document.querySelectorAll("[data-header-avatar]").forEach(function (img) { - if (avatar) setAvatarImg(img, avatar); - }); - - var name = String(session.nickName || session.accountNo || "").trim(); - if (name) { - document.querySelectorAll(".user-chip__name, .user-menu__meta strong, .drawer__meta strong").forEach(function (el) { - el.textContent = name; - }); - } - - if (session.userId) { - document.querySelectorAll(".user-menu__meta span, .drawer__meta span").forEach(function (el) { - if (/^ID\b/.test(String(el.textContent || "").trim())) { - el.textContent = "ID " + session.userId; - } - }); - } - - if (Object.prototype.hasOwnProperty.call(session, "recharge") && session.recharge !== "") { - var text = formatRecharge(session.recharge); - document.querySelectorAll("[data-header-recharge]").forEach(function (el) { - el.textContent = text; - }); - } - - refreshAvatarDecrypt(document); - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", syncHeaderUser); - } else { - syncHeaderUser(); - } - document.addEventListener("auth:login", syncHeaderUser); - document.addEventListener("auth:logout", syncHeaderUser); - window.refreshAvatarDecrypt = refreshAvatarDecrypt; - window.setAvatarImg = setAvatarImg; - window.syncHeaderUser = syncHeaderUser; })(); diff --git a/app/static/js/history.js b/app/static/js/history.js deleted file mode 100644 index 4ac9d73..0000000 --- a/app/static/js/history.js +++ /dev/null @@ -1,115 +0,0 @@ -(() => { - const root = document.querySelector('[data-history]'); - if (!root) return; - - const filters = Array.from(root.querySelectorAll('[data-history-filter]')); - const groups = Array.from(root.querySelectorAll('[data-history-group]')); - const items = Array.from(root.querySelectorAll('[data-history-item]')); - const empty = root.querySelector('[data-history-empty]'); - const clear = root.querySelector('[data-history-clear]'); - let activeKind = 'all'; - - async function postJSON(url, body) { - if (window.HttpRequest && typeof window.HttpRequest.post === 'function') { - return window.HttpRequest.post(url, body || {}); - } - const response = await fetch(url, { - method: 'POST', - credentials: 'same-origin', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(body || {}), - }); - return response.json(); - } - - function update() { - root.dataset.activeKind = activeKind; - items.forEach(item => { - item.hidden = item.dataset.removed === 'true' || (activeKind !== 'all' && item.dataset.kind !== activeKind); - }); - groups.forEach(group => { - group.hidden = !Array.from(group.querySelectorAll('[data-history-item]')).some(item => - !item.hidden && getComputedStyle(item).display !== 'none' - ); - }); - const hasVisible = groups.some(group => !group.hidden); - if (empty) empty.hidden = hasVisible; - if (clear) { - clear.hidden = items.length === 0 || items.every(item => item.dataset.removed === 'true'); - clear.disabled = clear.hidden; - } - filters.forEach(filter => { - const selected = filter.dataset.historyFilter === activeKind; - filter.classList.toggle('is-active', selected); - filter.setAttribute('aria-pressed', String(selected)); - }); - } - - filters.forEach(filter => filter.addEventListener('click', () => { - activeKind = filter.dataset.historyFilter; - update(); - })); - - root.querySelectorAll('[data-history-remove]').forEach(button => button.addEventListener('click', async (event) => { - event.preventDefault(); - event.stopPropagation(); - if (button.disabled) return; - const item = button.closest('[data-history-item]'); - if (!item) return; - const id = Number(item.dataset.id); - const kind = item.dataset.kind || 'short'; - if (!id) { - item.dataset.removed = 'true'; - update(); - return; - } - button.disabled = true; - try { - let result; - if (kind === 'series') { - result = await postJSON('/api/video/history/delete', {videoId: id}); - } else { - result = await postJSON('/api/playlet/history/delete', {playletId: id}); - } - if (!result || Number(result.code) !== 200) { - throw new Error((result && (result.tip || result.msg)) || '删除失败'); - } - item.dataset.removed = 'true'; - update(); - } catch (error) { - window.alert(error.message || '删除失败,请稍后重试'); - button.disabled = false; - } - })); - - if (clear) { - clear.addEventListener('click', async () => { - if (clear.disabled) return; - if (!window.confirm('确定清空全部观看历史?')) return; - clear.disabled = true; - try { - const [playletResult, videoResult] = await Promise.all([ - postJSON('/api/playlet/history/clear', {}), - postJSON('/api/video/history/clear', {}), - ]); - const playletOk = playletResult && Number(playletResult.code) === 200; - const videoOk = videoResult && Number(videoResult.code) === 200; - if (!playletOk && !videoOk) { - throw new Error( - (videoResult && (videoResult.tip || videoResult.msg)) || - (playletResult && (playletResult.tip || playletResult.msg)) || - '清空失败' - ); - } - items.forEach(item => { item.dataset.removed = 'true'; }); - update(); - window.location.reload(); - } catch (error) { - window.alert(error.message || '清空失败,请稍后重试'); - clear.disabled = false; - } - }); - } - window.addEventListener('resize', update); - update(); -})(); diff --git a/app/static/js/pages/dialogAd.js b/app/static/js/pages/dialogAd.js index fc1a6af..5633aa3 100644 --- a/app/static/js/pages/dialogAd.js +++ b/app/static/js/pages/dialogAd.js @@ -75,21 +75,6 @@ var displayedIndex = -1; var loadGeneration = 0; - function emitTrack(item, eventType) { - if (!item || eventType === "close") return; - if (window.tracker && typeof window.tracker.externalAd === "function") { - window.tracker.externalAd(item, eventType); - return; - } - try { - document.dispatchEvent(new CustomEvent("external-ad:track", { - detail: { element: item, eventType: eventType }, - })); - } catch (error) { - // 埋点不可用时不影响广告展示与关闭。 - } - } - function hideAllItems() { items.forEach(function (item) { item.hidden = true; @@ -118,7 +103,6 @@ var item = displayedIndex >= 0 ? items[displayedIndex] : null; if (item && !item.__dialogAdShowTracked) { item.__dialogAdShowTracked = true; - emitTrack(item, "show"); } } @@ -239,18 +223,13 @@ if (!item || item.hidden || !mask.contains(item)) return; var index = items.indexOf(item); if (index < 0 || index !== displayedIndex) return; - if (event.target.closest("[data-dialog-ad-link]")) { - emitTrack(item, "click"); - } if (event.target.closest("[data-dialog-ad-close]")) { - emitTrack(item, "close"); showItem(index + 1); } }); document.addEventListener("keydown", function (event) { if (event.key === "Escape" && mask.classList.contains("is-open") && displayedIndex >= 0) { - emitTrack(items[displayedIndex], "close"); showNextItem(); } }); diff --git a/app/static/js/pages/feedback.js b/app/static/js/pages/feedback.js index 691298b..76a3b8a 100644 --- a/app/static/js/pages/feedback.js +++ b/app/static/js/pages/feedback.js @@ -37,10 +37,6 @@ window.showToast(message, type); return; } - if (window.AuthAPI && typeof window.AuthAPI.toast === "function") { - window.AuthAPI.toast(message, type || "info"); - return; - } window.alert(message); } diff --git a/app/static/js/pages/login.js b/app/static/js/pages/login.js deleted file mode 100644 index 49a29c3..0000000 --- a/app/static/js/pages/login.js +++ /dev/null @@ -1,137 +0,0 @@ -(function () { - var form = document.querySelector("[data-login-page-form]"); - var password = document.querySelector("[data-login-page-password]"); - var toggle = document.querySelector("[data-login-page-toggle]"); - var toggleIcon = document.querySelector("[data-login-page-toggle-icon]"); - var submitBtn = document.querySelector("[data-login-page-submit]") || (form && form.querySelector('button[type="submit"]')); - var root = form || document; - - if (toggle && password && toggleIcon) { - toggle.addEventListener("click", function () { - var show = password.getAttribute("type") === "password"; - password.setAttribute("type", show ? "text" : "password"); - toggleIcon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg"); - toggle.setAttribute("aria-label", show ? "隐藏密码" : "显示密码"); - }); - } - - function clearFieldError(name) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.remove("is-error"); - if (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - } - } - - function clearAllErrors() { - root.querySelectorAll("[data-field]").forEach(function (el) { - el.classList.remove("is-error"); - }); - root.querySelectorAll("[data-field-error]").forEach(function (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - }); - } - - function setFieldError(name, message) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.add("is-error"); - if (err) { - err.hidden = false; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = message || ""; - } - } - - function isLen4to16(value) { - var len = String(value || "").length; - return len >= 4 && len <= 16; - } - - function validateForm() { - clearAllErrors(); - var fd = new FormData(form); - var account = String(fd.get("account") || "").trim(); - var pwd = String(fd.get("password") || ""); - var valid = true; - - if (!account) { - setFieldError("account", "请输入账号"); - valid = false; - } else if ( - !(window.AuthAPI && AuthAPI.isValidLoginAccount - ? AuthAPI.isValidLoginAccount(account) - : isLen4to16(account) || account.indexOf("@") >= 0) - ) { - setFieldError("account", "账号需为 4–16 位,或合法邮箱"); - valid = false; - } - - if (!pwd) { - setFieldError("password", "请输入密码"); - valid = false; - } else if (!isLen4to16(pwd)) { - setFieldError("password", "密码需为 4–16 位"); - valid = false; - } - - if (!valid) return null; - return { account: account, password: pwd }; - } - - function mapServerError(err) { - var tip = (err && err.message) || "登录失败,请稍后重试"; - if (/账号|账户|用户|不存在|邮箱/.test(tip) && !/密码/.test(tip)) { - setFieldError("account", tip); - return; - } - setFieldError("password", tip); - } - - function setSubmitting(loading) { - if (!submitBtn) return; - submitBtn.disabled = !!loading; - submitBtn.classList.toggle("is-loading", !!loading); - submitBtn.textContent = loading ? "登录中…" : "登录"; - } - - if (form) { - form.querySelectorAll("input").forEach(function (input) { - input.addEventListener("input", function () { - var name = input.getAttribute("name"); - if (name) clearFieldError(name); - }); - }); - - form.addEventListener("submit", function (event) { - event.preventDefault(); - if (!window.AuthAPI || !window.HttpRequest) { - setFieldError("password", "认证脚本未加载,请刷新页面"); - return; - } - if (submitBtn && submitBtn.classList.contains("is-loading")) return; - - var payload = validateForm(); - if (!payload) return; - - setSubmitting(true); - AuthAPI.login(payload) - .then(function (res) { - AuthAPI.toast((res && res.tip) || "登录成功", "success"); - window.setTimeout(function () { - window.location.href = "/"; - }, 400); - }) - .catch(function (err) { - setSubmitting(false); - mapServerError(err); - AuthAPI.toast((err && err.message) || "登录失败", "error"); - }); - }); - } -})(); diff --git a/app/static/js/pages/loginPop.js b/app/static/js/pages/loginPop.js deleted file mode 100644 index b40f5eb..0000000 --- a/app/static/js/pages/loginPop.js +++ /dev/null @@ -1,252 +0,0 @@ -(function () { - var root = document.querySelector("[data-login-pop]"); - if (!root) return; - - var form = root.querySelector("[data-login-form]"); - var password = root.querySelector("[data-login-password]"); - var toggle = root.querySelector("[data-login-toggle]"); - var toggleIcon = root.querySelector("[data-login-toggle-icon]"); - var title = root.querySelector("[data-login-title]"); - var description = root.querySelector("[data-login-desc]"); - var submitBtn = root.querySelector("[data-login-submit]") || (form && form.querySelector('button[type="submit"]')); - var previousOverflow = ""; - var switchAccountMode = false; - - function closeRegister() { - if (typeof window.closeRegisterPop === "function") { - window.closeRegisterPop(); - return; - } - var register = document.querySelector("[data-register-pop]"); - if (register) register.classList.remove("is-open"); - } - - function closeUserMenu() { - var menu = document.querySelector("[data-user-menu].is-open"); - if (menu) { - menu.classList.remove("is-open"); - var t = menu.querySelector("[data-user-menu-toggle]"); - if (t) t.setAttribute("aria-expanded", "false"); - } - } - - function clearFieldError(name) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.remove("is-error"); - if (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - } - } - - function clearAllErrors() { - root.querySelectorAll("[data-field]").forEach(function (el) { - el.classList.remove("is-error"); - }); - root.querySelectorAll("[data-field-error]").forEach(function (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - }); - } - - function setFieldError(name, message) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.add("is-error"); - if (err) { - err.hidden = false; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = message || ""; - } - } - - function isLen4to16(value) { - var len = String(value || "").length; - return len >= 4 && len <= 16; - } - - function validateForm() { - clearAllErrors(); - var fd = new FormData(form); - var account = String(fd.get("account") || "").trim(); - var pwd = String(fd.get("password") || ""); - var valid = true; - - if (!account) { - setFieldError("account", "请输入账号"); - valid = false; - } else if ( - !(window.AuthAPI && AuthAPI.isValidLoginAccount - ? AuthAPI.isValidLoginAccount(account) - : isLen4to16(account) || account.indexOf("@") >= 0) - ) { - setFieldError("account", "账号需为 4–16 位,或合法邮箱"); - valid = false; - } - - if (!pwd) { - setFieldError("password", "请输入密码"); - valid = false; - } else if (!isLen4to16(pwd)) { - setFieldError("password", "密码需为 4–16 位"); - valid = false; - } - - if (!valid) return null; - return { account: account, password: pwd }; - } - - function mapServerError(err) { - var tip = (err && err.message) || "登录失败,请稍后重试"; - if (/账号|账户|用户|不存在|邮箱/.test(tip) && !/密码/.test(tip)) { - setFieldError("account", tip); - return; - } - setFieldError("password", tip); - } - - function isMobile() { - return window.matchMedia && window.matchMedia("(max-width: 768px)").matches; - } - - function open(input) { - var event = input && typeof input.preventDefault === "function" ? input : null; - var options = event ? {} : input || {}; - if (isMobile() && !options.forcePopup) { - if (event) event.preventDefault(); - window.location.href = "/login"; - return; - } - if (event) event.preventDefault(); - switchAccountMode = !!options.switchAccount; - closeRegister(); - closeUserMenu(); - clearAllErrors(); - if (options.resetForm && form) form.reset(); - if (password) password.setAttribute("type", "password"); - if (toggle) toggle.setAttribute("aria-label", "显示密码"); - if (toggleIcon) { - toggleIcon.setAttribute("src", "/static/images/eye.svg"); - toggleIcon.setAttribute("data-src", "/static/images/eye.svg"); - } - if (title) title.textContent = switchAccountMode ? "切换账号" : "登录黄果短剧"; - if (description) { - description.textContent = switchAccountMode - ? "请输入其他账号和密码完成切换" - : "登录后可收藏作品、解锁付费剧集并查看观看历史"; - } - root.classList.add("is-open"); - previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - var first = root.querySelector(".login-pop__input"); - if (first) { - window.setTimeout(function () { - first.focus(); - }, 0); - } - } - - function close() { - root.classList.remove("is-open"); - document.body.style.overflow = previousOverflow; - clearAllErrors(); - switchAccountMode = false; - } - - window.openLoginPop = open; - window.openAccountSwitchLogin = function () { - open({ forcePopup: true, switchAccount: true, resetForm: true }); - }; - window.closeLoginPop = close; - - document.addEventListener("click", function (event) { - var opener = event.target.closest("[data-login-open]"); - if (opener) { - open(event); - return; - } - - // PC:拦截站内 /login 链接,改为打开弹窗;H5 放行跳转登录页 - var loginLink = event.target.closest('a[href="/login"], a[href="/login/"], a[href="#login"]'); - if (loginLink && !isMobile()) { - open(event); - return; - } - - if (!root.classList.contains("is-open")) return; - if (event.target.closest("[data-login-close]")) { - close(); - } - }); - - if (toggle && password && toggleIcon) { - toggle.addEventListener("click", function () { - var show = password.getAttribute("type") === "password"; - password.setAttribute("type", show ? "text" : "password"); - toggleIcon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg"); - toggle.setAttribute("aria-label", show ? "隐藏密码" : "显示密码"); - }); - } - - function setSubmitting(loading) { - if (!submitBtn) return; - submitBtn.disabled = !!loading; - submitBtn.classList.toggle("is-loading", !!loading); - submitBtn.textContent = loading ? "登录中…" : "登录"; - } - - if (form) { - form.querySelectorAll("input").forEach(function (input) { - input.addEventListener("input", function () { - var name = input.getAttribute("name"); - if (name) clearFieldError(name); - }); - }); - - form.addEventListener("submit", function (event) { - event.preventDefault(); - if (!window.AuthAPI || !window.HttpRequest) { - setFieldError("password", "认证脚本未加载,请刷新页面"); - return; - } - if (submitBtn && submitBtn.classList.contains("is-loading")) return; - - var payload = validateForm(); - if (!payload) return; - - setSubmitting(true); - AuthAPI.login(payload) - .then(function (res) { - AuthAPI.toast((res && res.tip) || "登录成功", "success"); - window.setTimeout(function () { - close(); - window.location.reload(); - }, 400); - }) - .catch(function (err) { - setSubmitting(false); - mapServerError(err); - AuthAPI.toast((err && err.message) || "登录失败", "error"); - }); - }); - } - - document.addEventListener("keydown", function (event) { - if (event.key === "Escape" && root.classList.contains("is-open")) { - close(); - } - }); - - try { - var params = new URLSearchParams(window.location.search || ""); - if (params.get("auth") === "login") { - params.delete("auth"); - var qs = params.toString(); - window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : "") + window.location.hash); - open(); - } - } catch (e) {} -})(); diff --git a/app/static/js/pages/play.js b/app/static/js/pages/play.js index b12bb99..b37c619 100644 --- a/app/static/js/pages/play.js +++ b/app/static/js/pages/play.js @@ -452,7 +452,6 @@ } var playDp = null; - var lastWatchReportKey = ""; var currentPlayMode = "main"; var mainBadgePC = ""; var mainBadgeH5 = ""; @@ -515,32 +514,6 @@ if (label) label.textContent = mainCtaLabel || "立即播放"; } - function toIntAttr(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function reportChapterWatch() { - var root = document.querySelector("[data-play-player]"); - if (!root) return; - var chapterId = toIntAttr(root.getAttribute("data-chapter-id"), 0); - var playletId = toIntAttr(root.getAttribute("data-playlet-id"), 0); - if (chapterId <= 0) return; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - if (!window.CookieUtil.isLoggedIn()) return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") return; - var key = String(playletId || 0) + ":" + chapterId; - if (key === lastWatchReportKey) return; - lastWatchReportKey = key; - var body = { chapterId: chapterId }; - if (playletId > 0) body.playletId = playletId; - window.HttpRequest.post("/api/playlet/chapter/watch/report", body) - .catch(function () { - lastWatchReportKey = ""; - }); - } - function hidePosterOverlay() { var root = document.querySelector("[data-play-player]"); if (root) root.classList.add("is-playing"); @@ -571,20 +544,6 @@ } } - function ensureComposerAvatarDecrypt() { - var avatar = document.querySelector(".play-composer__avatar-img.lazy[data-src]"); - if (!avatar) return; - if (typeof window.loadLazyNode === "function") { - window.loadLazyNode(avatar); - return; - } - if (typeof window.refreshAvatarDecrypt === "function") { - window.refreshAvatarDecrypt(avatar.parentNode || document); - } else if (typeof window.initLazyImages === "function") { - window.initLazyImages(avatar.parentNode || document, { eager: true }); - } - } - function initDPlayer(initialURL) { if (typeof window.DPlayer === "undefined") return null; var container = document.querySelector(".dplayer-video"); @@ -624,9 +583,6 @@ playDp.on("play", function () { patchVideoInlinePlayback(playDp); hidePosterOverlay(); - if (currentPlayMode === "main") { - reportChapterWatch(); - } }); } return playDp; @@ -669,9 +625,6 @@ var root = playerRoot(); if (!root) return; var next = mode === "preview" ? "preview" : "main"; - if (window.tracker && typeof window.tracker.prepareDramaModeChange === "function") { - window.tracker.prepareDramaModeChange(next); - } currentPlayMode = next; root.setAttribute("data-play-mode", next); root.classList.toggle("play-player--preview", next === "preview"); @@ -782,7 +735,6 @@ } function bootPlayer() { - ensureComposerAvatarDecrypt(); var root = playerRoot(); if (!root) return; @@ -888,7 +840,7 @@ Array.prototype.forEach.call(openers, function (btn) { btn.addEventListener("click", function () { - // 预告片模式不弹充值;属性被临时移除时也不弹。 + // 预告片模式不弹下载提示;属性被临时移除时也不弹。 if (!btn.hasAttribute("data-play-coin-open")) return; if (isPreviewPlayingSurface()) return; open(); @@ -906,123 +858,6 @@ }); })(); -// 金币解锁:POST /api/playlet/chapter/unlock → upstream /api/app/playlet/chapter/unlock -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function openRecharge() { - var opener = document.querySelector("[data-play-coin-open]"); - if (opener) { - opener.click(); - return; - } - if (typeof window.openRechargeModal === "function") { - window.openRechargeModal(); - return; - } - window.location.href = "/recharge"; - } - - function reloadAfterUnlockTracking() { - var tracking = Promise.resolve(); - if (window.tracker && typeof window.tracker.dramaUnlock === "function") { - tracking = Promise.resolve(window.tracker.dramaUnlock()).catch(function () { - // 埋点失败不影响解锁结果。 - }); - if (typeof window.tracker.flush === "function") { - [50, 200, 500, 700].forEach(function (delay) { - window.setTimeout(function () { - Promise.resolve(window.tracker.flush()).catch(function () { - // 埋点失败不影响解锁结果。 - }); - }, delay); - }); - } - } - var deadline = new Promise(function (resolve) { - window.setTimeout(resolve, 800); - }); - return Promise.race([tracking, deadline]).then(function () { - window.location.reload(); - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-play-unlock]"); - if (!btn) return; - event.preventDefault(); - if (btn.disabled || btn.getAttribute("data-unlocking") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("解锁服务未就绪", "error"); - return; - } - var chapterId = toInt(btn.getAttribute("data-chapter-id"), 0); - var playletId = toInt(btn.getAttribute("data-playlet-id"), 0); - if (chapterId <= 0 || playletId <= 0) { - toast("缺少剧集信息", "error"); - return; - } - var payload = { chapterId: chapterId, playletId: playletId }; - btn.setAttribute("data-unlocking", "1"); - btn.disabled = true; - window.HttpRequest.post("/api/playlet/chapter/unlock", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - var code = res && Number(res.code); - if (code === 401 || code === 1000) { - var login = document.querySelector("[data-login-open]"); - if (login) login.click(); - return; - } - // 常见余额不足码,转充值 - if (code === 400 || /金币|余额|不足/.test(String((res && (res.tip || res.msg)) || ""))) { - openRecharge(); - return; - } - throw new Error((res && (res.tip || res.msg)) || "解锁失败"); - } - var data = res.data || {}; - if (data.balance != null && window.CookieUtil && typeof window.CookieUtil.setAuthSession === "function") { - window.CookieUtil.setAuthSession({ recharge: data.balance }); - if (typeof window.syncHeaderUser === "function") window.syncHeaderUser(); - } - toast("购买成功", "success"); - return reloadAfterUnlockTracking(); - }) - .catch(function (err) { - toast((err && err.message) || "解锁失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-unlocking"); - btn.disabled = false; - }); - }); -})(); - (function () { var btn = document.querySelector("[data-guess-refresh]"); var grid = document.querySelector("[data-guess-grid]"); @@ -1350,622 +1185,6 @@ }); })(); -// 剧集点赞:POST /api/playlet/chapter/like/set → upstream /api/app/playlet/chapter/like/set -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function formatLikeCount(n) { - var v = Number(n) || 0; - if (v <= 0) return "0"; - if (v < 10000) return String(v); - var wan = v / 10000; - if (wan >= 100) return Math.round(wan) + " 万"; - return wan.toFixed(1) + " 万"; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function applyChapterLikeUI(btn, isLike, countLike) { - if (!btn) return; - var liked = Number(isLike) === 1; - btn.classList.toggle("is-active", liked); - btn.setAttribute("data-liked", liked ? "1" : "2"); - btn.setAttribute("aria-label", liked ? "取消点赞" : "点赞"); - var label = btn.querySelector("[data-chapter-like-label]"); - if (label) label.textContent = liked ? "已点赞" : "点赞"; - var countEl = btn.querySelector("[data-chapter-like-count]"); - if (countLike != null) { - btn.setAttribute("data-like-raw", String(countLike)); - if (countEl) countEl.textContent = formatLikeCount(countLike); - } - if (countEl) countEl.hidden = liked; - } - - function toggleChapterLike(btn) { - if (!btn || btn.disabled || btn.getAttribute("data-liking") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("点赞服务未就绪", "error"); - return; - } - var chapterId = toInt(btn.getAttribute("data-chapter-id"), 0); - if (chapterId <= 0) { - toast("缺少剧集 ID", "error"); - return; - } - var likedNow = Number(btn.getAttribute("data-liked") || 2) === 1; - var nextLike = !likedNow; - var prevCount = toInt(btn.getAttribute("data-like-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextLike ? 1 : -1)); - btn.setAttribute("data-liking", "1"); - btn.disabled = true; - applyChapterLikeUI(btn, nextLike ? 1 : 2, nextCount); - var payload = { chapterId: chapterId, isLike: nextLike }; - window.HttpRequest.post("/api/playlet/chapter/like/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyChapterLikeUI( - btn, - data.isLike != null ? data.isLike : nextLike ? 1 : 2, - data.countLike != null ? data.countLike : nextCount - ); - }) - .catch(function (err) { - applyChapterLikeUI(btn, likedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-liking"); - btn.disabled = false; - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-chapter-like]"); - if (!btn) return; - event.preventDefault(); - toggleChapterLike(btn); - }); -})(); - -// 短剧点赞:POST /api/playlet/like/set → upstream /api/app/playlet/like/set -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function formatLikeCount(n) { - var v = Number(n) || 0; - if (v <= 0) return "0"; - if (v < 10000) return String(v); - var wan = v / 10000; - if (wan >= 100) return Math.round(wan) + " 万"; - return wan.toFixed(1) + " 万"; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function applyPlayletLikeUI(btn, isLike, countLike) { - if (!btn) return; - var liked = Number(isLike) === 1; - btn.classList.toggle("is-active", liked); - btn.setAttribute("data-liked", liked ? "1" : "2"); - btn.setAttribute("aria-label", liked ? "取消点赞" : "点赞"); - var label = btn.querySelector("[data-playlet-like-label]"); - if (label) label.textContent = liked ? "已点赞" : "点赞"; - var countEl = btn.querySelector("[data-playlet-like-count]"); - if (countLike != null) { - btn.setAttribute("data-like-raw", String(countLike)); - if (countEl) countEl.textContent = formatLikeCount(countLike); - } - if (countEl) countEl.hidden = liked; - } - - function togglePlayletLike(btn) { - if (!btn || btn.disabled || btn.getAttribute("data-liking") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("点赞服务未就绪", "error"); - return; - } - var playletId = toInt(btn.getAttribute("data-playlet-id"), 0); - if (playletId <= 0) { - toast("缺少短剧 ID", "error"); - return; - } - var likedNow = Number(btn.getAttribute("data-liked") || 2) === 1; - var nextLike = !likedNow; - var prevCount = toInt(btn.getAttribute("data-like-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextLike ? 1 : -1)); - btn.setAttribute("data-liking", "1"); - btn.disabled = true; - applyPlayletLikeUI(btn, nextLike ? 1 : 2, nextCount); - var payload = { id: playletId, isLike: nextLike }; - window.HttpRequest.post("/api/playlet/like/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyPlayletLikeUI( - btn, - data.isLike != null ? data.isLike : nextLike ? 1 : 2, - data.countLike != null ? data.countLike : nextCount - ); - }) - .catch(function (err) { - applyPlayletLikeUI(btn, likedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-liking"); - btn.disabled = false; - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-playlet-like]"); - if (!btn) return; - event.preventDefault(); - togglePlayletLike(btn); - }); -})(); - -// 视频点赞:POST /api/video/like/set → upstream /api/app/video/like/set -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function formatLikeCount(n) { - var v = Number(n) || 0; - if (v <= 0) return "0"; - if (v < 10000) return String(v); - var wan = v / 10000; - if (wan >= 100) return Math.round(wan) + " 万"; - return wan.toFixed(1) + " 万"; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function applyVideoLikeUI(btn, isLike, countLike) { - if (!btn) return; - var liked = Number(isLike) === 1; - btn.classList.toggle("is-active", liked); - btn.setAttribute("data-liked", liked ? "1" : "2"); - btn.setAttribute("aria-label", liked ? "取消点赞" : "点赞"); - var label = btn.querySelector("[data-video-like-label]"); - if (label) label.textContent = liked ? "已点赞" : "点赞"; - var countEl = btn.querySelector("[data-video-like-count]"); - if (countLike != null) { - btn.setAttribute("data-like-raw", String(countLike)); - if (countEl) countEl.textContent = formatLikeCount(countLike); - } - if (countEl) countEl.hidden = liked; - } - - function toggleVideoLike(btn) { - if (!btn || btn.disabled || btn.getAttribute("data-liking") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("点赞服务未就绪", "error"); - return; - } - var videoId = toInt(btn.getAttribute("data-video-id"), 0); - if (videoId <= 0) { - toast("缺少视频 ID", "error"); - return; - } - var likedNow = Number(btn.getAttribute("data-liked") || 2) === 1; - var nextLike = !likedNow; - var prevCount = toInt(btn.getAttribute("data-like-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextLike ? 1 : -1)); - btn.setAttribute("data-liking", "1"); - btn.disabled = true; - applyVideoLikeUI(btn, nextLike ? 1 : 2, nextCount); - var payload = { id: videoId, isLike: nextLike }; - window.HttpRequest.post("/api/video/like/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyVideoLikeUI( - btn, - data.isLike != null ? data.isLike : nextLike ? 1 : 2, - data.countLike != null ? data.countLike : nextCount - ); - }) - .catch(function (err) { - applyVideoLikeUI(btn, likedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-liking"); - btn.disabled = false; - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-video-like]"); - if (!btn) return; - event.preventDefault(); - toggleVideoLike(btn); - }); -})(); - -// 视频收藏:POST /api/video/collect/set → upstream /api/app/video/collect/set -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function formatCount(n) { - var v = Number(n) || 0; - if (v <= 0) return "0"; - if (v < 10000) return String(v); - var wan = v / 10000; - if (wan >= 100) return Math.round(wan) + " 万"; - return wan.toFixed(1) + " 万"; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function applyVideoCollectUI(btn, isCollect, countCollect) { - if (!btn) return; - var collected = Number(isCollect) === 1; - btn.classList.toggle("is-active", collected); - btn.setAttribute("data-collected", collected ? "1" : "2"); - btn.setAttribute("aria-label", collected ? "取消收藏" : "收藏"); - var label = btn.querySelector("[data-video-collect-label]"); - if (label) label.textContent = collected ? "已收藏" : "收藏"; - var countEl = btn.querySelector("[data-video-collect-count]"); - if (countCollect != null) { - btn.setAttribute("data-collect-raw", String(countCollect)); - if (countEl) countEl.textContent = formatCount(countCollect); - } - if (countEl) countEl.hidden = collected; - } - - function toggleVideoCollect(btn) { - if (!btn || btn.disabled || btn.getAttribute("data-collecting") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("收藏服务未就绪", "error"); - return; - } - var videoId = toInt(btn.getAttribute("data-video-id"), 0); - if (videoId <= 0) { - toast("缺少视频 ID", "error"); - return; - } - var collectedNow = Number(btn.getAttribute("data-collected") || 2) === 1; - var nextCollect = !collectedNow; - var prevCount = toInt(btn.getAttribute("data-collect-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextCollect ? 1 : -1)); - btn.setAttribute("data-collecting", "1"); - btn.disabled = true; - applyVideoCollectUI(btn, nextCollect ? 1 : 2, nextCount); - var payload = { id: videoId, isCollect: nextCollect }; - window.HttpRequest.post("/api/video/collect/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyVideoCollectUI( - btn, - data.isCollect != null ? data.isCollect : nextCollect ? 1 : 2, - data.countCollect != null ? data.countCollect : nextCount - ); - }) - .catch(function (err) { - applyVideoCollectUI(btn, collectedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-collecting"); - btn.disabled = false; - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-video-collect]"); - if (!btn) return; - event.preventDefault(); - toggleVideoCollect(btn); - }); -})(); - -// 剧集收藏:POST /api/playlet/chapter/collect/set → upstream /api/app/playlet/chapter/collect/set -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function formatLikeCount(n) { - var v = Number(n) || 0; - if (v <= 0) return "0"; - if (v < 10000) return String(v); - var wan = v / 10000; - if (wan >= 100) return Math.round(wan) + " 万"; - return wan.toFixed(1) + " 万"; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function applyChapterCollectUI(btn, isCollect, countCollect) { - if (!btn) return; - var collected = Number(isCollect) === 1; - btn.classList.toggle("is-active", collected); - btn.setAttribute("data-collected", collected ? "1" : "2"); - btn.setAttribute("aria-label", collected ? "取消收藏" : "收藏"); - var label = btn.querySelector("[data-chapter-collect-label]"); - if (label) label.textContent = collected ? "已收藏" : "收藏"; - var countEl = btn.querySelector("[data-chapter-collect-count]"); - if (countCollect != null) { - btn.setAttribute("data-collect-raw", String(countCollect)); - if (countEl) countEl.textContent = formatLikeCount(countCollect); - } - if (countEl) countEl.hidden = collected; - } - - function toggleChapterCollect(btn) { - if (!btn || btn.disabled || btn.getAttribute("data-collecting") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("收藏服务未就绪", "error"); - return; - } - var chapterId = toInt(btn.getAttribute("data-chapter-id"), 0); - if (chapterId <= 0) { - toast("缺少剧集 ID", "error"); - return; - } - var collectedNow = Number(btn.getAttribute("data-collected") || 2) === 1; - var nextCollect = !collectedNow; - var prevCount = toInt(btn.getAttribute("data-collect-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextCollect ? 1 : -1)); - btn.setAttribute("data-collecting", "1"); - btn.disabled = true; - applyChapterCollectUI(btn, nextCollect ? 1 : 2, nextCount); - var payload = { chapterId: chapterId, isCollect: nextCollect }; - window.HttpRequest.post("/api/playlet/chapter/collect/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyChapterCollectUI( - btn, - data.isCollect != null ? data.isCollect : nextCollect ? 1 : 2, - data.countCollect != null ? data.countCollect : nextCount - ); - }) - .catch(function (err) { - applyChapterCollectUI(btn, collectedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-collecting"); - btn.disabled = false; - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-chapter-collect]"); - if (!btn) return; - event.preventDefault(); - toggleChapterCollect(btn); - }); -})(); - -// 短剧收藏:POST /api/playlet/collect/set → upstream /api/app/playlet/collect/set -(function () { - function toast(message, type) { - if (typeof window.showToast === "function") { - window.showToast(message, type); - return; - } - } - - function toInt(v, fallback) { - var n = parseInt(v, 10); - return isNaN(n) ? fallback || 0 : n; - } - - function formatLikeCount(n) { - var v = Number(n) || 0; - if (v <= 0) return "0"; - if (v < 10000) return String(v); - var wan = v / 10000; - if (wan >= 100) return Math.round(wan) + " 万"; - return wan.toFixed(1) + " 万"; - } - - function isLoggedIn() { - var composer = document.querySelector("[data-play-composer]"); - if (composer && composer.getAttribute("data-logged-in") === "1") return true; - if (window.CookieUtil && typeof window.CookieUtil.isLoggedIn === "function") { - return !!window.CookieUtil.isLoggedIn(); - } - return false; - } - - function applyPlayletCollectUI(btn, isCollect, countCollect) { - if (!btn) return; - var collected = Number(isCollect) === 1; - btn.classList.toggle("is-active", collected); - btn.setAttribute("data-collected", collected ? "1" : "2"); - btn.setAttribute("aria-label", collected ? "取消收藏" : "收藏"); - var label = btn.querySelector("[data-playlet-collect-label]"); - if (label) label.textContent = collected ? "已收藏" : "收藏"; - var countEl = btn.querySelector("[data-playlet-collect-count]"); - if (countCollect != null) { - btn.setAttribute("data-collect-raw", String(countCollect)); - if (countEl) countEl.textContent = formatLikeCount(countCollect); - var stats = document.querySelectorAll("[data-playlet-collect-stat]"); - Array.prototype.forEach.call(stats, function (el) { - el.textContent = formatLikeCount(countCollect); - }); - } - if (countEl) countEl.hidden = collected; - } - - function togglePlayletCollect(btn) { - if (!btn || btn.disabled || btn.getAttribute("data-collecting") === "1") return; - if (!isLoggedIn()) { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("收藏服务未就绪", "error"); - return; - } - var playletId = toInt(btn.getAttribute("data-playlet-id"), 0); - if (playletId <= 0) { - toast("缺少短剧 ID", "error"); - return; - } - var collectedNow = Number(btn.getAttribute("data-collected") || 2) === 1; - var nextCollect = !collectedNow; - var prevCount = toInt(btn.getAttribute("data-collect-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextCollect ? 1 : -1)); - btn.setAttribute("data-collecting", "1"); - btn.disabled = true; - applyPlayletCollectUI(btn, nextCollect ? 1 : 2, nextCount); - var payload = { id: playletId, isCollect: nextCollect }; - window.HttpRequest.post("/api/playlet/collect/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyPlayletCollectUI( - btn, - data.isCollect != null ? data.isCollect : nextCollect ? 1 : 2, - data.countCollect != null ? data.countCollect : nextCount - ); - }) - .catch(function (err) { - applyPlayletCollectUI(btn, collectedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-collecting"); - btn.disabled = false; - }); - } - - document.addEventListener("click", function (event) { - var btn = window.domClosest(event.target, "[data-playlet-collect]"); - if (!btn) return; - event.preventDefault(); - togglePlayletCollect(btn); - }); -})(); - // 简介展开:PC 超过 1 行、H5 超过 2 行才显示展开按钮 (function () { var root = document.querySelector("[data-play-synopsis]"); diff --git a/app/static/js/pages/playComments.js b/app/static/js/pages/playComments.js index d8b199e..aa4a69e 100644 --- a/app/static/js/pages/playComments.js +++ b/app/static/js/pages/playComments.js @@ -1,30 +1,19 @@ +// 播放页评论:只读列表 + 「查看更多评论」追加翻页 (function () { - var root = document.querySelector("[data-play-composer]"); - if (!root) return; + var commentsRoot = document.querySelector("[data-play-comments]"); + if (!commentsRoot) return; - var commentsRoot = document.querySelector("[data-play-comments]") || root.closest(".play-comments"); - var input = root.querySelector("[data-comment-input]"); - var countEl = root.querySelector("[data-comment-count]"); - var sendBtn = root.querySelector("[data-comment-send]"); var listEl = document.querySelector("[data-comment-list]"); var totalEl = document.querySelector("[data-comment-total-label]"); var moreBtn = document.querySelector("[data-comment-more]"); - var sending = false; - var inlineSending = false; var loadingMore = false; - var MAX_LEN = 200; var PAGE_SIZE = 4; - var replyTarget = null; function toast(message, type) { if (typeof window.showToast === "function") { window.showToast(message, type); return; } - if (window.AuthAPI && typeof window.AuthAPI.toast === "function") { - window.AuthAPI.toast(message, type || "info"); - return; - } } function toInt(v, fallback) { @@ -43,14 +32,14 @@ function getPageSize() { return Math.max( 1, - toInt(commentsRoot && commentsRoot.getAttribute("data-comment-size"), PAGE_SIZE) + toInt(commentsRoot.getAttribute("data-comment-size"), PAGE_SIZE) ); } function getPage() { return Math.max( 1, - toInt(commentsRoot && commentsRoot.getAttribute("data-comment-page"), 1) + toInt(commentsRoot.getAttribute("data-comment-page"), 1) ); } @@ -60,7 +49,6 @@ } function setPagerState(page, loaded, total) { - if (!commentsRoot) return; if (page != null) commentsRoot.setAttribute("data-comment-page", String(page)); if (loaded != null) commentsRoot.setAttribute("data-comment-loaded", String(loaded)); if (total != null) commentsRoot.setAttribute("data-comment-total", String(total)); @@ -80,12 +68,6 @@ } } - function syncCount() { - if (!input || !countEl) return; - var len = Array.from(input.value || "").length; - countEl.textContent = len + " / " + MAX_LEN; - } - function formatTime(raw) { var s = String(raw || "").trim(); if (!s) return "刚刚"; @@ -111,7 +93,6 @@ var text = escapeHtml(item.text || ""); var time = escapeHtml(formatTime(item.createdAt)); var like = toInt(item.countLike, 0); - var liked = Number(item.isLike) === 1; var avatarHtml = avatar ? '" + replies + '
' + - '" + - '' + + "" + "
" ); } @@ -195,26 +160,6 @@ totalEl.textContent = n > 0 ? n + " 条" : "暂无评论"; } - function renderList(list, totalCount) { - if (!listEl) return; - list = Array.isArray(list) ? list : []; - clearReplyTarget(); - if (!list.length) { - listEl.innerHTML = - '
  • 暂无评论

  • '; - } else { - listEl.innerHTML = list.map(renderCommentItem).join(""); - if (typeof window.initLazyImages === "function") { - window.initLazyImages(listEl); - } - } - var total = typeof totalCount === "number" ? totalCount : toInt(commentsRoot && commentsRoot.getAttribute("data-comment-total"), 0); - if (typeof totalCount === "number") { - updateTotal(totalCount); - } - setPagerState(1, list.length, total); - } - // 下一页追加到现有列表底部,不替换已展示评论。 function appendList(list, totalCount) { if (!listEl) return; @@ -222,7 +167,7 @@ var total = typeof totalCount === "number" ? totalCount - : toInt(commentsRoot && commentsRoot.getAttribute("data-comment-total"), 0); + : toInt(commentsRoot.getAttribute("data-comment-total"), 0); if (typeof totalCount === "number") { updateTotal(totalCount); } @@ -264,8 +209,8 @@ if (!window.HttpRequest || typeof window.HttpRequest.get !== "function") { return Promise.reject(new Error("评论服务未就绪")); } - var objectId = toInt(root.getAttribute("data-object-id"), 0); - var objectType = toInt(root.getAttribute("data-object-type"), 10); + var objectId = toInt(commentsRoot.getAttribute("data-object-id"), 0); + var objectType = toInt(commentsRoot.getAttribute("data-object-type"), 10); if (objectId <= 0) return Promise.reject(new Error("缺少评论对象")); var pageSize = getPageSize(); return window.HttpRequest.get("/api/comment/list", { @@ -284,14 +229,6 @@ }); } - function refreshList() { - return fetchCommentPage(1) - .then(function (data) { - renderList(data.list || [], toInt(data.totalCount, 0)); - }) - .catch(function () {}); - } - function loadMoreComments() { if (loadingMore || !moreBtn) return; var nextPage = getPage() + 1; @@ -301,9 +238,7 @@ .then(function (data) { var list = data.list || []; var total = toInt(data.totalCount, 0); - if (commentsRoot) { - commentsRoot.setAttribute("data-comment-page", String(nextPage)); - } + commentsRoot.setAttribute("data-comment-page", String(nextPage)); // 只追加,绝不 renderList 整表替换 appendList(list, total); }) @@ -316,284 +251,10 @@ }); } - function removeInlineComposer() { - if (!listEl) return; - listEl.querySelectorAll("[data-comment-inline-composer]").forEach(function (el) { - el.remove(); - }); - } - - function clearReplyTarget() { - replyTarget = null; - removeInlineComposer(); - } - - function applyLikeUI(btn, isLike, countLike) { - if (!btn) return; - var liked = Number(isLike) === 1; - btn.classList.toggle("is-active", liked); - btn.setAttribute("data-liked", liked ? "1" : "2"); - if (countLike != null) { - btn.setAttribute("data-like-raw", String(countLike)); - var countEl = btn.querySelector("[data-like-count]"); - if (countEl) countEl.textContent = String(countLike); - } - } - - function toggleCommentLike(btn) { - if (!btn || btn.getAttribute("data-liking") === "1") return; - if (root.getAttribute("data-logged-in") !== "1") { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("点赞服务未就绪", "error"); - return; - } - var commentId = toInt(btn.getAttribute("data-comment-like"), 0); - if (commentId <= 0) return; - var likedNow = Number(btn.getAttribute("data-liked") || 2) === 1; - var nextLike = !likedNow; - var prevCount = toInt(btn.getAttribute("data-like-raw"), 0); - var nextCount = Math.max(0, prevCount + (nextLike ? 1 : -1)); - btn.setAttribute("data-liking", "1"); - btn.disabled = true; - applyLikeUI(btn, nextLike ? 1 : 2, nextCount); - var payload = { commentId: commentId, isLike: nextLike }; - window.HttpRequest.post("/api/comment/like/set", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "操作失败"); - } - var data = res.data || {}; - applyLikeUI( - btn, - data.isLike != null ? data.isLike : nextLike ? 1 : 2, - data.countLike != null ? data.countLike : nextCount - ); - }) - .catch(function (err) { - applyLikeUI(btn, likedNow ? 1 : 2, prevCount); - toast((err && err.message) || "操作失败,请稍后重试", "error"); - }) - .finally(function () { - btn.removeAttribute("data-liking"); - btn.disabled = false; - }); - } - - function openInlineComposer(commentEl, target) { - if (!commentEl || !target) return; - removeInlineComposer(); - replyTarget = target; - var body = commentEl.querySelector(".play-comment__body") || commentEl; - var actions = body.querySelector(".play-comment__actions"); - var box = document.createElement("div"); - box.className = "play-comment__inline"; - box.setAttribute("data-comment-inline-composer", ""); - var name = target.replyName || "用户"; - box.innerHTML = - '
    ' + - "回复 @" + - escapeHtml(name) + - "" + - '' + - "
    " + - '
    ' + - '' + - '' + - "
    "; - if (actions && actions.parentNode === body) { - if (actions.nextSibling) body.insertBefore(box, actions.nextSibling); - else body.appendChild(box); - } else { - body.appendChild(box); - } - var inlineInput = box.querySelector("[data-comment-inline-input]"); - if (inlineInput) inlineInput.focus(); - } - - function setSending(on) { - sending = !!on; - if (sendBtn) sendBtn.disabled = !!on; - if (input) input.disabled = !!on; - } - - function submitTopComment() { - if (sending) return; - if (root.getAttribute("data-logged-in") !== "1") { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("评论服务未就绪", "error"); - return; - } - var text = String((input && input.value) || "").trim(); - if (!text) { - toast("请输入评论内容", "error"); - return; - } - if (Array.from(text).length > MAX_LEN) { - toast("评论最多 200 字", "error"); - return; - } - var objectId = toInt(root.getAttribute("data-object-id"), 0); - var objectType = toInt(root.getAttribute("data-object-type"), 10); - if (objectId <= 0) { - toast("缺少评论对象", "error"); - return; - } - - var payload = { - objectId: objectId, - objectType: objectType, - text: text, - parentsId: 0, - replyCommentId: 0, - }; - setSending(true); - window.HttpRequest.post("/api/comment/send", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "发表失败"); - } - if (input) input.value = ""; - syncCount(); - clearReplyTarget(); - toast("发布成功,评论审核后才会展示", "success"); - return refreshList(); - }) - .catch(function (err) { - toast((err && err.message) || "发表失败,请稍后重试", "error"); - }) - .finally(function () { - setSending(false); - }); - } - - function sendInlineReply(box) { - if (!box || inlineSending || !replyTarget) return; - if (!window.HttpRequest || typeof window.HttpRequest.post !== "function") { - toast("评论服务未就绪", "error"); - return; - } - var inlineInput = box.querySelector("[data-comment-inline-input]"); - var btn = box.querySelector("[data-comment-inline-send]"); - if (!inlineInput) return; - var text = String(inlineInput.value || "").trim(); - if (!text) { - toast("请输入评论内容", "error"); - return; - } - if (Array.from(text).length > MAX_LEN) { - toast("评论最多 200 字", "error"); - return; - } - var objectId = toInt(root.getAttribute("data-object-id"), 0); - var objectType = toInt(root.getAttribute("data-object-type"), 10); - if (objectId <= 0) { - toast("缺少评论对象", "error"); - return; - } - inlineSending = true; - if (btn) btn.disabled = true; - inlineInput.disabled = true; - var payload = { - objectId: objectId, - objectType: objectType, - text: text, - parentsId: toInt(replyTarget.parentsId, 0), - replyCommentId: toInt( - replyTarget.replyCommentId || replyTarget.parentsId, - 0 - ), - }; - window.HttpRequest.post("/api/comment/send", payload) - .then(function (res) { - if (!res || Number(res.code) !== 200) { - throw new Error((res && (res.tip || res.msg)) || "发表失败"); - } - clearReplyTarget(); - toast("发布成功,评论审核后才会展示", "success"); - return refreshList(); - }) - .catch(function (err) { - toast((err && err.message) || "发表失败,请稍后重试", "error"); - }) - .finally(function () { - inlineSending = false; - if (btn) btn.disabled = false; - if (inlineInput) inlineInput.disabled = false; - }); - } - - if (input) { - input.addEventListener("input", syncCount); - input.addEventListener("keydown", function (event) { - if (event.key === "Enter" && !event.shiftKey) { - event.preventDefault(); - submitTopComment(); - } - }); - syncCount(); - } - if (sendBtn) { - sendBtn.addEventListener("click", submitTopComment); - } if (moreBtn) { moreBtn.addEventListener("click", function (event) { event.preventDefault(); loadMoreComments(); }); } - if (listEl) { - listEl.addEventListener("click", function (event) { - var likeBtn = window.domClosest(event.target, "[data-comment-like]"); - if (likeBtn && listEl.contains(likeBtn)) { - event.preventDefault(); - toggleCommentLike(likeBtn); - return; - } - var cancelBtn = window.domClosest(event.target, "[data-comment-reply-cancel]"); - if (cancelBtn && listEl.contains(cancelBtn)) { - clearReplyTarget(); - return; - } - var sendInlineBtn = window.domClosest(event.target, "[data-comment-inline-send]"); - if (sendInlineBtn && listEl.contains(sendInlineBtn)) { - var box = sendInlineBtn.closest("[data-comment-inline-composer]"); - sendInlineReply(box); - return; - } - var replyBtn = window.domClosest(event.target, "[data-comment-reply]"); - if (!replyBtn || !listEl.contains(replyBtn)) return; - if (root.getAttribute("data-logged-in") !== "1") { - var loginBtn = document.querySelector("[data-login-open]"); - if (loginBtn) loginBtn.click(); - return; - } - var parentsId = toInt(replyBtn.getAttribute("data-parents-id"), 0); - var commentId = toInt(replyBtn.getAttribute("data-comment-id"), parentsId); - if (parentsId <= 0) return; - var commentEl = replyBtn.closest(".play-comment"); - openInlineComposer(commentEl, { - parentsId: parentsId, - replyCommentId: commentId, - replyName: replyBtn.getAttribute("data-reply-name") || "用户", - }); - }); - listEl.addEventListener("keydown", function (event) { - if (event.key !== "Enter" || event.shiftKey) return; - var inlineInput = window.domClosest(event.target, "[data-comment-inline-input]"); - if (!inlineInput || !listEl.contains(inlineInput)) return; - event.preventDefault(); - var box = inlineInput.closest("[data-comment-inline-composer]"); - sendInlineReply(box); - }); - } })(); diff --git a/app/static/js/pages/register.js b/app/static/js/pages/register.js deleted file mode 100644 index 66f639a..0000000 --- a/app/static/js/pages/register.js +++ /dev/null @@ -1,169 +0,0 @@ -(function () { - var form = document.querySelector("[data-register-page-form]"); - var password = document.querySelector("[data-register-page-password]"); - var confirm = document.querySelector("[data-register-page-password-confirm]"); - var submitBtn = document.querySelector("[data-register-page-submit]") || (form && form.querySelector('button[type="submit"]')); - var root = form || document; - - function bindToggle(key, input) { - var btn = document.querySelector('[data-register-page-toggle="' + key + '"]'); - var icon = document.querySelector('[data-register-page-toggle-icon="' + key + '"]'); - if (!btn || !input || !icon) return; - btn.addEventListener("click", function () { - var show = input.getAttribute("type") === "password"; - input.setAttribute("type", show ? "text" : "password"); - icon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg"); - btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码"); - }); - } - - bindToggle("password", password); - bindToggle("confirm", confirm); - - function clearFieldError(name) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.remove("is-error"); - if (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - } - } - - function clearAllErrors() { - root.querySelectorAll("[data-field]").forEach(function (el) { - el.classList.remove("is-error"); - }); - root.querySelectorAll("[data-field-error]").forEach(function (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - }); - } - - function setFieldError(name, message) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.add("is-error"); - if (err) { - err.hidden = false; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = message || ""; - } - } - - function isAccountNo(value) { - return /^[A-Za-z0-9]{6,20}$/.test(String(value || "")); - } - - function isPassword(value) { - var pwd = String(value || ""); - if (pwd.length < 8) return false; - return /[A-Za-z]/.test(pwd) && /[0-9]/.test(pwd); - } - - function validateForm() { - clearAllErrors(); - var fd = new FormData(form); - var account = String(fd.get("account") || "").trim(); - var pwd = String(fd.get("password") || ""); - var pwd2 = String(fd.get("passwordConfirm") || ""); - var agree = form.querySelector('input[name="agree"]'); - var valid = true; - - if (!account) { - setFieldError("account", "请输入账号"); - valid = false; - } else if (!isAccountNo(account)) { - setFieldError("account", "账号需为 6 到 20 位字母或数字"); - valid = false; - } - - if (!pwd) { - setFieldError("password", "请输入密码"); - valid = false; - } else if (!isPassword(pwd)) { - setFieldError("password", "密码需 8 位以上,且同时包含字母与数字"); - valid = false; - } - - if (!pwd2) { - setFieldError("passwordConfirm", "请再次输入密码"); - valid = false; - } else if (pwd2 !== pwd) { - setFieldError("passwordConfirm", "两次输入的密码不一致"); - valid = false; - } - - if (agree && !agree.checked) { - setFieldError("agree", "请先同意用户条款与隐私政策"); - valid = false; - } - - if (!valid) return null; - return { accountNo: account, password: pwd }; - } - - function mapServerError(err) { - var tip = (err && err.message) || "注册失败,请稍后重试"; - var code = err && err.code; - if (code === 5005 || /已存在|重复|占用/.test(tip)) { - setFieldError("account", tip); - return; - } - if (/密码|password/i.test(tip)) { - setFieldError("password", tip); - return; - } - setFieldError("account", tip); - } - - function setSubmitting(loading) { - if (!submitBtn) return; - submitBtn.disabled = !!loading; - submitBtn.classList.toggle("is-loading", !!loading); - submitBtn.textContent = loading ? "注册中…" : "注册"; - } - - if (form) { - form.querySelectorAll("input").forEach(function (input) { - input.addEventListener("input", function () { - var name = input.getAttribute("name"); - if (name === "agree") clearFieldError("agree"); - else if (name) clearFieldError(name); - }); - if (input.type === "checkbox") { - input.addEventListener("change", function () { - clearFieldError("agree"); - }); - } - }); - - form.addEventListener("submit", function (event) { - event.preventDefault(); - if (!window.AuthAPI || !window.HttpRequest) { - setFieldError("account", "认证脚本未加载,请刷新页面"); - return; - } - if (submitBtn && submitBtn.classList.contains("is-loading")) return; - - var payload = validateForm(); - if (!payload) return; - - setSubmitting(true); - AuthAPI.register(payload) - .then(function (res) { - AuthAPI.toast((res && res.tip) || "注册成功", "success"); - window.setTimeout(function () { - window.location.href = "/"; - }, 400); - }) - .catch(function (err) { - setSubmitting(false); - mapServerError(err); - AuthAPI.toast((err && err.message) || "注册失败", "error"); - }); - }); - } -})(); diff --git a/app/static/js/pages/registerPop.js b/app/static/js/pages/registerPop.js deleted file mode 100644 index aefb823..0000000 --- a/app/static/js/pages/registerPop.js +++ /dev/null @@ -1,256 +0,0 @@ -(function () { - var root = document.querySelector("[data-register-pop]"); - if (!root) return; - - var form = root.querySelector("[data-register-form]"); - var password = root.querySelector("[data-register-password]"); - var confirm = root.querySelector("[data-register-password-confirm]"); - var submitBtn = root.querySelector("[data-register-submit]") || (form && form.querySelector('button[type="submit"]')); - var previousOverflow = ""; - - function closeLogin() { - if (typeof window.closeLoginPop === "function") { - window.closeLoginPop(); - return; - } - var login = document.querySelector("[data-login-pop]"); - if (login) login.classList.remove("is-open"); - } - - function closeUserMenu() { - var menu = document.querySelector("[data-user-menu].is-open"); - if (menu) { - menu.classList.remove("is-open"); - var t = menu.querySelector("[data-user-menu-toggle]"); - if (t) t.setAttribute("aria-expanded", "false"); - } - } - - function clearFieldError(name) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.remove("is-error"); - if (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - } - } - - function clearAllErrors() { - root.querySelectorAll("[data-field]").forEach(function (el) { - el.classList.remove("is-error"); - }); - root.querySelectorAll("[data-field-error]").forEach(function (err) { - err.hidden = true; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = ""; - }); - } - - function setFieldError(name, message) { - var field = root.querySelector('[data-field="' + name + '"]'); - var err = root.querySelector('[data-field-error="' + name + '"]'); - if (field) field.classList.add("is-error"); - if (err) { - err.hidden = false; - var text = err.querySelector("[data-field-error-text]"); - if (text) text.textContent = message || ""; - } - } - - function isAccountNo(value) { - return /^[A-Za-z0-9]{6,20}$/.test(String(value || "")); - } - - function isPassword(value) { - var pwd = String(value || ""); - if (pwd.length < 8) return false; - return /[A-Za-z]/.test(pwd) && /[0-9]/.test(pwd); - } - - function bindingMessage(message, fallback) { - return String(message || fallback || "").replace(/注册/g, "绑定"); - } - - function validateForm() { - clearAllErrors(); - var fd = new FormData(form); - var account = String(fd.get("account") || "").trim(); - var pwd = String(fd.get("password") || ""); - var pwd2 = String(fd.get("passwordConfirm") || ""); - var agree = form.querySelector('input[name="agree"]'); - var valid = true; - - if (!account) { - setFieldError("account", "请输入账号"); - valid = false; - } else if (!isAccountNo(account)) { - setFieldError("account", "账号需为 6 到 20 位字母或数字"); - valid = false; - } - - if (!pwd) { - setFieldError("password", "请输入密码"); - valid = false; - } else if (!isPassword(pwd)) { - setFieldError("password", "密码需 8 位以上,且同时包含字母与数字"); - valid = false; - } - - if (!pwd2) { - setFieldError("passwordConfirm", "请再次输入密码"); - valid = false; - } else if (pwd2 !== pwd) { - setFieldError("passwordConfirm", "两次输入的密码不一致"); - valid = false; - } - - if (agree && !agree.checked) { - setFieldError("agree", "请先同意用户条款与隐私政策"); - valid = false; - } - - if (!valid) return null; - return { accountNo: account, password: pwd }; - } - - function mapServerError(err) { - var tip = bindingMessage(err && err.message, "绑定失败,请稍后重试"); - var code = err && err.code; - if (code === 5005 || /已存在|重复|占用/.test(tip)) { - setFieldError("account", tip); - return; - } - if (/密码|password/i.test(tip)) { - setFieldError("password", tip); - return; - } - setFieldError("account", tip); - } - - function open(event) { - if (event) event.preventDefault(); - closeLogin(); - closeUserMenu(); - clearAllErrors(); - root.classList.add("is-open"); - previousOverflow = document.body.style.overflow; - document.body.style.overflow = "hidden"; - var first = root.querySelector(".register-pop__input"); - if (first) { - window.setTimeout(function () { - first.focus(); - }, 0); - } - } - - function close() { - root.classList.remove("is-open"); - document.body.style.overflow = previousOverflow; - clearAllErrors(); - } - - window.openRegisterPop = open; - window.closeRegisterPop = close; - - function bindToggle(key, input) { - var btn = root.querySelector('[data-register-toggle="' + key + '"]'); - var icon = root.querySelector('[data-register-toggle-icon="' + key + '"]'); - if (!btn || !input || !icon) return; - btn.addEventListener("click", function () { - var show = input.getAttribute("type") === "password"; - input.setAttribute("type", show ? "text" : "password"); - icon.setAttribute("src", show ? "/static/images/eye-off.svg" : "/static/images/eye.svg"); - btn.setAttribute("aria-label", show ? "隐藏密码" : "显示密码"); - }); - } - - document.addEventListener("click", function (event) { - var opener = event.target.closest("[data-register-open]"); - if (opener) { - open(event); - return; - } - - // 站内注册入口统一使用绑定账号弹窗 - var registerLink = event.target.closest('a[href="/register"], a[href="/register/"], a[href="#register"]'); - if (registerLink) { - open(event); - return; - } - - if (!root.classList.contains("is-open")) return; - if (event.target.closest("[data-register-close]")) { - close(); - } - }); - - bindToggle("password", password); - bindToggle("confirm", confirm); - - function setSubmitting(loading) { - if (!submitBtn) return; - submitBtn.disabled = !!loading; - submitBtn.classList.toggle("is-loading", !!loading); - submitBtn.textContent = loading ? "绑定中…" : "绑定"; - } - - if (form) { - form.querySelectorAll("input").forEach(function (input) { - input.addEventListener("input", function () { - var name = input.getAttribute("name"); - if (name === "agree") clearFieldError("agree"); - else if (name) clearFieldError(name); - }); - if (input.type === "checkbox") { - input.addEventListener("change", function () { - clearFieldError("agree"); - }); - } - }); - - form.addEventListener("submit", function (event) { - event.preventDefault(); - if (!window.AuthAPI || !window.HttpRequest) { - setFieldError("account", "认证脚本未加载,请刷新页面"); - return; - } - if (submitBtn && submitBtn.classList.contains("is-loading")) return; - - var payload = validateForm(); - if (!payload) return; - - setSubmitting(true); - AuthAPI.register(payload) - .then(function (res) { - AuthAPI.toast(bindingMessage(res && res.tip, "绑定成功"), "success"); - window.setTimeout(function () { - close(); - window.location.reload(); - }, 400); - }) - .catch(function (err) { - setSubmitting(false); - mapServerError(err); - AuthAPI.toast(bindingMessage(err && err.message, "绑定失败"), "error"); - }); - }); - } - - document.addEventListener("keydown", function (event) { - if (event.key === "Escape" && root.classList.contains("is-open")) { - close(); - } - }); - - try { - var params = new URLSearchParams(window.location.search || ""); - if (params.get("auth") === "register") { - params.delete("auth"); - var qs = params.toString(); - window.history.replaceState(null, "", window.location.pathname + (qs ? "?" + qs : "") + window.location.hash); - open(); - } - } catch (e) {} -})(); diff --git a/app/static/js/recharge-modal.js b/app/static/js/recharge-modal.js deleted file mode 100644 index 9394129..0000000 --- a/app/static/js/recharge-modal.js +++ /dev/null @@ -1,246 +0,0 @@ -(function () { - var root = document.querySelector("[data-recharge-modal]"); - if (!root) return; - - var panel = root.querySelector(".recharge-modal__panel"); - var openers = document.querySelectorAll("[data-recharge-open]"); - var closers = root.querySelectorAll("[data-recharge-close]"); - var packagesRoot = root.querySelector("[data-recharge-packages]"); - var methodsRoot = root.querySelector("[data-recharge-methods]"); - var amount = root.querySelector("[data-recharge-amount]"); - var pay = root.querySelector("[data-recharge-pay]"); - var status = root.querySelector("[data-recharge-status]"); - var previousOverflow = ""; - var returnFocus = null; - var paying = false; - - function packages() { - return root.querySelectorAll("[data-recharge-package]"); - } - - function methods() { - return root.querySelectorAll("[data-recharge-method]"); - } - - function showStatus(message, ok) { - if (!status) return; - status.textContent = message || ""; - status.hidden = !message; - status.classList.toggle("is-ok", !!ok); - } - - function open(trigger) { - if (!root.hidden) return; - - var oldDialog = document.querySelector("[data-play-coin].is-open"); - if (oldDialog) { - var oldClose = oldDialog.querySelector("button[data-play-coin-close]"); - if (oldClose) oldClose.click(); - } - - returnFocus = trigger && oldDialog ? document.querySelector("[data-play-coin-open]") : trigger; - previousOverflow = document.body.style.overflow; - showStatus(""); - root.hidden = false; - document.body.style.overflow = "hidden"; - if (panel) panel.focus(); - } - - function close() { - if (root.hidden) return; - root.hidden = true; - document.body.style.overflow = previousOverflow; - if (returnFocus && typeof returnFocus.focus === "function") returnFocus.focus(); - } - - function parsePayMethods(raw) { - try { - var list = JSON.parse(raw || "[]"); - return Array.isArray(list) ? list : []; - } catch (e) { - return []; - } - } - - function renderMethods(list) { - if (!methodsRoot) return; - methodsRoot.replaceChildren(); - if (!list.length) { - var empty = document.createElement("p"); - empty.className = "recharge-modal__empty"; - empty.textContent = "该套餐暂无可用支付方式"; - methodsRoot.appendChild(empty); - return; - } - list.forEach(function (item, index) { - var type = String((item && item.type) || "").trim(); - var name = String((item && item.typeName) || type || "支付").trim(); - if (!type) return; - var btn = document.createElement("button"); - btn.className = "recharge-modal__method" + (index === 0 ? " is-selected" : ""); - btn.type = "button"; - btn.setAttribute("data-recharge-method", type); - btn.setAttribute("aria-pressed", index === 0 ? "true" : "false"); - btn.innerHTML = - '' + - '即时到账'; - btn.querySelector(".recharge-modal__method-copy span").textContent = name; - methodsRoot.appendChild(btn); - }); - if (typeof window.initLazyImages === "function") { - window.initLazyImages(methodsRoot); - } - } - - function selectPackage(button) { - Array.prototype.forEach.call(packages(), function (item) { - var selected = item === button; - item.classList.toggle("is-selected", selected); - item.setAttribute("aria-pressed", String(selected)); - }); - if (amount) amount.textContent = "¥" + (button.getAttribute("data-price") || "0"); - renderMethods(parsePayMethods(button.getAttribute("data-pay-methods"))); - showStatus(""); - } - - function bindMethodClicks() { - if (!methodsRoot) return; - methodsRoot.addEventListener("click", function (event) { - var button = event.target.closest("[data-recharge-method]"); - if (!button || !methodsRoot.contains(button)) return; - Array.prototype.forEach.call(methods(), function (item) { - var selected = item === button; - item.classList.toggle("is-selected", selected); - item.setAttribute("aria-pressed", String(selected)); - }); - showStatus(""); - }); - } - - Array.prototype.forEach.call(openers, function (button) { - button.addEventListener("click", function (event) { - event.preventDefault(); - open(button); - }); - }); - - Array.prototype.forEach.call(closers, function (element) { - element.addEventListener("click", close); - }); - - if (packagesRoot) { - packagesRoot.addEventListener("click", function (event) { - var button = event.target.closest("[data-recharge-package]"); - if (!button || !packagesRoot.contains(button)) return; - selectPackage(button); - }); - } - - bindMethodClicks(); - - if (pay) { - pay.addEventListener("click", function () { - if (paying) return; - var selectedPackage = root.querySelector("[data-recharge-package].is-selected"); - var selectedMethod = root.querySelector("[data-recharge-method].is-selected"); - var packageId = selectedPackage ? Number(selectedPackage.getAttribute("data-package-id") || 0) : 0; - var payMode = selectedMethod ? selectedMethod.getAttribute("data-recharge-method") : ""; - if (!packageId) { - showStatus("请选择充值套餐"); - return; - } - if (!payMode) { - showStatus("请选择支付方式"); - return; - } - paying = true; - pay.disabled = true; - showStatus("正在创建支付订单…"); - var payload = { packageId: packageId, payMode: payMode }; - var req = - window.HttpRequest && typeof window.HttpRequest.post === "function" - ? window.HttpRequest.post("/api/coin/buy", payload) - : fetch("/api/coin/buy", { - method: "POST", - credentials: "same-origin", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }).then(function (r) { - return r.json().then(function (body) { - return body; - }); - }); - req - .then(function (result) { - if (!result || Number(result.code) !== 200 || !result.data || !result.data.payUrl) { - throw new Error((result && (result.tip || result.msg)) || "创建支付订单失败"); - } - showStatus("订单创建成功,正在前往支付…", true); - openPayInNewWindow(result.data.payUrl); - paying = false; - pay.disabled = false; - }) - .catch(function (error) { - showStatus((error && error.message) || "创建支付订单失败,请稍后重试"); - paying = false; - pay.disabled = false; - }); - }); - } - - function openPayInNewWindow(url) { - url = String(url || "").trim(); - if (!url) return; - var opened = null; - try { - opened = window.open(url, "_blank", "noopener,noreferrer"); - } catch (e) { - opened = null; - } - if (opened) { - try { - opened.opener = null; - } catch (e2) {} - showStatus("已在新窗口打开支付页面,请完成支付后返回本页", true); - return; - } - // 弹窗被拦截时用 再试,仍不跳转当前页 - var link = document.createElement("a"); - link.href = url; - link.target = "_blank"; - link.rel = "noopener noreferrer"; - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - showStatus("若未打开支付页,请允许本站弹窗后重试", true); - } - - document.addEventListener("keydown", function (event) { - if (root.hidden) return; - if (event.key === "Escape") { - event.preventDefault(); - close(); - return; - } - if (event.key !== "Tab") return; - - var focusable = root.querySelectorAll("button:not([disabled]), a[href]"); - if (!focusable.length) return; - var first = focusable[0]; - var last = focusable[focusable.length - 1]; - if (event.shiftKey && (document.activeElement === first || document.activeElement === panel)) { - event.preventDefault(); - last.focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first.focus(); - } - }); - - window.openRechargeModal = function (trigger) { - open(trigger || null); - }; - - if (new URLSearchParams(window.location.search).get("recharge") === "1") open(null); -})(); diff --git a/app/static/js/recharge.js b/app/static/js/recharge.js deleted file mode 100644 index e75a9b6..0000000 --- a/app/static/js/recharge.js +++ /dev/null @@ -1,322 +0,0 @@ -(() => { - const root = document.querySelector('[data-recharge]'); - if (!root) return; - - const packages = Array.from(root.querySelectorAll('[data-recharge-package]')); - const methodsRoot = root.querySelector('[data-recharge-methods]'); - const methodEmpty = root.querySelector('[data-recharge-method-empty]'); - const payButton = root.querySelector('[data-recharge-pay]'); - const price = root.querySelector('[data-recharge-price]'); - const coins = root.querySelector('[data-recharge-coins]'); - const status = root.querySelector('[data-recharge-status]'); - const rowsRoot = root.querySelector('[data-recharge-rows]'); - const filters = Array.from(root.querySelectorAll('[data-recharge-filter]')); - const pagination = root.querySelector('[data-recharge-pagination]'); - const pageButtons = root.querySelector('[data-recharge-pages]'); - const previous = root.querySelector('[data-recharge-prev]'); - const next = root.querySelector('[data-recharge-next]'); - const total = root.querySelector('[data-recharge-total]'); - const empty = root.querySelector('[data-recharge-history-empty]'); - const more = root.querySelector('[data-recharge-more]'); - const exportBtn = root.querySelector('[data-recharge-export]'); - const mobile = window.matchMedia('(max-width: 768px)'); - let selectedPackage = null; - let selectedMethod = ''; - let activeType = 0; - let currentPage = Number(pagination.dataset.page) || 1; - let pageSize = Number(pagination.dataset.pageSize) || 5; - let totalCount = Number(pagination.dataset.total) || 0; - let expanded = false; - - function showStatus(message, success) { - status.textContent = message; - status.hidden = !message; - status.classList.toggle('is-success', Boolean(success)); - } - - function choose(button, choices) { - choices.forEach(choice => { - const isSelected = choice === button; - choice.classList.toggle('is-selected', isSelected); - choice.setAttribute('aria-checked', String(isSelected)); - }); - } - - function parseMethods(button) { - try { - const parsed = JSON.parse(button.dataset.payMethods || '[]'); - return Array.isArray(parsed) ? parsed : []; - } catch (_) { - return []; - } - } - - function renderMethods(items) { - methodsRoot.replaceChildren(); - selectedMethod = ''; - items.forEach((item, index) => { - const type = String(item.type || '').trim(); - if (!type) return; - const button = document.createElement('button'); - button.className = 'recharge-method'; - button.type = 'button'; - button.setAttribute('role', 'radio'); - button.dataset.rechargeMethod = type; - button.innerHTML = '支付成功后到账'; - button.querySelector('strong').textContent = item.typeName || type; - button.addEventListener('click', () => { - const choices = Array.from(methodsRoot.querySelectorAll('[data-recharge-method]')); - choose(button, choices); - selectedMethod = type; - showStatus(''); - }); - methodsRoot.appendChild(button); - if (index === 0) { - button.classList.add('is-selected'); - button.setAttribute('aria-checked', 'true'); - selectedMethod = type; - } else { - button.setAttribute('aria-checked', 'false'); - } - }); - const hasMethod = Boolean(selectedMethod); - methodEmpty.hidden = hasMethod; - payButton.disabled = !selectedPackage || !hasMethod; - if (typeof window.initLazyImages === 'function') { - window.initLazyImages(methodsRoot); - } - } - - function selectPackage(button) { - selectedPackage = button; - choose(button, packages); - price.textContent = button.dataset.price || '—'; - coins.textContent = button.dataset.coins || '—'; - renderMethods(parseMethods(button)); - showStatus(''); - } - - packages.forEach(button => button.addEventListener('click', () => selectPackage(button))); - const initialPackage = packages.find(button => button.classList.contains('is-selected')) || packages[0]; - if (initialPackage) selectPackage(initialPackage); - else payButton.disabled = true; - - payButton.addEventListener('click', async () => { - if (!selectedPackage || !selectedMethod) { - showStatus('请选择套餐和支付方式'); - return; - } - payButton.disabled = true; - showStatus('正在创建支付订单…'); - const payload = { - packageId: Number(selectedPackage.dataset.packageId), - payMode: selectedMethod, - }; - try { - let result; - if (window.HttpRequest && typeof window.HttpRequest.post === 'function') { - result = await window.HttpRequest.post('/api/coin/buy', payload); - } else { - const response = await fetch('/api/coin/buy', { - method: 'POST', - credentials: 'same-origin', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify(payload), - }); - result = await response.json(); - } - if (!result || Number(result.code) !== 200 || !result.data || !result.data.payUrl) { - if (result && (Number(result.code) === 401 || Number(result.code) === 1000)) { - const login = document.querySelector('[data-login-open]'); - if (login) login.click(); - else window.location.href = '/login'; - return; - } - throw new Error((result && (result.tip || result.msg)) || '创建支付订单失败'); - } - showStatus('订单创建成功,正在前往支付…', true); - openPayInNewWindow(result.data.payUrl); - payButton.disabled = false; - } catch (error) { - showStatus(error.message || '创建支付订单失败,请稍后重试'); - payButton.disabled = false; - } - }); - - function openPayInNewWindow(url) { - url = String(url || '').trim(); - if (!url) return; - let opened = null; - try { - opened = window.open(url, '_blank', 'noopener,noreferrer'); - } catch (e) { - opened = null; - } - if (opened) { - try { - opened.opener = null; - } catch (e2) {} - showStatus('已在新窗口打开支付页面,请完成支付后返回本页', true); - return; - } - const link = document.createElement('a'); - link.href = url; - link.target = '_blank'; - link.rel = 'noopener noreferrer'; - link.style.display = 'none'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - showStatus('若未打开支付页,请允许本站弹窗后重试', true); - } - - function appendText(parent, tag, className, value) { - const node = document.createElement(tag); - node.className = className; - node.textContent = value || ''; - parent.appendChild(node); - return node; - } - - function renderRows(items) { - rowsRoot.replaceChildren(); - items.forEach(item => { - const row = document.createElement('div'); - row.className = 'recharge-history__row'; - row.dataset.rechargeRow = ''; - row.dataset.category = item.category || ''; - appendText(row, 'span', 'recharge-history__kind', item.kind); - const detail = document.createElement('span'); - detail.className = 'recharge-history__detail-line'; - appendText(detail, 'span', 'recharge-history__description', item.description); - const time = document.createElement('time'); - time.className = 'recharge-history__time'; - appendText(time, 'span', 'only-desktop', item.time); - appendText(time, 'span', 'only-mobile', item.mobileTime); - detail.appendChild(time); - row.appendChild(detail); - const amount = appendText(row, 'strong', 'recharge-history__amount', item.amount); - amount.classList.toggle('is-credit', Boolean(item.credit)); - rowsRoot.appendChild(row); - }); - empty.hidden = items.length > 0; - empty.textContent = '暂无记录'; - } - - function renderPagination() { - const pageCount = Math.ceil(totalCount / pageSize); - currentPage = Math.min(currentPage, Math.max(1, pageCount)); - pagination.hidden = totalCount === 0; - more.hidden = totalCount <= 5 && !expanded; - previous.disabled = currentPage <= 1; - next.disabled = currentPage >= pageCount; - total.textContent = '共 ' + totalCount + ' 条'; - pageButtons.replaceChildren(); - const firstPage = Math.max(1, Math.min(currentPage - 1, Math.max(1, pageCount - 3))); - for (let page = firstPage; page <= Math.min(pageCount, firstPage + 3); page += 1) { - const button = document.createElement('button'); - button.type = 'button'; - button.textContent = String(page); - button.classList.toggle('is-active', page === currentPage); - if (page === currentPage) button.setAttribute('aria-current', 'page'); - button.addEventListener('click', () => loadHistory(page)); - pageButtons.appendChild(button); - } - } - - async function loadHistory(page) { - empty.hidden = false; - empty.textContent = '加载中…'; - try { - const query = new URLSearchParams({ - page: String(page), - pageSize: String(pageSize), - type: String(activeType), - }); - const url = '/api/coin/trans/list?' + query.toString(); - let result; - if (window.HttpRequest && typeof window.HttpRequest.get === 'function') { - result = await window.HttpRequest.get(url); - } else { - const response = await fetch(url, {credentials: 'same-origin'}); - result = await response.json(); - } - if (!result || Number(result.code) !== 200 || !result.data) { - throw new Error((result && (result.tip || result.msg)) || '交易明细加载失败'); - } - currentPage = Number(result.data.pageNum) || page; - totalCount = Number(result.data.totalCount) || 0; - renderRows(Array.isArray(result.data.list) ? result.data.list : []); - renderPagination(); - } catch (error) { - rowsRoot.replaceChildren(); - empty.hidden = false; - empty.textContent = error.message || '交易明细加载失败'; - pagination.hidden = true; - } - } - - async function exportHistory() { - if (!exportBtn) return; - exportBtn.disabled = true; - exportBtn.classList.add('is-loading'); - try { - const query = new URLSearchParams({type: String(activeType)}); - const url = '/api/coin/trans/export?' + query.toString(); - const response = await fetch(url, {credentials: 'same-origin'}); - const contentType = (response.headers.get('Content-Type') || '').toLowerCase(); - if (contentType.includes('application/json')) { - const result = await response.json(); - throw new Error((result && (result.tip || result.msg)) || '导出失败'); - } - if (!response.ok) { - throw new Error('导出失败'); - } - const blob = await response.blob(); - const disposition = response.headers.get('Content-Disposition') || ''; - let filename = 'coin-trans.csv'; - const match = /filename\*?=(?:UTF-8''|")?([^\";]+)/i.exec(disposition); - if (match && match[1]) { - filename = decodeURIComponent(match[1].replace(/"/g, '').trim()) || filename; - } - const objectURL = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = objectURL; - link.download = filename; - document.body.appendChild(link); - link.click(); - link.remove(); - URL.revokeObjectURL(objectURL); - showStatus('导出成功', true); - } catch (error) { - showStatus(error.message || '导出失败', false); - } finally { - exportBtn.disabled = false; - exportBtn.classList.remove('is-loading'); - } - } - - filters.forEach(button => button.addEventListener('click', () => { - activeType = Number(button.dataset.rechargeFilter) || 0; - filters.forEach(option => { - const selected = option === button; - option.classList.toggle('is-active', selected); - option.setAttribute('aria-pressed', String(selected)); - }); - loadHistory(1); - })); - previous.addEventListener('click', () => { if (currentPage > 1) loadHistory(currentPage - 1); }); - next.addEventListener('click', () => loadHistory(currentPage + 1)); - if (exportBtn) { - exportBtn.addEventListener('click', () => exportHistory()); - } - more.addEventListener('click', () => { - expanded = !expanded; - pageSize = expanded ? 100 : 5; - more.firstChild.textContent = expanded ? '收起 ' : '全部 '; - more.setAttribute('aria-expanded', String(expanded)); - loadHistory(1); - }); - - renderPagination(); -})(); diff --git a/app/static/js/setting.js b/app/static/js/setting.js deleted file mode 100644 index fa867bf..0000000 --- a/app/static/js/setting.js +++ /dev/null @@ -1,143 +0,0 @@ -(() => { - const form = document.querySelector('[data-setting-form]'); - if (!form) return; - - const status = form.querySelector('[data-setting-status]'); - const save = form.querySelector('[data-setting-save]'); - const passwordPop = document.querySelector('[data-setting-password-pop]'); - const passwordForm = document.querySelector('[data-setting-password-form]'); - const passwordOpen = document.querySelector('[data-setting-password-open]'); - const passwordStatus = document.querySelector('[data-setting-password-status]'); - const passwordSubmit = document.querySelector('[data-setting-password-submit]'); - let previousOverflow = ''; - - function showStatus(message, success = false) { - status.textContent = message; - status.classList.toggle('is-success', success); - } - - form.addEventListener('submit', async event => { - event.preventDefault(); - const data = {}; - const nickname = form.elements.nickName.value.trim(); - const introduction = form.elements.introduction.value.trim(); - - if (!nickname || [...nickname].length > 40) { - showStatus('昵称须为 1 至 40 个字符'); - form.elements.nickName.focus(); - return; - } - if ([...introduction].length > 300) { - showStatus('简介不能超过 300 个字符'); - return; - } - if (nickname !== form.elements.nickName.defaultValue) data.nickName = nickname; - if (introduction !== form.elements.introduction.defaultValue.trim()) data.introduction = introduction; - if (!Object.keys(data).length) { - showStatus('没有需要保存的修改'); - return; - } - - save.disabled = true; - showStatus('保存中…'); - try { - const response = await fetch('/api/auth/profile', { - method: 'POST', credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), - }); - const result = await response.json(); - if (!response.ok || result.code !== 200) throw new Error(result.tip || '保存失败,请稍后重试'); - showStatus('保存成功', true); - window.setTimeout(() => window.location.reload(), 800); - } catch (error) { - showStatus(error.message || '保存失败,请稍后重试'); - save.disabled = false; - } - }); - - function setPasswordStatus(message, success = false) { - if (!passwordStatus) return; - passwordStatus.textContent = message || ''; - passwordStatus.classList.toggle('is-success', success); - } - - function openPasswordPop() { - if (!passwordPop || !passwordForm) return; - passwordForm.reset(); - setPasswordStatus(''); - previousOverflow = document.body.style.overflow; - document.body.style.overflow = 'hidden'; - passwordPop.hidden = false; - const first = passwordForm.elements.oldPassword; - if (first) window.setTimeout(() => first.focus(), 0); - } - - function closePasswordPop() { - if (!passwordPop || passwordPop.hidden) return; - passwordPop.hidden = true; - document.body.style.overflow = previousOverflow; - if (passwordForm) passwordForm.reset(); - setPasswordStatus(''); - if (passwordOpen) passwordOpen.focus(); - } - - if (passwordOpen) passwordOpen.addEventListener('click', openPasswordPop); - if (passwordPop) { - passwordPop.querySelectorAll('[data-setting-password-close]').forEach(button => { - button.addEventListener('click', closePasswordPop); - }); - } - - document.addEventListener('keydown', event => { - if (event.key === 'Escape' && passwordPop && !passwordPop.hidden) closePasswordPop(); - }); - - if (passwordForm) { - passwordForm.addEventListener('submit', async event => { - event.preventDefault(); - const oldPassword = passwordForm.elements.oldPassword.value; - const newPassword = passwordForm.elements.newPassword.value; - const confirmPassword = passwordForm.elements.confirmPassword.value; - - if (oldPassword.length < 4 || oldPassword.length > 16) { - setPasswordStatus('请输入 4–16 位当前密码'); - passwordForm.elements.oldPassword.focus(); - return; - } - if (newPassword.length < 4 || newPassword.length > 16) { - setPasswordStatus('请输入 4–16 位新密码'); - passwordForm.elements.newPassword.focus(); - return; - } - if (newPassword !== confirmPassword) { - setPasswordStatus('两次输入的新密码不一致'); - passwordForm.elements.confirmPassword.focus(); - return; - } - - passwordSubmit.disabled = true; - passwordSubmit.textContent = '修改中…'; - setPasswordStatus(''); - try { - const response = await fetch('/api/auth/profile', { - method: 'POST', - credentials: 'same-origin', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ oldPassword, newPassword }), - }); - const result = await response.json(); - if (!response.ok || result.code !== 200) throw new Error(result.tip || '密码修改失败,请稍后重试'); - setPasswordStatus('密码修改成功', true); - window.setTimeout(() => { - closePasswordPop(); - window.location.reload(); - }, 600); - } catch (error) { - setPasswordStatus(error.message || '密码修改失败,请稍后重试'); - passwordSubmit.disabled = false; - passwordSubmit.textContent = '确认修改'; - } - }); - } - -})(); diff --git a/app/static/js/tracker.js b/app/static/js/tracker.js deleted file mode 100644 index 0e965ec..0000000 --- a/app/static/js/tracker.js +++ /dev/null @@ -1,921 +0,0 @@ -/** - * Web SDK v1.2.0 接入层。 - * 页面/点击/导航沿用 jha-1043 的口径,短剧使用最新版 dramaEvent/dramaStatusChange。 - */ -(function (global) { - "use strict"; - - var state = { - initialized: false, - initializing: false, - queue: [], - page: { key: "internal_other", name: "站内其他页面" }, - referrer: { key: "direct", name: "直接访问" }, - userType: "normal", - pageStartedAt: Date.now(), - dramaViewSent: false, - adShown: {}, - }; - - function stringValue(value, fallback) { - var result = value == null ? "" : String(value).trim(); - return result || (fallback == null ? "" : String(fallback)); - } - - function integer(value, fallback) { - var result = parseInt(value, 10); - return isFinite(result) ? result : (fallback || 0); - } - - function clamp(value, min, max) { - var result = Number(value); - if (!isFinite(result)) result = min; - return Math.max(min, Math.min(max, result)); - } - - function sdkAvailable() { - return !!(global.WebSDK && global.WebSDK.track); - } - - function logError() {} - - function invoke(method, payload) { - if (!state.initialized || !sdkAvailable()) { - return new Promise(function (resolve) { - state.queue.push([method, payload, resolve]); - }); - } - try { - var fn = global.WebSDK.track[method]; - if (typeof fn !== "function") { - var missing = new Error("SDK method does not exist: " + method); - logError(missing); - return Promise.resolve({ sent: false, ok: false, reason: "unknown_method" }); - } - var result = fn(payload); - return Promise.resolve(result).then(function (response) { - if (response && response.ok === false) { - logError(method + " rejected: " + JSON.stringify(response)); - } - return response; - }).catch(function (error) { - logError(error); - return { sent: false, ok: false, reason: "exception" }; - }); - } catch (error) { - logError(error); - return Promise.resolve({ sent: false, ok: false, reason: "exception" }); - } - } - - function flushQueue() { - var pending = state.queue.splice(0, state.queue.length); - pending.forEach(function (entry) { - Promise.resolve(invoke(entry[0], entry[1])).then(entry[2]); - }); - } - - function currentChannel(value) { - var channel = stringValue(value); - if (!channel && global.ChannelAttribution && typeof global.ChannelAttribution.get === "function") { - channel = stringValue(global.ChannelAttribution.get()); - } - if (!channel) channel = stringValue(global.__CHANNEL_CODE__, "organic"); - var normalized = channel.toLowerCase(); - if (["self", "natural", "official", "channel", "channel://natural"].indexOf(normalized) >= 0) { - return "organic"; - } - if (!channel || channel.length > 50 || !/^[A-Za-z0-9_.-]+$/.test(channel) || - ["{}", "0", "-", "unknown"].indexOf(normalized) >= 0) { - return "organic"; - } - return channel; - } - - function normalizeUid(value) { - var uid = stringValue(value); - if (uid === "0" || uid.length < 4 || uid.length > 32 || !/^[A-Za-z0-9_]+$/.test(uid) || - ["u_1001", "stress_user", "u_lab"].indexOf(uid.toLowerCase()) >= 0) return ""; - return uid; - } - - function inferPage(path) { - path = stringValue(path, "/").replace(/\/$/, "") || "/"; - if (path === "/") return { key: "home", name: "首页" }; - var rules = [ - [/^\/series-play(?:\/|$)/, "series_play", "AI成人剧集播放页"], - [/^\/series(?:\/|$)/, "series", "AI成人剧集"], - [/^\/play(?:\/|$)/, "play", "短剧播放页"], - [/^\/detail(?:\/|$)/, "detail", "短剧详情"], - [/^\/list(?:\/|$)/, "list", "短剧列表"], - [/^\/rank(?:\/|$)/, "rank", "排行榜"], - [/^\/topic-detail(?:\/|$)/, "topic_detail", "专题详情"], - [/^\/topic(?:\/|$)/, "topic", "专题列表"], - [/^\/tags(?:\/|$)/, "tags", "标签详情"], - [/^\/search(?:-mobile)?(?:\/|$)/, "search", "搜索结果页"], - [/^\/history(?:\/|$)/, "history", "观看历史"], - [/^\/favorites(?:\/|$)/, "favorites", "我的收藏"], - [/^\/profile(?:\/|$)/, "profile", "个人中心"], - ]; - for (var i = 0; i < rules.length; i += 1) { - if (rules[i][0].test(path)) return { key: rules[i][1], name: rules[i][2] }; - } - return { key: "internal_other", name: "站内其他页面" }; - } - - function readReferrer() { - try { - var saved = global.sessionStorage.getItem("__tracker_last_page"); - if (saved) { - var parsed = JSON.parse(saved); - if (parsed && parsed.key) return parsed; - } - } catch (error) {} - try { - if (document.referrer) { - var url = new URL(document.referrer, global.location.href); - if (url.host === global.location.host) return inferPage(url.pathname); - return { key: "external", name: url.host || "外部站点" }; - } - } catch (error) {} - return { key: "direct", name: "直接访问" }; - } - - function rememberPage() { - try { global.sessionStorage.setItem("__tracker_last_page", JSON.stringify(state.page)); } catch (error) {} - } - - function init(options) { - if (state.initialized || state.initializing) return; - options = options || {}; - var inferred = inferPage(global.location && global.location.pathname); - state.page = { - key: stringValue(options.pageKey, inferred.key), - name: stringValue(options.pageName, inferred.name), - }; - state.userType = options.userType === "vip" ? "vip" : "normal"; - state.referrer = readReferrer(); - state.initializing = true; - - var attempts = 0; - var timer = global.setInterval(function () { - attempts += 1; - if (!sdkAvailable() && attempts < 100) return; - global.clearInterval(timer); - if (!sdkAvailable()) { - state.initializing = false; - return; - } - try { - var result = global.WebSDK.init({ - appId: stringValue(options.appId), - uid: normalizeUid(options.uid), - channel: currentChannel(options.channel), - encryptedConfig: stringValue(options.encryptedConfig), - appVersion: stringValue(options.appVersion, "1.0.0"), - disableAutoEvents: ["app_page_view", "page_click"], - }); - Promise.resolve(result).then(function () { - state.initialized = true; - state.initializing = false; - __commonFieldsReady = null; - loadCommonFields(); - flushQueue(); - onReady(); - }).catch(function (error) { - state.initializing = false; - logError(error); - }); - } catch (error) { - state.initializing = false; - logError(error); - } - }, 50); - } - - function whenDOMReady(callback) { - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", callback, { once: true }); - } else { - callback(); - } - } - - function trackPageView(extra) { - extra = extra || {}; - invoke("appPageView", { - user_type: stringValue(extra.user_type, state.userType), - page_key: state.page.key, - page_name: state.page.name, - referrer_page_key: state.referrer.key, - referrer_page_name: state.referrer.name, - current_page_key: state.page.key, - current_page_name: state.page.name, - page_load_time: pageLoadDuration(), - recommend_trace_id: stringValue(extra.recommend_trace_id), - }); - rememberPage(); - } - - function pageLoadDuration() { - try { - if (global.performance && typeof global.performance.getEntriesByType === "function") { - var entries = global.performance.getEntriesByType("navigation"); - if (entries && entries[0]) { - var navigation = entries[0]; - var end = navigation.loadEventEnd || navigation.domContentLoadedEventEnd || global.performance.now(); - if (isFinite(end)) return Math.max(0, Math.round(end)); - } - } - if (global.performance && typeof global.performance.now === "function") { - return Math.max(0, Math.round(global.performance.now())); - } - } catch (error) {} - return Math.max(0, Date.now() - state.pageStartedAt); - } - - function trackPageClick(event) { - var doc = document.documentElement || {}; - var viewportWidth = global.innerWidth || doc.clientWidth || 0; - var viewportHeight = global.innerHeight || doc.clientHeight || 0; - var x = integer(event.clientX, 0); - var y = integer(event.clientY, 0); - invoke("pageClick", { - page_key: state.page.key, - page_name: state.page.name, - click_page_x: x, - click_page_y: y, - click_x_percent: viewportWidth ? Math.round(clamp(x / viewportWidth * 100, 0, 100)) : 0, - click_y_percent: viewportHeight ? Math.round(clamp(y / viewportHeight * 100, 0, 100)) : 0, - screen_width: integer(global.screen && global.screen.width, 0), - screen_height: integer(global.screen && global.screen.height, 0), - recommend_trace_id: "", - }); - } - - function trackNavigation(anchor) { - if (!anchor || (!anchor.hasAttribute("data-navigation-key") && !anchor.hasAttribute("data-nav-key")) || - !anchor.getAttribute("href")) return; - var href = anchor.getAttribute("href"); - if (!href || href.charAt(0) === "#" || /^javascript:/i.test(href)) return; - var label = stringValue(anchor.getAttribute("aria-label") || anchor.textContent).replace(/\s+/g, " "); - var navigationKey = stringValue( - anchor.getAttribute("data-navigation-key") || anchor.getAttribute("data-nav-key") - ); - if (!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(navigationKey)) { - navigationKey = ""; - try { - var url = new URL(href, global.location.href); - if (url.origin !== global.location.origin) { - navigationKey = "nav_external"; - } else { - var targetPage = inferPage(url.pathname); - if (targetPage.key !== "internal_other") { - navigationKey = "nav_" + targetPage.key; - } else { - var firstSegment = stringValue(url.pathname.split("/").filter(Boolean)[0]) - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, "") - .slice(0, 48); - navigationKey = firstSegment ? "nav_" + firstSegment : "nav_home"; - } - } - } catch (error) { - navigationKey = "nav_other"; - } - } - invoke("navigation", { - navigation_key: navigationKey.toLowerCase(), - navigation_name: stringValue( - anchor.getAttribute("data-navigation-name") || anchor.getAttribute("data-nav-name"), - label || navigationKey - ).slice(0, 255), - }); - } - - function adPayload(node) { - var id = stringValue(node && node.getAttribute("data-ad-id")); - if (!id) return null; - var adType = stringValue(node.getAttribute("data-ad-type"), "banner"); - var allSlots = document.querySelectorAll('[data-track-ad="external"][data-ad-id]'); - var slotPosition = Array.prototype.indexOf.call(allSlots, node) + 1; - if (slotPosition <= 0) slotPosition = 1; - var fallbackSlotKey = state.page.key + "_content_banner_" + slotPosition; - var fallbackSlotName = state.page.name + "内容横幅第" + slotPosition + "位"; - if (adType === "interstitial") { - fallbackSlotKey = state.page.key + "_popup_" + slotPosition; - fallbackSlotName = state.page.name + "弹窗广告位"; - } - return { - page_key: state.page.key, - page_name: state.page.name, - ad_slot_key: stringValue( - node.getAttribute("data-ad-slot-key"), - fallbackSlotKey - ), - ad_slot_name: stringValue( - node.getAttribute("data-ad-slot-name"), - fallbackSlotName - ), - ad_id: id, - creative_id: stringValue(node.getAttribute("data-creative-id")), - ad_type: adType, - }; - } - - function bindAds() { - var selector = '[data-track-ad="external"][data-ad-id]'; - var nodes = document.querySelectorAll(selector); - function show(node) { - trackExternalAd(node, "show"); - } - if (global.IntersectionObserver) { - var observer = new IntersectionObserver(function (entries) { - entries.forEach(function (entry) { - if (!entry.isIntersecting) return; - show(entry.target); - observer.unobserve(entry.target); - }); - }, { threshold: 0.1 }); - function observe(node) { - if (!node || node.__trackerAdObserved) return; - node.__trackerAdObserved = true; - observer.observe(node); - } - Array.prototype.forEach.call(nodes, observe); - if (global.MutationObserver) { - new MutationObserver(function (records) { - records.forEach(function (record) { - Array.prototype.forEach.call(record.addedNodes || [], function (node) { - if (!node || node.nodeType !== 1) return; - if (node.matches && node.matches(selector)) observe(node); - if (node.querySelectorAll) { - Array.prototype.forEach.call(node.querySelectorAll(selector), observe); - } - }); - }); - }).observe(document.documentElement, { childList: true, subtree: true }); - } - } else { - Array.prototype.forEach.call(nodes, show); - } - } - - function trackExternalAd(node, eventType) { - if (!node || (eventType !== "show" && eventType !== "click")) return; - var payload = adPayload(node); - if (!payload) return; - var key = payload.ad_slot_key + "|" + payload.ad_id; - if (!state.adShown[key]) { - state.adShown[key] = true; - invoke("adImpression", payload); - } - if (eventType === "click" && node.getAttribute("data-ad-clickable") === "1") { - return invoke("adClick", payload); - } - } - - function bindExternalAdEvents() { - document.addEventListener("external-ad:track", function (event) { - var detail = event && event.detail ? event.detail : {}; - trackExternalAd(detail.element, stringValue(detail.eventType)); - }); - } - - function trackAdvertising(node, eventType) { - if (!node || ["show", "click", "close"].indexOf(eventType) < 0) return; - invoke("advertising", { - event_type: eventType, - advertising_key: stringValue(node.getAttribute("data-advertising-key"), "dialog_ad"), - advertising_name: stringValue(node.getAttribute("data-advertising-name"), "弹窗广告"), - advertising_id: stringValue(node.getAttribute("data-advertising-id") || node.getAttribute("data-dialog-ad-id")), - }); - } - - function bindAdvertisingEvents() { - document.addEventListener("dialog-ad:track", function (event) { - var detail = event && event.detail ? event.detail : {}; - var node = detail.element; - if (!node || !node.getAttribute) return; - trackAdvertising(node, stringValue(detail.eventType)); - }); - } - - function dramaRoot() { - return document.querySelector("[data-track-drama]"); - } - - function dramaContext(root) { - root = root || dramaRoot(); - if (!root) return null; - var data = root.dataset || {}; - var dramaID = stringValue(data.dramaId); - var videoID = stringValue(data.videoId); - if (!dramaID || !videoID) return null; - return { - media_id: stringValue(data.mediaId), - drama_id: dramaID, - drama_title: stringValue(data.dramaTitle, "未命名短剧"), - episode: Math.max(1, integer(data.episode, 1)), - total_episode: Math.max(1, integer(data.totalEpisode, 1)), - video_id: videoID, - drama_type_id: stringValue(data.dramaTypeId, "default"), - drama_type_name: stringValue(data.dramaTypeName, "默认分类"), - drama_content_type: "short_drama", - recommend_trace_id: stringValue(data.recommendTraceId), - drama_tag_key: stringValue(data.dramaTagKey, "default"), - drama_tag_name: stringValue(data.dramaTagName, "默认标签"), - drama_duration: Math.max(0, integer(data.dramaDuration, 0)), - }; - } - - var dramaBehaviorNames = { - ep_view: "展示", - ep_play: "播放", - ep_pause: "暂停", - ep_forward: "快进", - ep_rewind: "快退", - ep_complete: "播放完成", - ep_share: "分享", - }; - - function videoPlaybackState(video) { - if (!video) return null; - if (!video.__trackerPlaybackState) { - video.__trackerPlaybackState = { - startedAt: 0, - elapsedMs: 0, - seekFrom: null, - lastPosition: Number(video.currentTime) || 0, - }; - } - return video.__trackerPlaybackState; - } - - function beginPlayback(video) { - var playback = videoPlaybackState(video); - if (playback && !playback.startedAt) playback.startedAt = Date.now(); - } - - function settlePlayback(video) { - var playback = videoPlaybackState(video); - if (!playback || !playback.startedAt) return; - playback.elapsedMs += Math.max(0, Date.now() - playback.startedAt); - playback.startedAt = 0; - } - - function playbackSeconds(video, keepDuration) { - var playback = videoPlaybackState(video); - if (!playback) return 0; - var now = Date.now(); - var activeMs = playback.startedAt ? Math.max(0, now - playback.startedAt) : 0; - var seconds = Math.max(0, Math.round((playback.elapsedMs + activeMs) / 1000)); - if (!keepDuration) { - playback.elapsedMs = 0; - if (playback.startedAt) playback.startedAt = now; - } - return seconds; - } - - function videoMetrics(video, context, keepDuration) { - var duration = video && isFinite(video.duration) ? Math.round(video.duration) : context.drama_duration; - var current = video && isFinite(video.currentTime) ? Math.round(video.currentTime) : 0; - return { - duration: Math.max(0, duration), - current: Math.max(0, current), - played: playbackSeconds(video, keepDuration), - progress: duration > 0 ? Math.round(clamp(current / duration * 100, 0, 100)) : 0, - speed: Math.max(1, Math.round(((video && video.playbackRate) || 1) * 100)), - }; - } - - function trackDramaEvent(behaviorKey, video, options) { - var context = dramaContext(); - if (!context) return; - options = options || {}; - var metrics = videoMetrics(video, context, options.keepPlaybackDuration === true); - context.drama_duration = metrics.duration; - context.play_duration = metrics.played; - context.play_progress = options.forceProgress != null - ? Math.round(clamp(options.forceProgress, 0, 100)) - : behaviorKey === "ep_complete" ? 100 : metrics.progress; - context.play_speed = metrics.speed; - context.play_behavior_key = behaviorKey; - context.play_behavior_name = dramaBehaviorNames[behaviorKey] || behaviorKey; - return invoke("dramaEvent", context); - } - - function trackDramaStatusChange(targetEpisode, targetVideoID, behaviorOverride) { - var context = dramaContext(); - if (!context) return; - var nextEpisode = Math.max(1, integer(targetEpisode, context.episode)); - var videoID = stringValue(targetVideoID, context.video_id); - var behavior = stringValue(behaviorOverride); - var names = { - ep_auto_play: "自动播放", - ep_previous: "上一集", - ep_next: "下一集", - ep_switch: "切换剧集", - ep_unlock: "解锁", - }; - if (!behavior) { - if (nextEpisode === context.episode && videoID === context.video_id) return; - behavior = "ep_switch"; - if (nextEpisode === context.episode + 1) behavior = "ep_next"; - else if (nextEpisode === context.episode - 1) behavior = "ep_previous"; - } - if (!names[behavior]) return; - return invoke("dramaStatusChange", { - media_id: context.media_id, - drama_id: context.drama_id, - drama_title: context.drama_title, - episode: nextEpisode, - video_id: videoID, - pre_episode: context.episode, - drama_behavior_key: behavior, - drama_behavior_name: names[behavior], - }); - } - - function trackDramaUnlock() { - var context = dramaContext(); - if (!context) return Promise.resolve({ sent: false, ok: false, reason: "no_drama_context" }); - return trackDramaStatusChange(context.episode, context.video_id, "ep_unlock"); - } - - function isMainDramaVideo(root, video) { - var mode = video && video.__trackerDramaMode; - if (!mode && root) { - mode = root.getAttribute("data-play-mode") === "preview" ? "preview" : "main"; - } - return !!( - root && - video && - video.tagName === "VIDEO" && - root.contains(video) && - mode !== "preview" - ); - } - - function rememberDramaVideoMode(root, video) { - if (!root || !video || video.tagName !== "VIDEO" || !root.contains(video)) return; - video.__trackerDramaMode = root.getAttribute("data-play-mode") === "preview" ? "preview" : "main"; - } - - function prepareDramaModeChange(nextMode) { - var root = dramaRoot(); - var video = root && root.querySelector("video"); - if (!root || !video) return; - var previousMode = video.__trackerDramaMode || - (root.getAttribute("data-play-mode") === "preview" ? "preview" : "main"); - nextMode = nextMode === "preview" ? "preview" : "main"; - if (previousMode === nextMode) return; - - if (previousMode === "main") { - var playback = video.__trackerPlaybackState; - var wasPlaying = !!(playback && (playback.startedAt || playback.elapsedMs > 0)); - settlePlayback(video); - if (wasPlaying) trackDramaEvent("ep_pause", video); - } - video.__trackerPlaybackState = null; - video.__trackerDramaMode = nextMode; - } - - function bindDramaEvents() { - var root = dramaRoot(); - if (!root) return; - if (!state.dramaViewSent) { - state.dramaViewSent = true; - trackDramaEvent("ep_view", null); - } - document.addEventListener("play", function (event) { - rememberDramaVideoMode(root, event.target); - if (isMainDramaVideo(root, event.target)) { - trackDramaEvent("ep_play", event.target); - } - }, true); - document.addEventListener("playing", function (event) { - rememberDramaVideoMode(root, event.target); - if (isMainDramaVideo(root, event.target)) beginPlayback(event.target); - }, true); - document.addEventListener("loadedmetadata", function (event) { - rememberDramaVideoMode(root, event.target); - }, true); - document.addEventListener("waiting", function (event) { - if (isMainDramaVideo(root, event.target)) settlePlayback(event.target); - }, true); - document.addEventListener("pause", function (event) { - if (isMainDramaVideo(root, event.target) && !event.target.ended) { - var playback = event.target.__trackerPlaybackState; - var wasPlaying = !!(playback && (playback.startedAt || playback.elapsedMs > 0)); - settlePlayback(event.target); - if (event.target.seeking || !wasPlaying) return; - trackDramaEvent("ep_pause", event.target); - } - }, true); - document.addEventListener("timeupdate", function (event) { - if (!isMainDramaVideo(root, event.target) || event.target.seeking) return; - videoPlaybackState(event.target).lastPosition = Number(event.target.currentTime) || 0; - }, true); - document.addEventListener("seeking", function (event) { - if (!isMainDramaVideo(root, event.target)) return; - var playback = videoPlaybackState(event.target); - if (playback.seekFrom != null) return; - settlePlayback(event.target); - playback.seekFrom = playback.lastPosition; - }, true); - document.addEventListener("seeked", function (event) { - if (!isMainDramaVideo(root, event.target)) return; - var playback = videoPlaybackState(event.target); - if (playback.seekFrom == null) return; - var from = playback.seekFrom; - var target = Number(event.target.currentTime) || 0; - playback.seekFrom = null; - playback.lastPosition = target; - if (Math.abs(target - from) >= 0.5) { - trackDramaEvent(target > from ? "ep_forward" : "ep_rewind", event.target); - } - if (!event.target.paused && !event.target.ended) beginPlayback(event.target); - }, true); - document.addEventListener("ended", function (event) { - if (isMainDramaVideo(root, event.target)) { - settlePlayback(event.target); - trackDramaEvent("ep_complete", event.target, { keepPlaybackDuration: true }); - trackDramaEvent("ep_play", event.target, { forceProgress: 100 }); - } - }, true); - - function reportPendingDramaProgress() { - var video = root.querySelector("video"); - if (!isMainDramaVideo(root, video)) return false; - var playback = video.__trackerPlaybackState; - if (!playback || (!playback.startedAt && playback.elapsedMs <= 0)) return false; - var pendingMs = playback.elapsedMs + - (playback.startedAt ? Math.max(0, Date.now() - playback.startedAt) : 0); - if (pendingMs < 500) return false; - settlePlayback(video); - trackDramaEvent("ep_play", video); - return true; - } - - function flushSDKAfterEnqueue() { - if (!global.WebSDK || typeof global.WebSDK.flush !== "function") return; - function flush() { - Promise.resolve(global.WebSDK.flush()).catch(logError); - } - Promise.resolve().then(function () { - return Promise.resolve(); - }).then(flush); - global.setTimeout(flush, 50); - global.setTimeout(flush, 200); - } - - document.addEventListener("visibilitychange", function () { - var video = root.querySelector("video"); - if (document.visibilityState === "hidden") { - if (reportPendingDramaProgress()) flushSDKAfterEnqueue(); - if (isMainDramaVideo(root, video) && !video.paused && !video.ended) beginPlayback(video); - return; - } - if (document.visibilityState === "visible" && isMainDramaVideo(root, video) && - !video.paused && !video.ended) { - beginPlayback(video); - } - }); - global.addEventListener("beforeunload", function () { - if (reportPendingDramaProgress()) flushSDKAfterEnqueue(); - }); - global.addEventListener("pagehide", function () { - if (reportPendingDramaProgress()) flushSDKAfterEnqueue(); - }); - } - - function bindSearchEvents() { - var container = document.querySelector("[data-search-results]"); - if (!container) return; - var keyword = stringValue(container.getAttribute("data-search-keyword")); - if (!keyword) return; - var grid = container.querySelector("[data-search-grid]"); - var cards = grid ? Array.prototype.slice.call(grid.querySelectorAll("[data-search-item-id]")) : []; - var resultCount = Math.max(0, integer(container.getAttribute("data-search-result-count"), cards.length)); - var pageNum = Math.max(1, integer(container.getAttribute("data-search-page"), 1)); - var pageSize = Math.max(1, integer(container.getAttribute("data-search-page-size"), cards.length || 1)); - if (pageNum === 1) { - invoke("keywordSearch", { - keyword: keyword, - search_result_count: resultCount, - search_content_type: "", - search_trace_id: "", - search_id: "", - }); - } - if (!grid) return; - grid.addEventListener("click", function (event) { - var target = event.target && event.target.closest ? event.target : null; - var resultLink = target && target.closest("[data-search-result-click]"); - var card = resultLink && resultLink.closest("[data-search-item-id]"); - if (!card || !grid.contains(card)) return; - var position = cards.indexOf(card); - if (position < 0) return; - invoke("keywordClick", { - keyword: keyword, - click_item_id: stringValue(card.getAttribute("data-search-item-id")), - click_item_type_key: "short_drama", - click_item_type_name: "短剧", - click_position: (pageNum - 1) * pageSize + position + 1, - search_trace_id: "", - }); - }, true); - } - - function bindClicks() { - document.addEventListener("click", function (event) { - var target = event.target && event.target.closest ? event.target : null; - if (!target) return; - var popupClose = target.closest( - "[data-dialog-ad-close],[data-ad-close],[data-register-close]," + - "[data-download-close],[data-login-close],[data-recharge-close]," + - "[data-play-coin-close],[data-setting-password-close],[data-drawer-close]" - ); - if (!popupClose) trackPageClick(event); - var switcher = target.closest("[data-drama-switch]"); - if (switcher) { - trackDramaStatusChange(switcher.getAttribute("data-target-episode"), switcher.getAttribute("data-target-video-id")); - } - if (target.closest("[data-share-page]") && dramaRoot()) { - var root = dramaRoot(); - var video = root.querySelector("video"); - trackDramaEvent("ep_share", isMainDramaVideo(root, video) ? video : null); - } - var ad = target.closest('[data-track-ad="external"][data-ad-clickable="1"][data-ad-id]'); - if (ad) { - trackExternalAd(ad, "click"); - } - trackNavigation(target.closest("a[data-navigation-key],a[data-nav-key]")); - }, true); - } - - function onReady() { - whenDOMReady(function () { - Promise.resolve(loadCommonFields()).then(function () { - trackPageView(); - }); - bindClicks(); - bindAds(); - bindExternalAdEvents(); - bindAdvertisingEvents(); - bindDramaEvents(); - bindSearchEvents(); - try { - global.dispatchEvent(new CustomEvent("tracker:ready", { detail: state.page })); - } catch (error) {} - }); - } - - - var COMMON_FIELD_KEYS = [ - "sid", - "device_id", - "device", - "device_fingerprint", - "fp_version", - ]; - var __commonFields = null; - var __commonFieldsReady = null; - - function emptyCommonFields() { - var out = {}; - for (var i = 0; i < COMMON_FIELD_KEYS.length; i++) { - out[COMMON_FIELD_KEYS[i]] = ""; - } - return out; - } - - function normalizeCommonFields(fields) { - var out = emptyCommonFields(); - fields = fields || {}; - for (var i = 0; i < COMMON_FIELD_KEYS.length; i++) { - var k = COMMON_FIELD_KEYS[i]; - if (fields[k] != null) out[k] = String(fields[k]); - } - return out; - } - - function publishCommonFields(fields) { - __commonFields = normalizeCommonFields(fields); - try { - global.__trackerCommonFields = __commonFields; - } catch (e) {} - try { - if (typeof CustomEvent === "function") { - global.dispatchEvent( - new CustomEvent("tracker:common-fields", { detail: __commonFields }) - ); - } - } catch (e) {} - return __commonFields; - } - - function loadCommonFields() { - if (__commonFieldsReady) return __commonFieldsReady; - __commonFieldsReady = new Promise(function (resolve) { - function finish(fields) { - resolve(publishCommonFields(fields)); - } - if (!global.WebSDK || typeof global.WebSDK.getCommonFields !== "function") { - finish(emptyCommonFields()); - return; - } - try { - Promise.resolve(global.WebSDK.getCommonFields(COMMON_FIELD_KEYS.slice())) - .then(function (fields) { - finish(fields || emptyCommonFields()); - }) - .catch(function (err) { - logError(err); - finish(emptyCommonFields()); - }); - } catch (err) { - logError(err); - finish(emptyCommonFields()); - } - }); - return __commonFieldsReady; - } - - function getCommonFieldsCached(keys) { - return loadCommonFields().then(function (all) { - if (!Array.isArray(keys) || !keys.length) return all; - var out = {}; - for (var i = 0; i < keys.length; i++) { - var k = String(keys[i] || "").trim(); - if (k) out[k] = Object.prototype.hasOwnProperty.call(all, k) ? all[k] : ""; - } - return out; - }); - } - - function getCommonFieldCached(key) { - var k = String(key || "").trim(); - if (!k) return Promise.resolve(""); - return getCommonFieldsCached([k]).then(function (m) { - return m[k] || ""; - }); - } - - function encodeSDKCommonHeader(fields) { - var payload = normalizeCommonFields(fields); - try { - return btoa(unescape(encodeURIComponent(JSON.stringify(payload)))); - } catch (e) { - logError(e); - return ""; - } - } - - function getSDKCommonHeader() { - return loadCommonFields().then(function (fields) { - return encodeSDKCommonHeader(fields); - }); - } - - - global.trackerInit = init; - global.tracker = { - init: init, - pageView: trackPageView, - dramaEvent: trackDramaEvent, - dramaStatusChange: trackDramaStatusChange, - dramaUnlock: trackDramaUnlock, - prepareDramaModeChange: prepareDramaModeChange, - advertising: function (payload) { return invoke("advertising", payload || {}); }, - externalAd: trackExternalAd, - getDramaContext: function () { - var context = dramaContext(); - return context ? Object.assign({}, context) : {}; - }, - keywordSearch: function (payload) { return invoke("keywordSearch", payload || {}); }, - keywordClick: function (payload) { return invoke("keywordClick", payload || {}); }, - getCommonFields: getCommonFieldsCached, - getCommonField: getCommonFieldCached, - getCommonFieldsSync: function () { return __commonFields; }, - encodeSDKCommonHeader: encodeSDKCommonHeader, - getSDKCommonHeader: getSDKCommonHeader, - flush: function () { - if (!global.WebSDK || typeof global.WebSDK.flush !== "function") return Promise.resolve(); - return Promise.resolve(global.WebSDK.flush()).catch(logError); - }, - setUid: function (uid) { - if (global.WebSDK && global.WebSDK.setUid) global.WebSDK.setUid(normalizeUid(uid)); - }, - setChannel: function (channel) { - if (global.WebSDK && global.WebSDK.setChannel) global.WebSDK.setChannel(currentChannel(channel)); - }, - setContext: function (context) { - if (global.WebSDK && global.WebSDK.setContext) global.WebSDK.setContext(context || {}); - }, - }; - - init(global.__TRACKER_BOOT__ || {}); -})(window); diff --git a/app/template/base/accountPagination.html b/app/template/base/accountPagination.html deleted file mode 100644 index 59ac9fe..0000000 --- a/app/template/base/accountPagination.html +++ /dev/null @@ -1,9 +0,0 @@ -{{define "accountPagination"}} -{{with .paging}}{{if gt .Total 1}} - -{{end}}{{end}} -{{end}} diff --git a/app/template/base/ads.html b/app/template/base/ads.html index 440f71f..77cddf3 100644 --- a/app/template/base/ads.html +++ b/app/template/base/ads.html @@ -5,7 +5,7 @@
    {{range .}} - + {{if .Title}}{{.Title}}{{else}}推荐{{end}} 广告 diff --git a/app/template/base/trackerInit.html b/app/template/base/analytics.html similarity index 72% rename from app/template/base/trackerInit.html rename to app/template/base/analytics.html index c3e56a0..9d87bf3 100644 --- a/app/template/base/trackerInit.html +++ b/app/template/base/analytics.html @@ -1,4 +1,4 @@ -{{define "trackerInit"}} +{{define "analytics"}} - - - - {{end}} diff --git a/app/template/base/cardMeta.html b/app/template/base/cardMeta.html new file mode 100644 index 0000000..b2276ab --- /dev/null +++ b/app/template/base/cardMeta.html @@ -0,0 +1 @@ +{{define "cardEpChip"}}{{if eq .UpdateStatus 1}}{{if .TotalEpisode}}更新至 {{.TotalEpisode}} 集{{else}}连载中{{end}}{{else if .TotalEpisode}}全 {{.TotalEpisode}} 集{{else}}{{updateStatusText .UpdateStatus}}{{end}}{{end}} diff --git a/app/template/base/dialogAd.html b/app/template/base/dialogAd.html index a5f40e4..f40a081 100644 --- a/app/template/base/dialogAd.html +++ b/app/template/base/dialogAd.html @@ -13,14 +13,6 @@ data-dialog-ad-item data-dialog-ad-index="{{$index}}" data-dialog-ad-id="{{if $dialog.AdvertiseCode}}{{$dialog.AdvertiseCode}}{{else}}{{$dialog.ID}}{{end}}" - data-track-ad="external-dialog" - data-ad-id="{{if $dialog.AdvertiseCode}}{{$dialog.AdvertiseCode}}{{else}}{{$dialog.ID}}{{end}}" - data-creative-id="" - data-ad-location="{{$dialog.Location}}" - data-ad-slot-key="{{if $dialog.AdvertiseLocationCode}}{{$dialog.AdvertiseLocationCode}}{{else if $dialog.Location}}{{$dialog.Location}}{{end}}" - data-ad-slot-name="{{if $dialog.AdvertiseLocationName}}{{$dialog.AdvertiseLocationName}}{{else if $dialog.Location}}弹窗广告位{{$dialog.Location}}{{else}}弹窗广告位{{end}}" - data-ad-type="interstitial" - {{if $dialog.Link}}data-ad-clickable="1"{{end}} role="dialog" aria-modal="true" aria-label="{{if $dialog.Title}}{{$dialog.Title}}{{else}}广告{{end}}" diff --git a/app/template/base/footer.html b/app/template/base/footer.html index 26cbc83..9baf846 100644 --- a/app/template/base/footer.html +++ b/app/template/base/footer.html @@ -1,5 +1,5 @@ {{define "footer"}} - +