logo和配置修改

This commit is contained in:
谢宇宁
2026-09-23 16:50:34 +07:00
parent bef9f8b4ed
commit 0db8087765
163 changed files with 393 additions and 20366 deletions
+1
View File
@@ -12,6 +12,7 @@ func Application(c *gin.Context) {
vm := applistser.BuildPageVM(raw)
data := gin.H{
"title": "福利应用",
"pageName": "应用推荐",
"navActive": "application",
"categories": vm.Categories,
}
-548
View File
@@ -1,548 +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"
)
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"`
}
type resetPasswordBody struct {
AccountNo string `json:"accountNo"`
Email string `json:"email"`
NewPassword string `json:"newPassword"`
}
type updateProfileBody struct {
Avatar string `json:"avatar"`
Introduction string `json:"introduction"`
NickName string `json:"nickName"`
OldPassword string `json:"oldPassword"`
NewPassword string `json:"newPassword"`
PrivacyMode *bool `json:"privacyMode"`
}
// Register 代理上游注册,成功后写入登录 cookie。
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 = strings.TrimSpace(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 + "@qmdj.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.Code != 200 || user == nil {
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": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": env.Msg,
"tip": firstNonEmpty(env.Tip, "注册成功"),
"data": user,
})
}
// 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 = strings.TrimSpace(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.Code != 200 || user == nil {
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": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": env.Msg,
"tip": firstNonEmpty(env.Tip, "登录成功"),
"data": user,
})
}
// ResetPassword 代理上游找回密码接口。
func ResetPassword(c *gin.Context) {
var body resetPasswordBody
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)
if body.AccountNo == "" || body.Email == "" || body.NewPassword == "" {
c.JSON(http.StatusOK, gin.H{"code": 1001, "msg": "参数错误", "tip": "请填写账号、邮箱和新密码", "data": nil})
return
}
env, err := authser.ResetPassword(c, authser.ResetPasswordReq{
AccountNo: body.AccountNo,
Email: body.Email,
NewPassword: body.NewPassword,
})
if err != nil {
log.Error("reset password upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{"code": 500, "msg": "reset password failed", "tip": "重置密码失败,请稍后重试", "data": nil})
return
}
tip := firstNonEmpty(env.Tip, env.Msg)
if env.Code == 200 {
tip = firstNonEmpty(tip, "密码重置成功")
} else {
tip = firstNonEmpty(tip, "密码重置失败")
}
c.JSON(http.StatusOK, gin.H{"code": env.Code, "msg": env.Msg, "tip": tip, "data": env.Data})
}
// Guest 代理上游游客登录,成功后写入登录 cookie。
// 对应 POST /api/app/auth/guest;需传 devID,上游要求 X-User-Agent(由 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 = strings.TrimSpace(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.Code != 200 || user == nil {
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": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "游客登录成功"),
"data": user,
})
}
// UpdateProfile 代理上游修改资料,成功后刷新 getInfo 并写 cookie。
func UpdateProfile(c *gin.Context) {
var body updateProfileBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
body.Avatar = strings.TrimSpace(body.Avatar)
body.Introduction = strings.TrimSpace(body.Introduction)
body.NickName = strings.TrimSpace(body.NickName)
body.OldPassword = strings.TrimSpace(body.OldPassword)
body.NewPassword = strings.TrimSpace(body.NewPassword)
req := authser.UpdateProfileReq{
Avatar: body.Avatar,
Introduction: body.Introduction,
NickName: body.NickName,
OldPassword: body.OldPassword,
NewPassword: body.NewPassword,
PrivacyMode: body.PrivacyMode,
}
if req.Avatar == "" && req.Introduction == "" && req.NickName == "" && req.OldPassword == "" && req.NewPassword == "" && req.PrivacyMode == nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "请至少修改一项资料",
"data": nil,
})
return
}
if (req.OldPassword == "") != (req.NewPassword == "") {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "修改密码需同时填写旧密码和新密码",
"data": nil,
})
return
}
env, err := authser.UpdateProfile(c, req)
if err != nil {
log.Error("update profile upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "update profile failed",
"tip": "保存失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = authser.UpdateProfile(c, req)
if err != nil {
log.Error("update profile retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "update profile failed",
"tip": "保存失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "保存失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
// 上游成功后拉最新资料写 cookie
_, user, infoErr := authser.GetInfo(c)
if infoErr != nil || user == nil {
log.Error("update profile getInfo failed", log.E(infoErr))
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": env.Msg,
"tip": firstNonEmpty(env.Tip, "保存成功"),
"data": nil,
})
return
}
authser.SetAuthCookies(c, user)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": env.Msg,
"tip": firstNonEmpty(env.Tip, "保存成功"),
"data": user,
})
}
// UploadImage 代理上游图片上传(头像不加 watermark)。
func UploadImage(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "请选择图片文件",
"data": nil,
})
return
}
f, err := fileHeader.Open()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "open file failed",
"tip": "读取图片失败",
"data": nil,
})
return
}
defer f.Close()
resp, err := authser.UploadImage(c, fileHeader.Filename, f)
if err != nil {
log.Error("upload image upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "upload failed",
"tip": "上传失败,请稍后重试",
"data": nil,
})
return
}
if resp == nil || resp.Code != 200 || resp.Data == nil || strings.TrimSpace(resp.Data.SourceNameFs) == "" {
tip := "上传失败"
msg := ""
code := 500
if resp != nil {
code = resp.Code
msg = resp.Msg
tip = firstNonEmpty(resp.Tip, resp.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(resp.Msg, "success"),
"tip": firstNonEmpty(resp.Tip, "上传成功"),
"data": gin.H{
"id": resp.Data.ID,
"sourceNameFs": resp.Data.SourceNameFs,
"url": resp.Data.SourceNameFs,
},
})
}
// GetUserInfo 代理上游 GET /api/app/auth/getInfo。
func GetUserInfo(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录",
"data": nil,
})
return
}
env, user, err := authser.GetInfo(c)
if err != nil {
log.Error("getInfo upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "getInfo failed",
"tip": "获取用户信息失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, user, err = authser.GetInfo(c)
if err != nil {
log.Error("getInfo retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "getInfo 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
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
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": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": user,
})
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
-57
View File
@@ -1,57 +0,0 @@
package web
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// LoginEntryURL 首页弹窗登录入口;redirectPath 为登录成功后回跳路径(须为站内相对路径)。
func LoginEntryURL(redirectPath string) string {
redirectPath = strings.TrimSpace(redirectPath)
if i := strings.IndexByte(redirectPath, '?'); i >= 0 {
redirectPath = redirectPath[:i]
}
if !isSafeRedirect(redirectPath) || redirectPath == "/" {
return "/login/"
}
return "/login/return" + ensureRedirectTrailingSlash(redirectPath)
}
// RegisterEntryURL 首页弹窗注册入口。
func RegisterEntryURL() string {
return "/register/"
}
func ensureRedirectTrailingSlash(path string) string {
if i := strings.IndexByte(path, '?'); i >= 0 {
path = path[:i]
}
if path != "/" && !strings.HasSuffix(path, "/") {
path += "/"
}
return path
}
// RedirectToLogin 未登录访问受保护页时,跳到首页并打开登录弹窗。
func RedirectToLogin(c *gin.Context) {
path := ""
if c != nil && c.Request != nil && c.Request.URL != nil {
path = c.Request.URL.RequestURI()
}
if path == "" || path == "/login" || strings.HasPrefix(path, "/login/") {
path = "/"
}
c.Redirect(http.StatusFound, LoginEntryURL(path))
}
func isSafeRedirect(path string) bool {
if path == "" || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") {
return false
}
if strings.ContainsAny(path, "\r\n") {
return false
}
return true
}
-5
View File
@@ -4,10 +4,8 @@ import (
"net/http"
"strconv"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
@@ -38,9 +36,6 @@ func ChapterDetail(c *gin.Context) {
})
return
}
if data != nil && useragent.TokenFromRequest(c.Request) != "" {
authser.SetRechargeCookie(c, data.Balance)
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "success",
-196
View File
@@ -1,196 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type chapterLikeBody struct {
ChapterID int64 `json:"chapterId"`
IsLike *bool `json:"isLike"`
}
// ChapterLikeSet 代理上游 POST /api/app/playlet/chapter/like/set。
func ChapterLikeSet(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再点赞",
"data": nil,
})
return
}
var body chapterLikeBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 chapterId 或 isLike",
"data": nil,
})
return
}
env, err := playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
ChapterID: body.ChapterID,
IsLike: *body.IsLike,
})
if err != nil {
log.Error("chapter like set upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter like failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.ChapterLikeSet(c, playletser.PlayletChapterLikeSetReq{
ChapterID: body.ChapterID,
IsLike: *body.IsLike,
})
if err != nil {
log.Error("chapter like set retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter like failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "操作失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": env.Data,
})
}
type chapterCollectBody struct {
ChapterID int64 `json:"chapterId"`
IsCollect *bool `json:"isCollect"`
}
// ChapterCollectSet 代理上游 POST /api/app/playlet/chapter/collect/set。
func ChapterCollectSet(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再收藏",
"data": nil,
})
return
}
var body chapterCollectBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 || body.IsCollect == nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 chapterId 或 isCollect",
"data": nil,
})
return
}
env, err := playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
ChapterID: body.ChapterID,
IsCollect: *body.IsCollect,
})
if err != nil {
log.Error("chapter collect set upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter collect failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.ChapterCollectSet(c, playletser.PlayletChapterCollectSetReq{
ChapterID: body.ChapterID,
IsCollect: *body.IsCollect,
})
if err != nil {
log.Error("chapter collect set retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter collect failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "操作失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": env.Data,
})
}
-106
View File
@@ -1,106 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type chapterUnlockBody struct {
ChapterID int64 `json:"chapterId"`
PlayletID int64 `json:"playletId"`
}
// ChapterUnlock 代理上游 POST /api/app/playlet/chapter/unlock。
func ChapterUnlock(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再解锁",
"data": nil,
})
return
}
var body chapterUnlockBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 || body.PlayletID <= 0 {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 chapterId 或 playletId",
"data": nil,
})
return
}
env, err := playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
ChapterID: body.ChapterID,
PlayletID: body.PlayletID,
})
if err != nil {
log.Error("chapter unlock upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter unlock failed",
"tip": "解锁失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.ChapterUnlock(c, playletser.PlayletChapterUnlockReq{
ChapterID: body.ChapterID,
PlayletID: body.PlayletID,
})
if err != nil {
log.Error("chapter unlock retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter unlock failed",
"tip": "解锁失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "解锁失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
authser.SetRechargeCookie(c, env.Data.Balance)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "解锁成功"),
"data": env.Data,
})
}
-103
View File
@@ -1,103 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type chapterWatchReportBody struct {
ChapterID int64 `json:"chapterId"`
PlayletID int64 `json:"playletId"`
}
// ChapterWatchReport 代理上游 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 chapterWatchReportBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ChapterID <= 0 {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 chapterId",
"data": nil,
})
return
}
env, err := playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
ChapterID: body.ChapterID,
PlayletID: body.PlayletID,
})
if err != nil {
log.Error("chapter watch report upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter watch report failed",
"tip": "上报失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.ChapterWatchReport(c, playletser.PlayletChapterWatchReportReq{
ChapterID: body.ChapterID,
PlayletID: body.PlayletID,
})
if err != nil {
log.Error("chapter watch report retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "chapter watch report failed",
"tip": "上报失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "上报失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": env.Data,
})
}
-84
View File
@@ -1,84 +0,0 @@
package web
import (
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
// Collect 我的收藏 SSR:
// 短剧 GET /api/app/playlet/collect/list;剧集 GET /api/app/playlet/chapter/collect/list。
func Collect(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
RedirectToLogin(c)
return
}
q := playletser.ParseCollectListQuery(routeOrQuery(c, "page", "page"), routeOrQuery(c, "type", "type"))
if redirectCanonical(c, playletser.CollectCanonicalPath(q)) {
return
}
data := gin.H{
"title": "我的收藏",
"description": "查看你在黄果短剧收藏的短剧与剧集,随时继续追更喜欢的内容。",
"navActive": "userCenter",
"collectType": q.Type,
"pageNum": q.PageNum,
"pageSize": q.PageSize,
"totalCount": int64(0),
"pagination": playletser.BuildCollectPagination(q, 0),
}
setNoIndex(data, false)
if q.Type == playletser.CollectTypeChapter {
raw, code, err := playletser.FetchChapterCollectList(c, q)
if err != nil {
aborted, retry := AbortSSRIfSessionExpired(c, code)
if aborted {
return
}
if retry {
raw, _, err = playletser.FetchChapterCollectList(c, q)
}
if err != nil {
log.Error("fetch playlet chapter collect list failed", log.E(err))
raw = &playletser.PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
}
if raw == nil {
raw = &playletser.PlayletChapterCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
data["chapterList"] = raw.List
data["pageNum"] = raw.PageNum
data["pageSize"] = raw.PageSize
data["totalCount"] = raw.TotalCount
data["pagination"] = playletser.BuildCollectPagination(q, raw.TotalCount)
} else {
raw, code, err := playletser.FetchCollectList(c, q)
if err != nil {
aborted, retry := AbortSSRIfSessionExpired(c, code)
if aborted {
return
}
if retry {
raw, _, err = playletser.FetchCollectList(c, q)
}
if err != nil {
log.Error("fetch playlet collect list failed", log.E(err))
raw = &playletser.PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
}
if raw == nil {
raw = &playletser.PlayletCollectListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
data["list"] = raw.List
data["pageNum"] = raw.PageNum
data["pageSize"] = raw.PageSize
data["totalCount"] = raw.TotalCount
data["pagination"] = playletser.BuildCollectPagination(q, raw.TotalCount)
}
HTMLOK(c, "collect.html", data)
}
-228
View File
@@ -3,11 +3,8 @@ package web
import (
"net/http"
"strconv"
"strings"
"unicode/utf8"
"videoweb/app/service/commentser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
@@ -55,228 +52,3 @@ func CommentList(c *gin.Context) {
"data": data,
})
}
type commentSendBody struct {
ObjectID int64 `json:"objectId"`
ObjectType int `json:"objectType"`
Text string `json:"text"`
ParentsID int64 `json:"parentsId"`
ReplyCommentID int64 `json:"replyCommentId"`
}
// CommentSend 代理上游 POST /api/app/comment/send。
// parentsId=0 一级评论;parentsId>0 二级评论;replyCommentId 为实际被回复评论。
func CommentSend(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再发表评论",
"data": nil,
})
return
}
var body commentSendBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
body.Text = strings.TrimSpace(body.Text)
if body.ObjectID <= 0 {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 objectId",
"data": nil,
})
return
}
if body.ObjectType != commentser.ObjectTypeChapter {
body.ObjectType = commentser.ObjectTypePlaylet
}
if body.Text == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "请输入评论内容",
"data": nil,
})
return
}
if utf8.RuneCountInString(body.Text) > 200 {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "评论最多 200 字",
"data": nil,
})
return
}
if body.ParentsID < 0 {
body.ParentsID = 0
}
if body.ReplyCommentID < 0 {
body.ReplyCommentID = 0
}
env, err := commentser.Send(c, commentser.CommentSendReq{
ObjectID: body.ObjectID,
ObjectType: body.ObjectType,
Text: body.Text,
ParentsID: body.ParentsID,
ReplyCommentID: body.ReplyCommentID,
})
if err != nil {
log.Error("comment send upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "comment send failed",
"tip": "发表失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = commentser.Send(c, commentser.CommentSendReq{
ObjectID: body.ObjectID,
ObjectType: body.ObjectType,
Text: body.Text,
ParentsID: body.ParentsID,
ReplyCommentID: body.ReplyCommentID,
})
if err != nil {
log.Error("comment send retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "comment send failed",
"tip": "发表失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "发表失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "发表成功"),
"data": env.Data,
})
}
type commentLikeBody struct {
CommentID int64 `json:"commentId"`
IsLike *bool `json:"isLike"`
}
// CommentLikeSet 代理上游 POST /api/app/comment/like/set。
func CommentLikeSet(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再点赞",
"data": nil,
})
return
}
var body commentLikeBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.CommentID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 commentId 或 isLike",
"data": nil,
})
return
}
env, err := commentser.LikeSet(c, commentser.CommentLikeSetReq{
CommentID: body.CommentID,
IsLike: *body.IsLike,
})
if err != nil {
log.Error("comment like set upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "comment like failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = commentser.LikeSet(c, commentser.CommentLikeSetReq{
CommentID: body.CommentID,
IsLike: *body.IsLike,
})
if err != nil {
log.Error("comment like set retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "comment like failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "操作失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": env.Data,
})
}
-244
View File
@@ -1,244 +0,0 @@
package web
import (
"net/http"
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/rechargeser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type buyPayBody struct {
PackageID int64 `json:"packageId"`
PayMode string `json:"payMode"`
}
// attachWalletFromGetInfo 登录态拉取 getInfo,刷新 cookie,并写入模板金币余额。
// 返回 true 表示注册账号会话失效,调用方应跳转登录。
func attachWalletFromGetInfo(c *gin.Context, data gin.H) bool {
if data == nil || useragent.TokenFromRequest(c.Request) == "" {
return false
}
env, user, err := authser.GetInfo(c)
if err != nil {
log.Error("wallet getInfo failed", log.E(err))
return false
}
if env != nil && authser.IsSessionExpiredCode(env.Code) {
retry, needLogin := authser.OnSessionExpired(c, env.Code)
if needLogin {
return true
}
if retry {
env, user, err = authser.GetInfo(c)
if err != nil {
log.Error("wallet getInfo retry failed", log.E(err))
return false
}
}
}
if env == nil || env.Code != 200 || user == nil {
return false
}
authser.SetAuthCookies(c, user)
data["userId"] = authser.FormatUserID(user.UserID)
data["recharge"] = user.Recharge
data["rechargeText"] = authser.FormatRecharge(user.Recharge)
data["vipType"] = user.VipType
data["showCardId"] = user.ShowCardID
data["showCardName"] = strings.TrimSpace(user.ShowCardName)
active := authser.IsActiveVipUser(user.VipType, user.CurrentVipExpire)
data["isVip"] = active
if active {
data["vipLabel"] = authser.VipDisplayLabel(user.ShowCardName, user.VipType)
data["vipExpireText"] = authser.FormatVipExpireText(user.CurrentVipExpire)
} else {
delete(data, "vipLabel")
delete(data, "vipExpireText")
}
return false
}
// Deposit 金币与会员充值页 SSR:套餐列表 + 金币流水明细。
// Query tab=coin 时默认选中金币充值套餐。
func Deposit(c *gin.Context) {
// 先补游客会话,保证可拉流水。
authser.EnsureGuestOnEnter(c)
preferTab := routeOrQuery(c, "tab", "tab")
if hasQuery(c, "tab") {
target := "/deposit"
if preferTab != "" {
target += "/" + preferTab
}
target += "/"
if redirectCanonical(c, target) {
return
}
}
raw := rechargeser.FetchPackageListOrEmpty(c)
vm := rechargeser.BuildDepositPageVM(raw, preferTab)
if useragent.TokenFromRequest(c.Request) != "" {
list, code, err := rechargeser.FetchCoinTransList(c, 1, 20)
if err != nil {
retry, needLogin := authser.OnSessionExpired(c, code)
if needLogin {
RedirectToLogin(c)
return
}
if retry {
list, _, err = rechargeser.FetchCoinTransList(c, 1, 20)
}
if err != nil {
log.Error("fetch coin trans list failed", log.E(err))
list = nil
}
}
vm.AttachTxList(list)
}
data := gin.H{
"title": "金币与会员",
"vipList": vm.VipList,
"coinList": vm.CoinList,
"payMethods": vm.PayMethods,
"txList": vm.TxList,
"preferTab": preferTab,
}
setNoIndex(data, false)
if attachWalletFromGetInfo(c, data) {
RedirectToLogin(c)
return
}
HTMLOK(c, "deposit.html", data)
}
// CoinBuy 代理上游 POST /api/app/coin/buy。
func CoinBuy(c *gin.Context) {
handleBuyPay(c, "coin")
}
// VipPay 代理上游 POST /api/app/vip/pay。
func VipPay(c *gin.Context) {
handleBuyPay(c, "vip")
}
func handleBuyPay(c *gin.Context, kind string) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再支付",
"data": nil,
})
return
}
var body buyPayBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
body.PayMode = rechargeser.NormalizePayMode(body.PayMode)
if body.PackageID <= 0 || body.PayMode == "" {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "请选择套餐和支付方式",
"data": nil,
})
return
}
req := rechargeser.BuyPayReq{
PackageID: body.PackageID,
PayMode: body.PayMode,
}
var (
env *rechargeser.APIEnvelope[rechargeser.PayOrderResp]
err error
)
switch kind {
case "coin":
env, err = rechargeser.BuyCoin(c, req)
default:
env, err = rechargeser.VipPay(c, req)
}
if err != nil {
log.Error("buy/pay upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "pay failed",
"tip": "发起支付失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
switch kind {
case "coin":
env, err = rechargeser.BuyCoin(c, req)
default:
env, err = rechargeser.VipPay(c, req)
}
if err != nil {
log.Error("buy/pay retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "pay failed",
"tip": "发起支付失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "发起支付失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
payURL := strings.TrimSpace(env.Data.PayURL)
if payURL == "" {
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "empty payUrl",
"tip": "未获取到支付链接,请稍后重试",
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "请完成支付"),
"data": gin.H{
"isOpenNewBrowser": env.Data.IsOpenNewBrowser,
"payUrl": payURL,
"payUrlMode": env.Data.PayURLMode,
"tradeNo": env.Data.TradeNo,
},
})
}
-2
View File
@@ -9,8 +9,6 @@ import (
var dialogTemplates = map[string]string{
"download": "downloadAppDialog",
"login": "loginDialog",
"register": "registerDialog",
"search": "searchDialog",
}
+3 -14
View File
@@ -4,9 +4,9 @@ import (
"net/http"
"strings"
"unicode/utf8"
"videoweb/app/service/authser"
"videoweb/app/service/feedbackser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
@@ -28,15 +28,7 @@ type contactUsBody struct {
// AddFeedback 代理上游 POST /api/app/feedback/add。
func AddFeedback(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再提交反馈",
"data": nil,
})
return
}
authser.EnsureGuestOnEnter(c)
var body feedbackBody
if err := c.ShouldBindJSON(&body); err != nil {
@@ -158,10 +150,7 @@ func AddFeedback(c *gin.Context) {
// AddContactUs 代理上游 POST /api/app/contactus/add。
func AddContactUs(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{"code": 401, "msg": "unauthorized", "tip": "请先登录后再提交", "data": nil})
return
}
authser.EnsureGuestOnEnter(c)
var body contactUsBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1001, "msg": "参数错误", "tip": "参数错误", "data": nil})
-117
View File
@@ -1,117 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
// History 观看历史 SSR:GET /api/app/playlet/history/list。
func History(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
RedirectToLogin(c)
return
}
q := playletser.ParseHistoryListQuery(routeOrQuery(c, "page", "page"))
if redirectCanonical(c, playletser.HistoryCanonicalPath(q)) {
return
}
raw, code, err := playletser.FetchHistoryList(c, q)
if err != nil {
aborted, retry := AbortSSRIfSessionExpired(c, code)
if aborted {
return
}
if retry {
raw, _, err = playletser.FetchHistoryList(c, q)
}
if err != nil {
log.Error("fetch playlet history list failed", log.E(err))
raw = &playletser.PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
}
if raw == nil {
raw = &playletser.PlayletHistoryListResp{PageNum: q.PageNum, PageSize: q.PageSize}
}
data := gin.H{
"title": "观看历史",
"description": "查看黄果短剧观看历史,快速回到上次看到的剧集继续观看。",
"navActive": "userCenter",
"list": raw.List,
"pageNum": raw.PageNum,
"pageSize": raw.PageSize,
"totalCount": raw.TotalCount,
"pagination": playletser.BuildHistoryPagination(q, raw.TotalCount),
"emptyTitle": "暂无观看历史",
"emptyDesc": "开始播放短剧后,记录会出现在这里",
}
setNoIndex(data, false)
HTMLOK(c, "history.html", data)
}
// HistoryClear 代理上游 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("playlet history clear upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "history clear failed",
"tip": "清空失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.HistoryClear(c)
if err != nil {
log.Error("playlet history clear retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "history clear failed",
"tip": "清空失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "清空失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "已清空观看历史"),
"data": env.Data,
})
}
-16
View File
@@ -8,17 +8,6 @@ import (
// Index 首页 SSR:拉取 /api/app/playlet/home,直接用接口字段渲染。
func Index(c *gin.Context) {
renderIndex(c, "")
}
func AuthIndex(mode string) gin.HandlerFunc {
return func(c *gin.Context) {
renderIndex(c, mode)
}
}
func renderIndex(c *gin.Context, authMode string) {
authRedirect := c.Param("redirect")
raw := playletser.FetchHomeOrEmpty(c)
data := gin.H{
"title": "黄果短剧",
@@ -29,11 +18,6 @@ func renderIndex(c *gin.Context, authMode string) {
"homeCategoryData": raw.HomeCategoryData,
"homeRecommendThemes": raw.HomeRecommendThemes,
"homeBannerData": playletser.ActiveHomeBanners(raw.HomeBannerData),
"authMode": authMode,
"authRedirect": authRedirect,
}
if authMode != "" {
setNoIndex(data, true)
}
HTMLOK(c, "index.html", data)
}
-6
View File
@@ -2,9 +2,7 @@ package web
import (
"strings"
"videoweb/app/service/authser"
"videoweb/app/service/playletser"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
@@ -56,10 +54,6 @@ func Play(c *gin.Context) {
chapters = playlet.PlayletChapterInfo
episodeRanges = playletser.BuildEpisodeRanges(chapters, playlet.TotalEpisode, 30)
if chapter != nil && useragent.TokenFromRequest(c.Request) != "" {
authser.SetRechargeCookie(c, chapter.Balance)
}
seoInfo := playletser.ResolvePlayPageSEO(chapter, playlet)
data := gin.H{
-196
View File
@@ -1,196 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
type playletLikeBody struct {
ID int64 `json:"id"`
IsLike *bool `json:"isLike"`
}
// PlayletLikeSet 代理上游 POST /api/app/playlet/like/set。
func PlayletLikeSet(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再点赞",
"data": nil,
})
return
}
var body playletLikeBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ID <= 0 || body.IsLike == nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 id 或 isLike",
"data": nil,
})
return
}
env, err := playletser.LikeSet(c, playletser.PlayletLikeSetReq{
ID: body.ID,
IsLike: *body.IsLike,
})
if err != nil {
log.Error("playlet like set upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "playlet like failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.LikeSet(c, playletser.PlayletLikeSetReq{
ID: body.ID,
IsLike: *body.IsLike,
})
if err != nil {
log.Error("playlet like set retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "playlet like failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "操作失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": env.Data,
})
}
type playletCollectBody struct {
ID int64 `json:"id"`
IsCollect *bool `json:"isCollect"`
}
// PlayletCollectSet 代理上游 POST /api/app/playlet/collect/set。
func PlayletCollectSet(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
c.JSON(http.StatusOK, gin.H{
"code": 401,
"msg": "unauthorized",
"tip": "请先登录后再收藏",
"data": nil,
})
return
}
var body playletCollectBody
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "参数错误",
"data": nil,
})
return
}
if body.ID <= 0 || body.IsCollect == nil {
c.JSON(http.StatusOK, gin.H{
"code": 1001,
"msg": "参数错误",
"tip": "缺少 id 或 isCollect",
"data": nil,
})
return
}
env, err := playletser.CollectSet(c, playletser.PlayletCollectSetReq{
ID: body.ID,
IsCollect: *body.IsCollect,
})
if err != nil {
log.Error("playlet collect set upstream failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "playlet collect failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
if env != nil && TryJSONSessionRecover(c, env.Code) {
env, err = playletser.CollectSet(c, playletser.PlayletCollectSetReq{
ID: body.ID,
IsCollect: *body.IsCollect,
})
if err != nil {
log.Error("playlet collect set retry failed", log.E(err))
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "playlet collect failed",
"tip": "操作失败,请稍后重试",
"data": nil,
})
return
}
}
if env == nil || env.Code != 200 {
tip := "操作失败"
msg := ""
code := 500
if env != nil {
code = env.Code
msg = env.Msg
tip = firstNonEmpty(env.Tip, env.Msg, tip)
}
c.JSON(http.StatusOK, gin.H{
"code": code,
"msg": msg,
"tip": tip,
"data": nil,
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": firstNonEmpty(env.Msg, "success"),
"tip": firstNonEmpty(env.Tip, "成功"),
"data": env.Data,
})
}
+1
View File
@@ -64,6 +64,7 @@ func Rank(c *gin.Context) {
}
data := gin.H{
"title": "排行榜",
"pageName": "排行榜",
"description": "黄果短剧排行榜实时更新热播短剧,查看热门榜、飙升榜,不错过高口碑必看剧集。",
"navActive": "rank",
"seoInfo": raw.SeoInfo,
+13 -77
View File
@@ -13,7 +13,7 @@ import (
"github.com/gin-gonic/gin"
)
// HTML 渲染页面并注入 footer / 导航 / 公共配置 / 登录态(以 cookie 为准)。
// HTML 渲染页面并注入 footer / 导航 / 公共配置,并确保存在游客会话。
func HTML(c *gin.Context, code int, name string, data gin.H) {
if data == nil {
data = gin.H{}
@@ -69,7 +69,6 @@ func HTML(c *gin.Context, code int, name string, data gin.H) {
if !ok {
cfg = configser.FetchAppConfigVMOrEmpty(c)
}
cfg = configser.ApplyTrackerFallback(cfg)
data["appConfig"] = cfg
siteURL := seoser.ResolveSiteURL(c, cfg)
data["foreverUrlAddr"] = siteURL
@@ -79,7 +78,6 @@ func HTML(c *gin.Context, code int, name string, data gin.H) {
canonicalURL = seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data))
data["canonicalURL"] = canonicalURL
data["seoVerify"] = seoser.VerifyFromConfig()
attachTrackerPage(c, code, data)
noindex := pageNoIndex(data)
if noindex {
data["noindex"] = true
@@ -88,84 +86,14 @@ func HTML(c *gin.Context, code int, name string, data gin.H) {
data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg)
}
// 无登录 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。
// 无 token 时静默建立游客会话(POST /api/app/auth/guest),上游内容接口依赖该会话。
authser.EnsureGuestOnEnter(c)
loggedIn, userName, avatar, email := authser.AuthFromCookies(c)
registered := authser.IsRegisteredFromCookies(c)
data["isLoggedIn"] = loggedIn
data["isRegistered"] = registered
if _, ok := data["isVip"]; !ok {
data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c)
}
isVip, _ := data["isVip"].(bool)
data["isVip"] = isVip
if loggedIn {
if userName != "" {
data["userName"] = userName
}
if 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)
}
}
// 允许页面 handler 先注入最新钱包数据(如 getInfo),避免被 cookie 覆盖。
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)
}
vipType := toInt(data["vipType"])
data["vipType"] = vipType
if _, ok := data["showCardName"]; !ok {
data["showCardName"] = authser.ShowCardNameFromCookies(c)
}
showCardName, _ := data["showCardName"].(string)
data["showCardName"] = strings.TrimSpace(showCardName)
if _, ok := data["showCardId"]; !ok {
data["showCardId"] = authser.ShowCardIDFromCookies(c)
}
if isVip {
if _, ok := data["vipLabel"]; !ok {
data["vipLabel"] = authser.VipDisplayLabel(showCardName, vipType)
}
if _, ok := data["vipExpireText"]; !ok {
data["vipExpireText"] = authser.VipExpireTextFromCookies(c)
}
} else {
delete(data, "vipLabel")
delete(data, "vipExpireText")
}
} else {
delete(data, "userName")
delete(data, "avatar")
delete(data, "email")
delete(data, "userId")
delete(data, "recharge")
delete(data, "rechargeText")
delete(data, "vipType")
delete(data, "showCardId")
delete(data, "showCardName")
delete(data, "vipLabel")
delete(data, "vipExpireText")
}
c.HTML(code, name, data)
}
// buildPageConsoleData 构建浏览器控制台使用的页面返回数据快照。
// 快照在公共配置和 cookie 登录态注入前生成,避免把会话相关公共字段写入日志。
// 快照不包含公共配置等大字段。
func buildPageConsoleData(path string, code int, templateName string, data gin.H) gin.H {
pageData := make(gin.H, len(data))
for key, value := range data {
@@ -191,8 +119,7 @@ func buildPageConsoleData(path string, code int, templateName string, data gin.H
func pageConsoleExcludedKey(key string) bool {
switch key {
case "pageConsoleData", "appConfig", "footer", "headerNav", "jsonLD", "seoVerify",
"isLoggedIn", "isRegistered", "userName", "avatar", "email", "userId":
case "pageConsoleData", "appConfig", "footer", "headerNav", "jsonLD", "seoVerify":
return true
default:
return false
@@ -312,3 +239,12 @@ func toInt64(v any) int64 {
func toInt(v any) int {
return int(toInt64(v))
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return v
}
}
return ""
}
+2 -4
View File
@@ -7,15 +7,13 @@ import (
"github.com/gin-gonic/gin"
)
func TestBuildPageConsoleDataFiltersCommonSessionFields(t *testing.T) {
func TestBuildPageConsoleDataFiltersCommonFields(t *testing.T) {
payload := buildPageConsoleData("/detail/example", http.StatusOK, "detail.html", gin.H{
"title": "示例短剧",
"detail": gin.H{"id": 12, "slug": "example"},
"appConfig": "public config",
"headerNav": "navigation",
"footer": "footer data",
"isLoggedIn": true,
"email": "viewer@example.com",
"pageConsoleData": "old payload",
})
@@ -32,7 +30,7 @@ func TestBuildPageConsoleDataFiltersCommonSessionFields(t *testing.T) {
if payload["headerNav"] != "navigation" || payload["footer"] != "footer data" {
t.Fatalf("common navigation/footer data missing: %#v", payload)
}
for _, key := range []string{"appConfig", "headerNav", "isLoggedIn", "email", "pageConsoleData"} {
for _, key := range []string{"appConfig", "headerNav", "pageConsoleData"} {
if _, ok := pageData[key]; ok {
t.Fatalf("excluded key %q remains in console data: %#v", key, pageData)
}
+11 -9
View File
@@ -20,7 +20,7 @@ func Richtext(c *gin.Context) {
if redirectCanonical(c, target) {
return
}
renderRichtext(c, title)
renderRichtext(c, title, "富文本页")
}
func RichtextPage(title string) gin.HandlerFunc {
@@ -34,31 +34,32 @@ func RichtextPage(title string) gin.HandlerFunc {
"帮助中心": "helpCenter.html",
}
if fallbackTemplate := fallbackTemplates[title]; fallbackTemplate != "" {
data := gin.H{"title": title}
data := gin.H{"title": title, "pageName": title}
applyRichtextIndexPolicy(data, title)
if title == "帮助中心" {
data["faqItems"] = []seoser.FAQItem{
{Question: "无法登录账号怎么办?", Answer: "请确认邮箱与密码是否正确;若忘记密码,可在登录页面使用“找回密码”。"},
{Question: "观看记录没有同步?", Answer: "请确认已登录同一账号并保持网络连接,记录通常会自动同步。"},
{Question: "付费剧集怎么观看?", Answer: "网页端可免费观看免费剧集;付费剧集请下载黄果短剧 App 观看完整内容。"},
{Question: "需要注册账号吗?", Answer: "不需要,网页端无需注册即可直接观看免费剧集。"},
{Question: "视频无法播放或加载缓慢?", Answer: "建议切换网络、刷新页面或更新浏览器;问题持续时可提交反馈。"},
{Question: "如何管理收藏内容?", Answer: "进入个人中心的“追剧与收藏”页面,可查看或取消收藏。"},
{Question: "在哪里下载 App?", Answer: "点击页面上的“下载APP”入口,按提示扫码或直接下载安装。"},
}
}
HTMLOK(c, fallbackTemplate, data)
return
}
}
renderRichtextData(c, title, page)
renderRichtextData(c, title, title, page)
}
}
func renderRichtext(c *gin.Context, title string) {
func renderRichtext(c *gin.Context, title, pageName string) {
title = strings.TrimSpace(title)
page := configser.FetchRichTextByTitle(c, title)
renderRichtextData(c, title, page)
renderRichtextData(c, title, pageName, page)
}
func renderRichtextData(c *gin.Context, title string, page configser.RichTextPageVM) {
// pageName 为面包屑名称,与上游富文本标题可能不同。
func renderRichtextData(c *gin.Context, title, pageName string, page configser.RichTextPageVM) {
if !page.Found {
NotFound(c, "未找到该内容")
return
@@ -73,6 +74,7 @@ func renderRichtextData(c *gin.Context, title string, page configser.RichTextPag
"title": displayTitle,
"richtext": page,
"richtextKey": title,
"pageName": pageName,
}
applyRichtextIndexPolicy(data, title)
HTMLOK(c, "richtext.html", data)
+2 -22
View File
@@ -6,27 +6,7 @@ import (
"github.com/gin-gonic/gin"
)
// AbortSSRIfSessionExpired 处理 SSR 场景下的会话失效码。
// aborted=true:已跳转登录,调用方应立刻 return;
// retry=true:游客已重新登录,调用方应重试上游请求。
func AbortSSRIfSessionExpired(c *gin.Context, code int) (aborted, retry bool) {
if !authser.IsSessionExpiredCode(code) {
return false, false
}
retry, needLogin := authser.OnSessionExpired(c, code)
if needLogin {
RedirectToLogin(c)
return true, false
}
return false, retry
}
// TryJSONSessionRecover JSON 接口遇到会话失效时:游客重登后返回 true(应重试);
// 注册账号清 cookie 后返回 false,调用方继续把 code=1000 交给前端弹登录。
// TryJSONSessionRecover 上游返回会话失效时重建游客会话;返回 true 表示调用方应重试一次。
func TryJSONSessionRecover(c *gin.Context, code int) bool {
if !authser.IsSessionExpiredCode(code) {
return false
}
retry, _ := authser.OnSessionExpired(c, code)
return retry
return authser.RecoverContentSession(c, code)
}
-119
View File
@@ -1,119 +0,0 @@
package web
import (
"net/http"
"videoweb/app/service/authser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
// Setting 账号设置页:SSR 拉取 /api/app/auth/getInfo。
func Setting(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
RedirectToLogin(c)
return
}
env, user, err := authser.GetInfo(c)
if err != nil {
log.Error("setting getInfo failed", log.E(err))
authser.ClearAuthCookies(c)
RedirectToLogin(c)
return
}
if env != nil && authser.IsSessionExpiredCode(env.Code) {
retry, needLogin := authser.OnSessionExpired(c, env.Code)
if needLogin || !retry {
RedirectToLogin(c)
return
}
env, user, err = authser.GetInfo(c)
if err != nil || env == nil || env.Code != 200 || user == nil {
authser.ClearAuthCookies(c)
RedirectToLogin(c)
return
}
} else if env == nil || env.Code != 200 || user == nil {
authser.ClearAuthCookies(c)
RedirectToLogin(c)
return
}
authser.SetAuthCookies(c, user)
vm := authser.BuildSettingVM(user)
data := gin.H{
"title": "账号设置",
"setting": vm,
}
setNoIndex(data, false)
HTMLOK(c, "setting.html", data)
}
// EditProfile 编辑资料页:SSR 拉取 getInfo,头像/昵称等回填。
func EditProfile(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
RedirectToLogin(c)
return
}
env, user, err := authser.GetInfo(c)
if err != nil {
log.Error("edit-profile getInfo failed", log.E(err))
authser.ClearAuthCookies(c)
RedirectToLogin(c)
return
}
if env != nil && authser.IsSessionExpiredCode(env.Code) {
retry, needLogin := authser.OnSessionExpired(c, env.Code)
if needLogin || !retry {
RedirectToLogin(c)
return
}
env, user, err = authser.GetInfo(c)
if err != nil || env == nil || env.Code != 200 || user == nil {
authser.ClearAuthCookies(c)
RedirectToLogin(c)
return
}
} else if env == nil || env.Code != 200 || user == nil {
authser.ClearAuthCookies(c)
RedirectToLogin(c)
return
}
authser.SetAuthCookies(c, user)
vm := authser.BuildSettingVM(user)
data := gin.H{
"title": "编辑个人资料",
"profile": vm,
}
setNoIndex(data, false)
HTMLOK(c, "editProfile.html", data)
}
// EditPassword 修改密码页:需登录。
func EditPassword(c *gin.Context) {
if useragent.TokenFromRequest(c.Request) == "" {
RedirectToLogin(c)
return
}
data := gin.H{
"title": "修改登录密码",
}
setNoIndex(data, false)
HTMLOK(c, "editPassword.html", data)
}
// 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,
})
}
+1
View File
@@ -19,6 +19,7 @@ func TodayUpdate(c *gin.Context) {
}
data := gin.H{
"title": "今日更新",
"pageName": "今日更新",
"description": "黄果短剧今日更新:第一时间追更最新上线短剧与连载剧集,不错过每日精彩更新。",
"navActive": "todayUpdate",
"seoInfo": raw.SeoInfo,
-127
View File
@@ -1,127 +0,0 @@
package web
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// attachTrackerPage 按响应状态和路径注入 pageKey / pageName(handler 可显式覆盖)。
// 所有 HTML 页面都必须得到明确页面信息,避免埋点落入 unknown / 未知页面。
func attachTrackerPage(c *gin.Context, code int, data gin.H) {
if data == nil {
return
}
key, name := trackerPageFromRequest(c.Request.URL.Path, code)
if code >= http.StatusBadRequest {
data["pageKey"] = key
data["pageName"] = name
return
}
existingKey, _ := data["pageKey"].(string)
existingName, _ := data["pageName"].(string)
existingKey = strings.TrimSpace(existingKey)
existingName = strings.TrimSpace(existingName)
if existingKey == "" || existingKey == "unknown" {
data["pageKey"] = key
}
if existingName == "" || existingName == "未知页面" {
data["pageName"] = name
}
}
func trackerPageFromRequest(path string, code int) (pageKey, pageName string) {
if code >= http.StatusInternalServerError {
return "server_error", "服务异常页面"
}
if code >= http.StatusBadRequest {
return "not_found", "404页面"
}
return trackerPageFromPath(path)
}
func trackerPageFromPath(path string) (pageKey, pageName string) {
p := strings.TrimSpace(path)
if p == "" || p == "/" {
return "home", "首页"
}
// 去掉尾部斜杠,便于精确匹配
p = strings.TrimSuffix(p, "/")
switch {
case trackerPathHasPrefix(p, "/list"):
return "list", "短剧列表"
case trackerPathHasPrefix(p, "/series"):
return "series", "AI成人剧集"
case trackerPathHasPrefix(p, "/tag"), trackerPathHasPrefix(p, "/tag-list"):
return "tag_list", "标签详情"
case trackerPathHasPrefix(p, "/rank"):
return "rank", "排行榜"
case trackerPathHasPrefix(p, "/today-update"):
return "today_update", "今日更新"
case trackerPathHasPrefix(p, "/category"):
return "category_detail", "分类详情"
case trackerTopicIndexPath(p):
return "theme_index", "专题列表"
case trackerPathHasPrefix(p, "/topic"), trackerPathHasPrefix(p, "/more"):
return "topic", "专题详情"
case trackerPathHasPrefix(p, "/download-app"):
return "download_app", "下载APP"
case trackerPathHasPrefix(p, "/application"):
return "application", "应用推荐"
case trackerPathHasPrefix(p, "/home-road"):
return "home_road", "回家的路"
case trackerPathHasPrefix(p, "/detail"):
return "detail", "短剧详情"
case trackerPathHasPrefix(p, "/play"):
return "play", "播放页"
case trackerPathHasPrefix(p, "/login"):
return "login", "登录"
case trackerPathHasPrefix(p, "/register"):
return "register", "注册"
case trackerPathHasPrefix(p, "/user-center"):
return "user_center", "个人中心"
case trackerPathHasPrefix(p, "/deposit"):
return "deposit", "充值中心"
case trackerPathHasPrefix(p, "/collect"):
return "collect", "我的收藏"
case trackerPathHasPrefix(p, "/history"):
return "history", "观看历史"
case trackerPathHasPrefix(p, "/setting"):
return "setting", "设置"
case trackerPathHasPrefix(p, "/edit-profile"):
return "edit_profile", "编辑资料"
case trackerPathHasPrefix(p, "/edit-password"):
return "edit_password", "修改密码"
case trackerPathHasPrefix(p, "/search"):
return "search", "搜索结果页"
case trackerPathHasPrefix(p, "/about-us"):
return "about_us", "关于我们"
case trackerPathHasPrefix(p, "/user-agreement"):
return "user_agreement", "用户协议"
case trackerPathHasPrefix(p, "/privacy-policy"):
return "privacy_policy", "隐私政策"
case trackerPathHasPrefix(p, "/help-center"):
return "help_center", "帮助中心"
case trackerPathHasPrefix(p, "/richtext"):
return "rich_text", "富文本页"
case trackerPathHasPrefix(p, "/feedback"):
return "feedback", "问题反馈"
case trackerPathHasPrefix(p, "/nav"), trackerPathHasPrefix(p, "/dh"):
return "nav", "官方导航"
default:
return "internal_other", "站内其他页面"
}
}
func trackerPathHasPrefix(path, base string) bool {
return path == base || strings.HasPrefix(path, base+"/")
}
func trackerTopicIndexPath(path string) bool {
if path == "/topic" || path == "/more" {
return true
}
return strings.HasPrefix(path, "/topic/page/") || strings.HasPrefix(path, "/more/page/")
}
-107
View File
@@ -1,107 +0,0 @@
package web
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestTrackerPageFromPath(t *testing.T) {
tests := []struct {
path string
key string
name string
}{
{"/", "home", "首页"},
{"/list/ai-short-play/2/page/3/", "list", "短剧列表"},
{"/series/hot/page/2/", "series", "AI成人剧集"},
{"/tag/family/page/2/", "tag_list", "标签详情"},
{"/tag-list/family/", "tag_list", "标签详情"},
{"/rank/2/page/3/", "rank", "排行榜"},
{"/today-update/page/2/", "today_update", "今日更新"},
{"/category/drama/page/2/", "category_detail", "分类详情"},
{"/topic/page/2/", "theme_index", "专题列表"},
{"/topic/sweet-love/page/2/", "topic", "专题详情"},
{"/more/legacy-topic/", "topic", "专题详情"},
{"/detail/my-playlet/", "detail", "短剧详情"},
{"/play/my-playlet/42/", "play", "播放页"},
{"/login/return/detail/foo/", "login", "登录"},
{"/register/", "register", "注册"},
{"/collect/chapter/page/2/", "collect", "我的收藏"},
{"/search/测试/page/2/", "search", "搜索结果页"},
{"/about-us/", "about_us", "关于我们"},
{"/user-agreement/", "user_agreement", "用户协议"},
{"/privacy-policy/", "privacy_policy", "隐私政策"},
{"/help-center/", "help_center", "帮助中心"},
{"/richtext/自定义内容/", "rich_text", "富文本页"},
{"/dh/", "nav", "官方导航"},
{"/unregistered-path/", "internal_other", "站内其他页面"},
}
for _, tt := range tests {
t.Run(tt.path, func(t *testing.T) {
key, name := trackerPageFromPath(tt.path)
if key != tt.key || name != tt.name {
t.Fatalf("trackerPageFromPath(%q) = (%q, %q), want (%q, %q)", tt.path, key, name, tt.key, tt.name)
}
})
}
}
func TestTrackerPageFromRequestUsesResponseStatus(t *testing.T) {
tests := []struct {
status int
key string
name string
}{
{http.StatusNotFound, "not_found", "404页面"},
{http.StatusInternalServerError, "server_error", "服务异常页面"},
}
for _, tt := range tests {
key, name := trackerPageFromRequest("/missing/", tt.status)
if key != tt.key || name != tt.name {
t.Fatalf("trackerPageFromRequest(status=%d) = (%q, %q), want (%q, %q)", tt.status, key, name, tt.key, tt.name)
}
}
}
func TestAttachTrackerPagePreservesExplicitPage(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodGet, "/topic/custom/", nil)
data := gin.H{"pageKey": "topic", "pageName": "自定义专题"}
attachTrackerPage(c, http.StatusOK, data)
if data["pageKey"] != "topic" || data["pageName"] != "自定义专题" {
t.Fatalf("explicit tracker page was overwritten: %#v", data)
}
}
func TestAttachTrackerPageReplacesUnknownValues(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodGet, "/play/demo/1/", nil)
data := gin.H{"pageKey": "unknown", "pageName": "未知页面"}
attachTrackerPage(c, http.StatusOK, data)
if data["pageKey"] != "play" || data["pageName"] != "播放页" {
t.Fatalf("unknown tracker page was not replaced: %#v", data)
}
}
func TestAttachTrackerPageMarksErrorResponse(t *testing.T) {
gin.SetMode(gin.TestMode)
c, _ := gin.CreateTestContext(httptest.NewRecorder())
c.Request = httptest.NewRequest(http.MethodGet, "/topic/deleted/", nil)
data := gin.H{"pageKey": "topic", "pageName": "已删除专题"}
attachTrackerPage(c, http.StatusNotFound, data)
if data["pageKey"] != "not_found" || data["pageName"] != "404页面" {
t.Fatalf("error response retained business page mapping: %#v", data)
}
}
-58
View File
@@ -1,58 +0,0 @@
package web
import (
"strconv"
"videoweb/app/service/playletser"
"videoweb/pub/common/log"
"videoweb/pub/common/useragent"
"github.com/gin-gonic/gin"
)
// UserCenter 个人中心:登录态刷新 getInfo 金币余额,并拉取收藏数量。
func UserCenter(c *gin.Context) {
data := gin.H{
"title": "个人中心",
"navActive": "userCenter",
}
setNoIndex(data, false)
if attachWalletFromGetInfo(c, data) {
RedirectToLogin(c)
return
}
if useragent.TokenFromRequest(c.Request) != "" {
raw, code, err := playletser.FetchCollectList(c, playletser.CollectListQuery{
PageNum: 1,
PageSize: 1,
})
if err != nil {
aborted, retry := AbortSSRIfSessionExpired(c, code)
if aborted {
return
}
if retry {
raw, _, err = playletser.FetchCollectList(c, playletser.CollectListQuery{
PageNum: 1,
PageSize: 1,
})
}
if err != nil {
log.Error("user-center collect count failed", log.E(err))
raw = nil
}
}
count := int64(0)
if raw != nil {
count = raw.TotalCount
}
if count < 0 {
count = 0
}
data["collectCount"] = count
data["collectCountText"] = strconv.FormatInt(count, 10) + "部"
} else {
data["collectCount"] = int64(0)
data["collectCountText"] = "0部"
}
HTMLOK(c, "userCenter.html", data)
}
+6 -13
View File
@@ -10,13 +10,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 相关配置。
@@ -25,9 +24,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"`
}
+1 -6
View File
@@ -1,11 +1,6 @@
serverAddr: "http://172.104.181.99:27413"
serverAddr: "https://webhgdjapi.mfcg.me"
listenAddr: "25119"
dhListenAddr: "26812"
runMod: "dev"
tsAuthKey: "fT5xSg4hltHpzVy6aV9rVECDJ1J1pN"
# 数据上报 SDK(web-sdk)。encryptedConfig 优先用上游 config/info.reportSdkConfig;
# 若上游暂未下发,可在此填写密文兜底。
tracker:
appId: "JHA-1043"
encryptedConfig: ""
-45
View File
@@ -9,7 +9,6 @@ import (
"strings"
"videoweb/app/api/web"
"videoweb/app/appg"
"videoweb/app/service/configser"
"videoweb/app/service/playletser"
"videoweb/app/service/seoser"
@@ -62,7 +61,6 @@ func setupEngine(engine *gin.Engine) {
tplFuncs := playletser.TemplateFuncs()
tplFuncs["InJSONScript"] = inJSONScript
tplFuncs["jsonStr"] = jsonStr
tplFuncs["InTrackerAppId"] = inTrackerAppId
engine.SetFuncMap(tplFuncs)
engine.LoadHTMLFiles(listHTMLTemplates("templates")...)
engine.Use(staticCacheHeaders(), crawlerHeaders())
@@ -145,42 +143,19 @@ func jsonStr(s string) template.JS {
return template.JS(b)
}
func inTrackerAppId() string {
return configser.ResolveTrackerAppID()
}
func registerRoutes(r *gin.Engine) {
r.GET("/", web.Index)
r.GET("/ui/dialog/:name", web.DialogFragment)
r.POST("/api/auth/register", web.Register)
r.POST("/api/auth/login", web.Login)
r.POST("/api/auth/resetPassword", web.ResetPassword)
r.POST("/api/auth/guest", web.Guest)
r.POST("/api/auth/logout", web.Logout)
r.POST("/api/auth/profile", web.UpdateProfile)
r.GET("/api/auth/getInfo", web.GetUserInfo)
r.POST("/api/upload/image", web.UploadImage)
r.POST("/api/feedback/add", web.AddFeedback)
r.POST("/api/contactus/add", web.AddContactUs)
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/like/set", web.PlayletLikeSet)
r.POST("/api/playlet/collect/set", web.PlayletCollectSet)
r.POST("/api/playlet/forward/heat", web.PlayletForwardHeat)
r.GET("/api/playlet/chapter/detail", web.ChapterDetail)
r.GET("/api/playlet/chapter/list", web.ChapterList)
r.GET("/api/playlet/operate/list", web.OperateList)
r.GET("/api/playlet/hot-like", web.HotLike)
r.GET("/api/app/playlet/hot-like", web.HotLike)
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/history/clear", web.HistoryClear)
r.POST("/api/playlet/chapter/unlock", web.ChapterUnlock)
r.POST("/api/coin/buy", web.CoinBuy)
r.POST("/api/vip/pay", web.VipPay)
r.GET("/list/", web.List)
r.GET("/list/:slug/", web.List)
@@ -239,26 +214,6 @@ func registerRoutes(r *gin.Engine) {
r.GET("/play/:slug/", web.Play)
r.GET("/play/:slug/:chapterId/", web.Play)
r.GET("/login/", web.AuthIndex("login"))
r.GET("/login/return/*redirect", web.AuthIndex("login"))
r.GET("/register/", web.AuthIndex("register"))
r.GET("/user-center/", web.UserCenter)
r.GET("/deposit/", web.Deposit)
r.GET("/deposit/:tab/", web.Deposit)
r.GET("/collect/", web.Collect)
r.GET("/collect/page/:page/", web.Collect)
r.GET("/collect/:type/", web.Collect)
r.GET("/collect/:type/page/:page/", web.Collect)
r.GET("/history/", web.History)
r.GET("/history/page/:page/", web.History)
r.GET("/setting/", web.Setting)
r.GET("/edit-profile/", web.EditProfile)
r.GET("/edit-password/", web.EditPassword)
r.GET("/search/", web.Search)
r.GET("/search/:keyword/", web.Search)
r.GET("/search/:keyword/page/:page/", web.Search)
-1
View File
@@ -46,7 +46,6 @@ func TestContentPagesOnlyRegisterPathParameterRoutes(t *testing.T) {
}
}
for _, route := range []string{
"POST /api/auth/resetPassword",
"POST /api/contactus/add",
"GET /api/playlet/chapter/list",
"GET /api/playlet/operate/list",
+2 -4
View File
@@ -48,9 +48,8 @@ func TestMainSiteKeepsMainVerificationAndAnalytics(t *testing.T) {
func renderSEOCommonForTrackingTest(t *testing.T, isNavPage bool) string {
t.Helper()
tpl, err := template.New("tracking-test").Funcs(template.FuncMap{
"InTrackerAppId": func() string { return "test-app" },
"jsonStr": func(any) template.JS { return template.JS(`""`) },
}).ParseFiles("../templates/base/seoCommon.html", "../templates/base/trackerInit.html")
}).ParseFiles("../templates/base/seoCommon.html", "../templates/base/analytics.html")
if err != nil {
t.Fatal(err)
}
@@ -58,8 +57,7 @@ func renderSEOCommonForTrackingTest(t *testing.T, isNavPage bool) string {
"isNavPage": isNavPage,
"seoVerify": map[string]any{"Baidu": ""},
"appConfig": map[string]any{
"ReportSDKConfig": "",
"SeoInfo": map[string]any{"WebSiteName": ""},
"SeoInfo": map[string]any{"WebSiteName": ""},
},
}
var out bytes.Buffer
+5 -153
View File
@@ -3,10 +3,8 @@ package authser
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"videoweb/app/service/upstream"
@@ -16,58 +14,11 @@ import (
)
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 创建或找回游客账号)。
// GuestLogin 调用上游游客登录,站点不提供注册/登录,上游内容接口依赖游客会话。
func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, error) {
var env APIEnvelope
if _, err := upstream.PostJSON(c, guestPath, req, &env); err != nil {
@@ -83,47 +34,6 @@ func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, er
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")
@@ -135,7 +45,7 @@ func decodeLoginData(raw json.RawMessage) (*LoginResp, error) {
return &user, nil
}
// SetAuthCookies 将登录/注册返回写入 cookie。
// SetAuthCookies 写入游客会话 cookie。
func SetAuthCookies(c *gin.Context, user *LoginResp) {
if c == nil || user == nil {
return
@@ -144,33 +54,13 @@ func SetAuthCookies(c *gin.Context, user *LoginResp) {
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, "introduction", user.Introduction, secure)
setCookie(c, "avatar", user.Avatar, secure)
setCookie(c, "email", user.Email, secure)
setCookie(c, "inviteCode", user.InviteCode, 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, "showCardId", strconv.FormatInt(user.ShowCardID, 10), secure)
setCookie(c, "showCardName", user.ShowCardName, secure)
setCookie(c, "userType", strconv.Itoa(user.Type), secure)
isReg := "0"
if user.IsRegister || user.Type == 1 {
isReg = "1"
}
setCookie(c, "isRegister", isReg, secure)
if sid := useragent.SIDFromRequest(c.Request); sid != "" {
setCookie(c, "sid", sid, secure)
}
}
func setCookie(c *gin.Context, name, value string, secure bool) {
// Cookie Value 仅允许 ASCII;中文昵称等需 URL 编码。
// 使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致;
// 避免 QueryEscape 的空格→+,否则 JS decodeURIComponent 会留下字面量 "+"。
// Cookie Value 仅允许 ASCII;使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致。
http.SetCookie(c.Writer, &http.Cookie{
Name: name,
Value: url.PathEscape(value),
@@ -199,7 +89,7 @@ func cookieValue(c *gin.Context, name string) string {
return raw
}
// ClearAuthCookies 清除登录相关 cookie。
// ClearAuthCookies 清除会话 cookie,同时清理旧版账号体系遗留的用户信息 cookie。
func ClearAuthCookies(c *gin.Context) {
for _, name := range authCookieNames() {
http.SetCookie(c.Writer, &http.Cookie{
@@ -218,44 +108,6 @@ func authCookieNames() []string {
"token", "_token_", "sid", "_s_token_",
"userId", "accountNo", "nickName", "introduction", "avatar",
"email", "inviteCode", "recharge", "vipType", "currentVipExpire", "showCardId", "showCardName",
"userType", "isRegister",
"userType", "isRegister", "guest_skip_auto",
}
}
// 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 ""
}
+4 -89
View File
@@ -4,7 +4,6 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
@@ -16,24 +15,17 @@ import (
)
const guestDevIDCookie = "guest_dev_id"
const guestSkipAutoCookie = "guest_skip_auto"
// EnsureGuestOnEnter 页面进入时:无 token(未自己登录)则走游客登录并写 cookie。
// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 读到登录态。
// 手动退出后带 guest_skip_auto 时跳过本次;清掉标记后,下次进页再自动游客。
// EnsureGuestOnEnter 页面进入时:无 token 则走游客登录并写 cookie。
// 对应上游 POST /api/app/auth/guest。成功后会把会话补进当前 Request,便于同一次 SSR 直接使用。
func EnsureGuestOnEnter(c *gin.Context) *LoginResp {
if c == nil || c.Request == nil {
return nil
}
// 已有任意会话(游客或正式账号)则跳过
// 已有会话则跳过
if useragent.TokenFromRequest(c.Request) != "" {
return nil
}
// 手动退出:本次进页不自动游客,并清除标记,供下次进页恢复
if IsGuestSkipAuto(c) {
ClearGuestSkipAuto(c)
return nil
}
devID := ensureGuestDevID(c)
env, user, err := GuestLogin(c, GuestLoginReq{
@@ -55,25 +47,10 @@ func EnsureGuestOnEnter(c *gin.Context) *LoginResp {
}
SetAuthCookies(c, user)
ClearGuestSkipAuto(c)
patchAuthCookiesToRequest(c, user)
patchRequestCookie(c, "token", user.Token)
return user
}
// RefreshGuestSession 游客会话失效时(如上游 6001 用户不存在)重新游客登录。
// 正式注册账号不处理,避免把用户踢成游客。
func RefreshGuestSession(c *gin.Context) *LoginResp {
if c == nil || c.Request == nil {
return nil
}
if IsRegisteredFromCookies(c) {
return nil
}
dropRequestCookie(c, "token")
dropRequestCookie(c, "_token_")
return EnsureGuestOnEnter(c)
}
func dropRequestCookie(c *gin.Context, name string) {
if c == nil || c.Request == nil || name == "" {
return
@@ -88,43 +65,6 @@ func dropRequestCookie(c *gin.Context, name string) {
c.Request.Header.Set("Cookie", strings.Join(parts, "; "))
}
// MarkGuestSkipAuto 手动退出后标记:跳过紧接着的一次自动游客登录。
func MarkGuestSkipAuto(c *gin.Context) {
if c == nil {
return
}
secure := c.Request != nil && c.Request.TLS != nil
http.SetCookie(c.Writer, &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
}
http.SetCookie(c.Writer, &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
@@ -164,31 +104,6 @@ func guestChannel(c *gin.Context) string {
return ""
}
func patchAuthCookiesToRequest(c *gin.Context, user *LoginResp) {
if c == nil || user == nil {
return
}
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
+2 -28
View File
@@ -2,7 +2,7 @@ package authser
import "github.com/gin-gonic/gin"
// TokenExpiredCode 上游约定:token 失效,需重新登录。
// TokenExpiredCode 上游约定:token 失效。
const TokenExpiredCode = 1000
// IsSessionExpiredCode 是否为会话失效类业务码。
@@ -15,31 +15,7 @@ func IsSessionExpiredCode(code int) bool {
}
}
// 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_")
return false, true
}
if RefreshGuestSession(c) != nil {
return true, false
}
ClearAuthCookies(c)
dropRequestCookie(c, "token")
dropRequestCookie(c, "_token_")
return false, false
}
// RecoverContentSession 用于允许游客访问的内容读取接口。
// 正式账号或游客 token 失效后,清理旧登录态、重新建立游客会话,调用方随后重试一次原请求。
// 写操作仍应使用 OnSessionExpired,避免把用户操作静默降级为游客操作。
// RecoverContentSession 会话失效后清理旧 cookie 并重新建立游客会话,调用方随后重试一次原请求。
func RecoverContentSession(c *gin.Context, code int) bool {
if c == nil || !IsSessionExpiredCode(code) {
return false
@@ -48,7 +24,5 @@ func RecoverContentSession(c *gin.Context, code int) bool {
for _, name := range authCookieNames() {
dropRequestCookie(c, name)
}
dropRequestCookie(c, guestSkipAutoCookie)
ClearGuestSkipAuto(c)
return EnsureGuestOnEnter(c) != nil
}
+3 -71
View File
@@ -2,31 +2,6 @@ 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 {
@@ -36,39 +11,10 @@ type GuestLoginReq struct {
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。
// LoginResp 游客登录成功 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"` // 是否已注册
LoginType int `json:"loginType"` // 登录方式
PrivacyMode bool `json:"privacyMode"` // 隐私模式
Score int64 `json:"score"` // 积分
Recharge int64 `json:"recharge"` // 金币余额
Token string `json:"token"`
UserID int64 `json:"userId"`
}
// APIEnvelope 上游统一响应壳。Data 在失败时可能是 string/null。
@@ -78,17 +24,3 @@ type APIEnvelope struct {
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"`
}
-87
View File
@@ -1,87 +0,0 @@
package authser
import (
"fmt"
"strings"
)
// SettingPageVM 账号设置页展示模型。
type SettingPageVM struct {
UserID int64
AccountNo string
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)
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,
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)
}
-187
View File
@@ -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()
}
-22
View File
@@ -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
}
-45
View File
@@ -1,45 +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")
}
if req.ObjectType != ObjectTypeChapter {
req.ObjectType = ObjectTypePlaylet
}
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
}
-29
View File
@@ -1,29 +0,0 @@
package configser
import (
"strings"
"videoweb/app/appg"
"videoweb/pub/pubdef"
)
// ResolveTrackerAppID 优先 yaml,否则固定 JHA-1043。
func ResolveTrackerAppID() string {
if id := strings.TrimSpace(appg.Conf.Tracker.AppID); id != "" {
return id
}
return pubdef.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
}
+6 -8
View File
@@ -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 子集,按页面需要取用)。
-2
View File
@@ -24,7 +24,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"`
@@ -54,7 +53,6 @@ func BuildAppConfigVM(info *ConfigInfoResp) AppConfigVM {
OfficialAppAddr: sanitizeURL(addr.OfficialAppAddr),
PDFAddr: sanitizeURL(addr.PDFAddr),
TwitterAddr: sanitizeURL(addr.TwitterAddr),
ReportSDKConfig: strings.TrimSpace(info.ReportSDKConfig),
NavPageList: buildNavPageList(info.NavPageList),
Advitise: info.Advitise,
DetailTopList: buildAdvertisementList(info.Advitise.DetailTopList),
-44
View File
@@ -139,20 +139,6 @@ func (c *PlayletChapterInfoResp) AllowWatch() bool {
return strings.TrimSpace(c.VideoURL) != "" && c.PayType == 0
}
// UnlockCost 金币解锁消耗:优先 costCoins。
func (c *PlayletChapterInfoResp) UnlockCost() int64 {
if c == nil {
return 0
}
if c.CostCoins > 0 {
return c.CostCoins
}
if c.SingleCoins > 0 {
return int64(c.SingleCoins)
}
return 0
}
// AllowWatch 详情选集是否可直达播放(优先 canWatch)。
func (c PlayletChapterData) AllowWatch() bool {
if c.CanWatch {
@@ -166,33 +152,3 @@ func (c PlayletChapterData) AllowWatch() bool {
}
return c.PayType == 0
}
// NeedCoinGate 是否弹出金币解锁。
func (c PlayletChapterData) NeedCoinGate() bool {
if c.AllowWatch() {
return false
}
if c.NeedUnlock {
return true
}
return c.PayType == 1
}
// NeedVipGate 是否弹出 VIP 开通。
func (c PlayletChapterData) NeedVipGate() bool {
if c.AllowWatch() || c.NeedCoinGate() {
return false
}
if c.NeedVip {
return true
}
return c.PayType == 2
}
// UnlockCost 金币解锁消耗:优先 costCoins。
func (c PlayletChapterData) UnlockCost() int {
if c.CostCoins > 0 {
return int(c.CostCoins)
}
return c.SingleCoins
}
-84
View File
@@ -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}
}
-63
View File
@@ -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
}
-40
View File
@@ -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
}
-39
View File
@@ -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
}
-155
View File
@@ -1,155 +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"`
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
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
}
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))
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}
}
-89
View File
@@ -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)
}
-139
View File
@@ -1,139 +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"
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"`
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"`
}
// 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,无请求体。
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
}
-147
View File
@@ -1,147 +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)
}
// formatHistoryAt 最近观看时间展示。
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 t.Format("2006-01-02 15:04")
}
}
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
}
-35
View File
@@ -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
}
+44 -75
View File
@@ -11,62 +11,50 @@ import (
// TemplateFuncs 模板辅助:仅做展示格式化 / URL,不把接口字段映射成另一套名字。
func TemplateFuncs() template.FuncMap {
return template.FuncMap{
"detailURL": detailURL,
"playURL": playURL,
"chapterPlayURL": chapterPlayURL,
"categoryURL": categoryURL,
"categoryDetailURL": categoryDetailURL,
"themeListURL": themeListURLByKey,
"coverURL": coverURL,
"formatHeat": formatHeat,
"formatCollect": formatCollectHeat,
"formatLikeCount": formatLikeCount,
"episodeLabel": episodeText,
"displayTag": displayTag,
"firstTagName": firstTagName,
"themeTagText": themeTagText,
"tagListURL": tagListURL,
"cardTag": cardTag,
"cardTags": cardTags,
"namedTags": namedTags,
"namedThemeTags": namedThemeTags,
"firstName": firstName,
"rankClass": rankClass,
"add": addInt,
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"footerLinkURL": footerLinkURL,
"footerLinkIsExternal": footerLinkIsExternal,
"footerContactURL": footerContactURL,
"footerContactName": footerContactName,
"footerContactValue": footerContactValue,
"navDisplayURL": navDisplayURL,
"urlquery": url.QueryEscape,
"pathescape": url.PathEscape,
"metaLine": metaLine,
"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,
"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,
"coverURL": coverURL,
"formatHeat": formatHeat,
"formatCollect": formatCollectHeat,
"formatLikeCount": formatLikeCount,
"episodeLabel": episodeText,
"displayTag": displayTag,
"firstTagName": firstTagName,
"themeTagText": themeTagText,
"tagListURL": tagListURL,
"cardTag": cardTag,
"cardTags": cardTags,
"namedTags": namedTags,
"namedThemeTags": namedThemeTags,
"firstName": firstName,
"rankClass": rankClass,
"add": addInt,
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"footerLinkURL": footerLinkURL,
"footerLinkIsExternal": footerLinkIsExternal,
"footerContactURL": footerContactURL,
"footerContactName": footerContactName,
"footerContactValue": footerContactValue,
"navDisplayURL": navDisplayURL,
"urlquery": url.QueryEscape,
"pathescape": url.PathEscape,
"metaLine": metaLine,
"updateStatusText": updateStatusText,
"browseText": browseText,
"usableAnchor": usableAnchor,
"playletYearText": playletYearText,
"playletDateText": playletDateText,
"isPlayletAllFree": isPlayletAllFree,
"absInt64": absInt64Tpl,
"absURL": absURLTpl,
"canonicalOr": canonicalOr,
"joinStrings": joinStrings,
"dict": dict,
}
}
@@ -315,25 +303,6 @@ func absInt64Tpl(n int64) int64 {
return n
}
func formatShortDateTime(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) >= 16 {
raw = raw[:16]
}
raw = strings.ReplaceAll(raw, "-", "/")
return raw
}
func metaLine(item CommonData) string {
cat := firstName(item.ThemeNames)
if cat == "" {
-212
View File
@@ -1,212 +0,0 @@
package rechargeser
import (
"encoding/json"
"fmt"
"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"
)
// 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) {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
full := fmt.Sprintf("%s?page=%d&pageSize=%d", coinTransPath, page, pageSize)
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 decodeCoinTransList(env.Data), env.Code, nil
}
// 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 {
if len(raw) == 0 || string(raw) == "null" {
return nil
}
var arr []CoinTransListItem
if err := json.Unmarshal(raw, &arr); err == nil {
return arr
}
var obj struct {
List []CoinTransListItem `json:"list"`
}
if err := json.Unmarshal(raw, &obj); err != nil {
return nil
}
return obj.List
}
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
}
}
-117
View File
@@ -1,117 +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"`
}
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"`
}
-439
View File
@@ -1,439 +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
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
TimeText string
AmountText string
AmountClass string // deposit-tx-amount--plus / --minus
StatusText string
}
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,
TimeText: formatTransTime(item.CreatedAt),
AmountText: amountText,
AmountClass: amountClass,
StatusText: strings.TrimSpace(item.FlowDirName),
})
}
return out
}
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),
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 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()
}
-2
View File
@@ -280,8 +280,6 @@ func navActiveKey(href, name string, download bool) string {
return "aiSeries"
case "/home-road":
return "homeRoad"
case "/user-center":
return "userCenter"
case "/download-app":
return "downloadApp"
default:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-137
View File
@@ -1,137 +0,0 @@
.coin-dialog-mask {
position: fixed;
inset: 0;
z-index: 1100;
display: none;
align-items: center;
justify-content: center;
padding: 24px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.7);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.coin-dialog-mask.is-open { display: flex; }
.coin-dialog-stack {
width: 440px;
max-width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.coin-dialog-card {
width: 440px;
max-width: 100%;
padding: 36px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
color: #fff;
background: #0d0d0d;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 40px;
font-family: "PingFang SC", "Noto Sans SC", sans-serif;
}
.coin-dialog-icon { width: 60px; height: 60px; flex: 0 0 auto; object-fit: contain; }
.coin-dialog-title { color: #fff; font-size: 24px; font-weight: 600; line-height: 32px; text-align: center; }
.coin-dialog-desc { color: rgba(255,255,255,.55); font-size: 16px; font-weight: 400; line-height: 22px; text-align: center; }
.coin-dialog-balance {
width: 100%;
padding: 18px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.1);
border-radius: 12px;
}
.coin-dialog-balance-col { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; }
.coin-dialog-balance-col--end { align-items: flex-end; gap: 2px; }
.coin-dialog-balance-label { color: rgba(255,255,255,.55); font-size: 12px; font-weight: 400; line-height: 18px; }
.coin-dialog-balance-value,
.coin-dialog-balance-cost { color: #fff; font-size: 16px; font-weight: 600; line-height: 22px; }
.coin-dialog-balance-cost { color: #ec237f; }
.coin-dialog-hint { color: rgba(255,255,255,.55); font-size: 14px; font-weight: 400; line-height: 20px; text-align: center; }
.coin-dialog-actions { width: 100%; display: flex; gap: 18px; }
.coin-dialog-download,
.coin-dialog-cta {
min-width: 0;
min-height: 58px;
padding: 18px 24px;
box-sizing: border-box;
display: flex;
flex: 1 1 0;
align-items: center;
justify-content: center;
color: #fff;
font-family: inherit;
font-size: 16px;
font-weight: 600;
line-height: 22px;
text-decoration: none;
white-space: nowrap;
border: 0;
border-radius: 18px;
cursor: pointer;
}
.coin-dialog-download { background: linear-gradient(135deg, #ff5f6e 0%, #fe2a7f 50%, #8c206e 100%); }
.coin-dialog-cta {
padding-right: 30px;
padding-left: 30px;
color: #8b1a15;
background: linear-gradient(180deg, #fbc297 0%, #fae1c0 100%);
}
.coin-dialog-cta-label-h5 { display: none; }
.coin-dialog-cta-loading { display: none; }
.coin-dialog-cta.is-loading,
.coin-dialog-cta:disabled { opacity: .75; cursor: not-allowed; }
.coin-dialog-cta.is-loading .coin-dialog-cta-label { display: none; }
.coin-dialog-cta.is-loading .coin-dialog-cta-loading { display: inline; }
.coin-dialog-recharge {
width: 100%;
color: #ec237f;
font-size: 16px;
font-weight: 600;
line-height: 22px;
text-align: center;
text-decoration: none;
}
.coin-dialog-close {
width: 36px;
height: 36px;
padding: 0;
flex: 0 0 36px;
border: 0;
background: transparent;
cursor: pointer;
}
.coin-dialog-close img { display: block; width: 36px; height: 36px; }
@media (max-width: 767px) {
.coin-dialog-mask {
padding: 16px;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
}
.coin-dialog-stack { width: 300px; }
.coin-dialog-card { width: 300px; padding: 24px 20px; border-radius: 20px; }
.coin-dialog-title { font-size: 18px; line-height: 24px; }
.coin-dialog-desc { font-size: 14px; line-height: 20px; }
.coin-dialog-hint { font-size: 12px; line-height: 18px; }
.coin-dialog-actions { flex-direction: column; gap: 18px; }
.coin-dialog-download,
.coin-dialog-cta {
width: 100%;
min-height: 46px;
padding: 12px 24px;
flex: none;
border-radius: 9px;
}
.coin-dialog-cta { padding-right: 48px; padding-left: 48px; }
.coin-dialog-cta-label-pc { display: none; }
.coin-dialog-cta-label-h5 { display: inline; }
.coin-dialog-recharge { font-size: 14px; font-weight: 400; line-height: 20px; }
}
-489
View File
@@ -1,489 +0,0 @@
.collect-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.collect-layout .site-header,
.collect-layout .footer-box {
flex-shrink: 0;
}
.collect-page {
width: 100%;
flex: 1 1 auto;
min-height: 0;
padding: 36px 0;
background: #0A0A0A;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
box-sizing: border-box;
}
.collect-pc {
width: min(1800px, calc(100% - 120px));
max-width: none;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.collect-nav {
display: none;
}
.collect-main {
width: min(1800px, calc(100% - 120px));
max-width: none;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.collect-breadcrumb {
align-self: stretch;
overflow: hidden;
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 10px;
}
.collect-breadcrumb-link {
color: #EC237F;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
text-decoration: none;
}
.collect-breadcrumb-sep {
color: #666666;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.collect-breadcrumb-current {
color: #8A8A8A;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.collect-head {
align-self: stretch;
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 12px;
}
.collect-title {
margin: 0;
color: white;
font-size: 30px;
font-family: PingFang SC;
font-weight: 400;
line-height: 36px;
flex-shrink: 0;
}
.collect-desc {
margin: 0;
color: #A3A3A3;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.collect-tabs {
overflow: hidden;
justify-content: flex-start;
align-items: center;
gap: 9px;
display: inline-flex;
}
.collect-tab {
padding: 7px 16px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
display: inline-flex;
justify-content: center;
align-items: center;
color: #A3A3A3;
font-size: 12px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
text-decoration: none;
box-sizing: border-box;
}
.collect-tab--active {
background: #EC237F;
border: none;
color: #181818;
font-weight: 700;
}
.collect-grid {
align-self: stretch;
width: 100%;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
justify-content: stretch;
align-items: stretch;
gap: 14px 16px;
}
.collect-card {
width: 100%;
min-width: 0;
background: transparent;
overflow: hidden;
border-radius: 14px;
display: flex;
flex-direction: column;
align-items: stretch;
text-decoration: none;
color: inherit;
box-sizing: border-box;
}
.collect-card-cover {
width: 100%;
height: auto;
aspect-ratio: 3 / 4;
position: relative;
overflow: hidden;
background: rgba(255, 255, 255, 0.05);
}
.collect-card-cover-img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.collect-card-eps {
position: absolute;
right: 8px;
bottom: 8px;
height: 28px;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.60);
border-radius: 8px;
display: inline-flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 12px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
box-sizing: border-box;
z-index: 2;
}
.collect-card-body {
width: 100%;
flex: 1 1 auto;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.05);
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
box-sizing: border-box;
}
.collect-card-title {
width: 100%;
min-height: 20px;
color: #F5F5F5;
font-size: 14px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-break: break-all;
}
.collect-card-desc {
width: 100%;
min-height: 18px;
color: #A3A3A3;
font-size: 12px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.collect-card-meta {
width: 100%;
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
overflow: hidden;
}
.collect-card-tag {
min-width: 0;
color: #8A8A8A;
font-size: 12px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.collect-card-spacer {
flex: 1 1 0;
min-width: 4px;
height: 9px;
}
.collect-card-progress {
flex-shrink: 0;
color: #808080;
font-size: 12px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
white-space: nowrap;
}
.collect-empty {
width: 100%;
grid-column: 1 / -1;
padding: 48px 16px;
text-align: center;
color: #A3A3A3;
}
.collect-pagination {
align-self: stretch;
width: 100%;
margin-top: 0;
}
.collect-pagination .pagination {
width: 100%;
}
@media (max-width: 767px) {
body.is-collect-page {
background: #0A0A0A;
}
body.is-collect-page .site-header,
body.is-collect-page .footer-box,
body.is-collect-page .floating-action,
body.is-collect-page .download-fab {
display: none !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
body.is-collect-page .collect-layout {
min-height: 100vh;
background: #0A0A0A;
}
body.is-collect-page .collect-page {
padding: 0;
background: #0A0A0A;
align-items: stretch;
justify-content: flex-start;
gap: 0;
min-height: 100vh;
}
body.is-collect-page .collect-pc {
display: none;
}
body.is-collect-page .collect-nav {
position: sticky;
top: 0;
z-index: 40;
width: 100%;
height: 48px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
background: #181818;
}
body.is-collect-page .collect-nav-back {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
body.is-collect-page .collect-nav-back img {
width: 24px;
height: 24px;
display: block;
opacity: 0.65;
filter: brightness(0) invert(1);
}
body.is-collect-page .collect-nav-title {
margin: 0;
flex: 1 1 0;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 16px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 22px;
}
body.is-collect-page .collect-nav-spacer {
width: 24px;
height: 24px;
flex-shrink: 0;
}
body.is-collect-page .collect-main {
width: calc(100% - 32px);
max-width: 343px;
margin: 0 auto;
padding: 12px 0;
align-items: stretch;
gap: 12px;
box-sizing: border-box;
}
body.is-collect-page .collect-tabs {
width: auto;
height: 42px;
padding: 4px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
display: inline-flex;
justify-content: flex-start;
align-items: flex-start;
gap: 4px;
box-sizing: border-box;
align-self: flex-start;
margin: 0 auto;
}
body.is-collect-page .collect-tab {
width: 109px;
flex: 0 0 109px;
min-width: 109px;
height: 34px;
padding: 0;
background: transparent;
border: none;
overflow: hidden;
border-radius: 9px;
display: flex;
justify-content: center;
align-items: center;
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
box-sizing: border-box;
}
body.is-collect-page .collect-tab--active {
background: #EC237F;
color: #171717;
font-weight: 700;
}
body.is-collect-page .collect-grid {
width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
justify-content: stretch;
gap: 12px 7px;
}
body.is-collect-page .collect-card {
width: 100%;
border-radius: 10px;
}
body.is-collect-page .collect-card-body {
background: rgba(255, 255, 255, 0.05);
}
body.is-collect-page .collect-card-title {
color: rgba(255, 255, 255, 0.95);
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
}
body.is-collect-page .collect-card-desc {
color: rgba(255, 255, 255, 0.60);
}
body.is-collect-page .collect-card-tag {
color: rgba(255, 255, 255, 0.45);
}
body.is-collect-page .collect-card-progress {
color: rgba(255, 255, 255, 0.45);
}
body.is-collect-page .collect-card-cover {
height: 0;
padding-bottom: calc(224 / 168 * 100%);
}
body.is-collect-page .collect-card-cover-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
body.is-collect-page .collect-pagination {
margin-top: 0;
}
}
File diff suppressed because it is too large Load Diff
+15 -436
View File
@@ -309,6 +309,21 @@
flex-wrap: wrap;
}
.detail-episode-lock {
position: absolute;
top: 0;
right: 0;
padding: 0 4px;
border-radius: 0 7px 0 7px;
background: #ec237f;
color: #fff;
font-size: 9px;
font-family: PingFang SC;
font-weight: 500;
line-height: 14px;
pointer-events: none;
}
.detail-episode {
flex: 0 0 calc((100% - 98px) / 8);
width: calc((100% - 98px) / 8);
@@ -331,29 +346,6 @@
box-sizing: border-box;
}
.detail-episode-coin {
width: 16px;
height: 16px;
right: 6px;
left: auto;
top: 6px;
position: absolute;
display: block;
padding: 0;
background: none;
overflow: visible;
border-radius: 0;
box-sizing: border-box;
pointer-events: none;
}
.detail-episode-coin img {
width: 16px;
height: 16px;
object-fit: contain;
display: block;
}
.detail-comments {
align-self: stretch;
width: 100%;
@@ -416,189 +408,6 @@
color: #F5F5F5;
}
.detail-comment-composer {
align-self: stretch;
padding: 12px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 10px;
justify-content: flex-start;
align-items: flex-start;
gap: 12px;
display: flex;
box-sizing: border-box;
}
.detail-comment-composer-avatar {
width: 36px;
height: 36px;
border-radius: 18px;
object-fit: cover;
flex-shrink: 0;
}
.detail-comment-composer-main {
flex: 1;
min-width: 0;
overflow: hidden;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 12px;
display: flex;
}
.detail-comment-composer-input {
width: 100%;
min-height: 56px;
padding: 10px 12px;
background: #000;
overflow: hidden;
border-radius: 8px;
border: none;
resize: none;
box-sizing: border-box;
box-shadow: none;
outline: none;
outline-color: transparent;
-webkit-appearance: none;
appearance: none;
color: #F5F5F5;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.detail-comment-composer-input:focus,
.detail-comment-composer-input:focus-visible {
border: none;
outline: none;
outline-color: transparent;
box-shadow: none;
}
.detail-comment-composer-input::placeholder {
color: #A3A3A3;
}
.detail-comment-composer-footer {
width: 100%;
height: 28px;
overflow: hidden;
justify-content: space-between;
align-items: center;
display: flex;
}
.detail-comment-composer-status {
height: 20px;
overflow: hidden;
justify-content: flex-start;
align-items: center;
gap: 8px;
display: flex;
}
.detail-comment-composer-logged {
color: #ec237f;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.detail-comment-composer-user {
color: #A3A3A3;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.detail-comment-composer-count {
color: #D4D4D4;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.detail-comment-composer-submit {
width: 84px;
height: 28px;
background: #ec237f;
overflow: hidden;
border-radius: 14px;
justify-content: center;
align-items: center;
display: flex;
border: none;
cursor: pointer;
color: #fff;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
padding: 0;
position: relative;
gap: 6px;
box-sizing: border-box;
}
.detail-comment-composer-submit:disabled,
.detail-comment-composer-submit.is-loading {
opacity: 0.75;
cursor: not-allowed;
}
.detail-comment-composer-submit.is-loading::before {
content: "";
width: 12px;
height: 12px;
border: 2px solid rgba(24, 24, 24, 0.35);
border-top-color: #181818;
border-radius: 50%;
animation: detail-comment-spin 0.7s linear infinite;
flex-shrink: 0;
}
@keyframes detail-comment-spin {
to {
transform: rotate(360deg);
}
}
.detail-comments-login {
align-self: stretch;
height: 44px;
padding: 0 12px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 7px;
justify-content: flex-start;
align-items: center;
display: flex;
box-sizing: border-box;
}
.detail-comments-login-link {
color: #ec237f;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
text-decoration: none;
}
.detail-comments-login-hint {
color: #A3A3A3;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.detail-comment-list {
align-self: stretch;
width: 100%;
@@ -675,50 +484,6 @@
line-height: 18px;
}
.detail-comment-like {
display: inline-flex;
align-items: center;
gap: 4px;
background: none;
border: none;
cursor: pointer;
padding: 0;
color: #A3A3A3;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.detail-comment-like img {
width: 14px;
height: 14px;
}
.detail-comment-like--active {
color: #A3A3A3;
}
.detail-comment-like--active img {
filter: none;
}
.detail-comment-reply-label {
color: #A3A3A3;
font-size: 12px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 400;
line-height: 18px;
background: none;
border: none;
padding: 0;
cursor: pointer;
}
.detail-comment-reply-label:hover {
color: #ec237f;
}
.detail-comment-reply-to {
color: #A3A3A3;
}
@@ -778,89 +543,6 @@
opacity: 0.92;
}
.detail-comment-reply-hint {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
color: #A3A3A3;
font-size: 12px;
line-height: 18px;
}
.detail-comment-reply-hint button {
background: none;
border: none;
padding: 0;
color: #ec237f;
cursor: pointer;
font-size: 12px;
}
.detail-comment-inline-composer {
width: 100%;
margin-top: 8px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 6px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 8px;
}
.detail-comment-inline-input {
width: 100%;
min-height: 56px;
padding: 8px 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
background: #0A0A0A;
color: #F5F5F5;
font-size: 13px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
line-height: 20px;
resize: none;
box-sizing: border-box;
}
.detail-comment-inline-input::placeholder {
color: #737373;
}
.detail-comment-inline-input:focus,
.detail-comment-inline-input:focus-visible {
border-color: rgba(236, 35, 127, 0.55);
outline: none;
outline-color: transparent;
box-shadow: none;
}
.detail-comment-inline-footer {
display: flex;
justify-content: flex-end;
}
.detail-comment-inline-send {
height: 32px;
padding: 0 14px;
border: none;
border-radius: 6px;
background: #ec237f;
color: #fff;
font-size: 13px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 500;
cursor: pointer;
}
.detail-comment-inline-send:disabled,
.detail-comment-inline-send.is-loading {
opacity: 0.6;
cursor: not-allowed;
}
.detail-comments-more {
align-self: stretch;
height: 43px;
@@ -1048,23 +730,6 @@
box-sizing: border-box;
}
.detail-h5-top-btn--like {
width: auto;
min-width: 20px;
height: auto;
gap: 4px;
}
.detail-h5-top-like-count {
color: rgba(255, 255, 255, 0.92);
font-size: 11px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 400;
line-height: 12px;
white-space: nowrap;
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.55);
}
.detail-h5-top-btn img {
width: 18px;
height: 18px;
@@ -1074,12 +739,6 @@
opacity: 0.92;
}
.detail-h5-top-btn--active img,
.detail-h5-top-btn img[src*="h5_liked"] {
filter: none;
opacity: 1;
}
.detail-page.page-shell {
position: relative;
z-index: 1;
@@ -1305,19 +964,6 @@
text-align: center;
}
.detail-episode-coin {
width: 16px;
height: 16px;
left: auto;
right: 6px;
top: 6px;
}
.detail-episode-coin img {
width: 16px;
height: 16px;
}
.detail-related {
width: 100%;
gap: 12px;
@@ -1467,50 +1113,6 @@
gap: 12px;
}
.detail-h5-collect,
.detail-h5-like {
width: 132px;
padding: 11px 24px;
background: rgba(255, 255, 255, 0.10);
border: none;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 12px;
cursor: pointer;
color: white;
font-size: 16px;
font-family: PingFang SC;
font-weight: 400;
line-height: 22px;
flex-shrink: 0;
}
.detail-h5-collect--active,
.detail-h5-like--active {
color: #ec237f;
}
.detail-h5-collect--active img,
.detail-h5-like--active img {
filter: none;
}
.detail-h5-collect img,
.detail-h5-like img {
width: 22px;
height: 22px;
display: block;
object-fit: contain;
filter: none;
}
.detail-h5-collect--active img,
.detail-h5-like--active img {
filter: none;
}
.detail-h5-play {
flex: 1;
padding: 11px 24px;
@@ -1823,18 +1425,6 @@
text-align: center;
}
.detail-episode-coin {
width: calc(16px * var(--detail-s));
height: calc(16px * var(--detail-s));
top: calc(6px * var(--detail-s));
right: calc(6px * var(--detail-s));
}
.detail-episode-coin img {
width: 100%;
height: 100%;
}
.content-official-info {
padding: calc(24px * var(--detail-s)) calc(30px * var(--detail-s));
border-radius: calc(18px * var(--detail-s));
@@ -1876,15 +1466,6 @@
line-height: calc(18px * var(--detail-s));
}
.detail-comment-composer,
.detail-comment-login {
min-height: calc(44px * var(--detail-s));
padding: 12px calc(12px * var(--detail-s));
border-radius: calc(7px * var(--detail-s));
font-size: calc(12px * var(--detail-s));
line-height: calc(18px * var(--detail-s));
}
.detail-related {
display: flex;
width: calc(296px * var(--detail-s));
@@ -2034,7 +1615,5 @@
line-height: 14px;
}
.detail-h5-collect--active,
.detail-h5-like--active { color: #ec237f; }
.detail-h5-play { background: #ec237f; color: #fff; }
}
-382
View File
@@ -1,382 +0,0 @@
.edit-password-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #0A0A0A;
}
.edit-password-layout .site-header,
.edit-password-layout .footer-box {
flex-shrink: 0;
}
.edit-password-page {
width: 100%;
flex: 1 1 auto;
padding: 36px 0 48px;
background: #0A0A0A;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
box-sizing: border-box;
}
.edit-password-pc {
width: 100%;
max-width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
}
.edit-password-breadcrumb {
display: inline-flex;
align-items: center;
gap: 10px;
}
.edit-password-breadcrumb-link {
color: #EC237F;
font-size: 14px;
font-family: PingFang SC, "Noto Sans SC", sans-serif;
font-weight: 400;
line-height: 20px;
text-decoration: none;
}
.edit-password-breadcrumb-sep {
color: #666666;
font-size: 14px;
font-family: PingFang SC, "Noto Sans SC", sans-serif;
font-weight: 400;
line-height: 20px;
}
.edit-password-breadcrumb-current {
color: #8A8A8A;
font-size: 14px;
font-family: PingFang SC, "Noto Sans SC", sans-serif;
font-weight: 400;
line-height: 20px;
}
.edit-password-pc-head {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.edit-password-pc-title {
margin: 0;
color: #F5F5F5;
font-size: 30px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 36px;
}
.edit-password-pc-desc {
color: rgba(255, 255, 255, 0.60);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 20px;
}
.edit-password-shell {
width: 100%;
max-width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: stretch;
}
.edit-password-nav {
display: none;
}
.edit-password-form {
width: 100%;
max-width: 480px;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 14px;
overflow: hidden;
}
.edit-password-intro {
width: 100%;
height: 86px;
min-height: 86px;
padding: 14px 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
box-sizing: border-box;
}
.edit-password-intro-title {
color: rgba(255, 255, 255, 0.95);
font-size: 18px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 24px;
}
.edit-password-intro-desc {
width: 100%;
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
}
.edit-password-field {
width: 100%;
height: 78px;
min-height: 78px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
box-sizing: border-box;
}
.edit-password-field.is-error {
height: auto;
min-height: 78px;
border-color: rgba(255, 107, 107, 0.65);
}
.edit-password-error {
margin: 0;
color: #FF6B6B;
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
}
.edit-password-error[hidden] {
display: none;
}
.edit-password-label {
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 18px;
}
.edit-password-input-row {
width: 100%;
height: 30px;
min-height: 30px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
overflow: hidden;
}
.edit-password-input {
flex: 1 1 0;
min-width: 0;
height: 30px;
padding: 0;
border: none;
background: transparent;
box-shadow: none;
outline: none;
outline-color: transparent;
-webkit-appearance: none;
appearance: none;
color: rgba(255, 255, 255, 0.95);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 20px;
}
.edit-password-input:focus,
.edit-password-input:focus-visible {
border: none;
outline: none;
outline-color: transparent;
box-shadow: none;
}
.edit-password-input::placeholder {
color: rgba(255, 255, 255, 0.40);
}
.edit-password-toggle {
flex-shrink: 0;
padding: 0;
border: none;
background: transparent;
color: #EC237F;
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.edit-password-submit {
width: 100%;
height: 50px;
background: #EC237F;
border: none;
border-radius: 14px;
color: #171717;
font-size: 16px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 22px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-sizing: border-box;
overflow: hidden;
}
.edit-password-submit:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.edit-password-reset {
width: 100%;
min-height: 18px;
padding: 0;
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 14px;
color: #EC237F;
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 18px;
text-decoration: none;
overflow: hidden;
box-sizing: border-box;
}
@media (max-width: 767px) {
body.is-edit-password-page {
background: #0A0A0A;
padding-bottom: 0;
}
body.is-edit-password-page .site-header,
body.is-edit-password-page .footer-box,
body.is-edit-password-page .floating-action,
body.is-edit-password-page .download-fab {
display: none !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
.edit-password-page {
padding: 0;
gap: 0;
align-items: stretch;
min-height: 100vh;
}
.edit-password-pc {
display: none;
}
.edit-password-shell {
width: 100%;
max-width: 100%;
min-height: 100vh;
align-items: center;
}
.edit-password-nav {
position: sticky;
top: 0;
z-index: 40;
width: 100%;
height: 48px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
background: #181818;
overflow: hidden;
}
.edit-password-back {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.edit-password-back img {
width: 24px;
height: 24px;
display: block;
opacity: 0.65;
filter: brightness(0) invert(1);
}
.edit-password-nav-title {
flex: 1 1 0;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #F5F5F5;
font-size: 18px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 24px;
overflow: hidden;
}
.edit-password-nav-spacer {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.edit-password-form {
width: calc(100% - 32px);
max-width: 343px;
margin: 0 auto;
padding: 16px 0;
box-sizing: border-box;
}
}
-347
View File
@@ -1,347 +0,0 @@
.edit-profile-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #0A0A0A;
}
.edit-profile-layout .site-header,
.edit-profile-layout .footer-box {
flex-shrink: 0;
}
.edit-profile-page {
width: 100%;
flex: 1 1 auto;
background: #0A0A0A;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
box-sizing: border-box;
}
.edit-profile-pc {
display: none;
}
.setting-breadcrumb {
display: inline-flex;
align-items: center;
gap: 10px;
}
.setting-breadcrumb-link {
color: #EC237F;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
text-decoration: none;
}
.setting-breadcrumb-sep {
color: #666666;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.setting-breadcrumb-current {
color: #8A8A8A;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.edit-profile-h5 {
width: 100%;
max-width: 480px;
min-height: 100%;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
}
.edit-profile-h5-nav {
position: sticky;
top: 0;
z-index: 40;
width: 100%;
height: 48px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
background: #181818;
overflow: hidden;
}
.edit-profile-h5-back {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.edit-profile-h5-back img {
width: 24px;
height: 24px;
display: block;
opacity: 0.65;
filter: brightness(0) invert(1);
}
.edit-profile-h5-nav-title {
flex: 1 1 0;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #F5F5F5;
font-size: 18px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 24px;
overflow: hidden;
}
.edit-profile-h5-nav-spacer {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.edit-profile-h5-body {
width: calc(100% - 32px);
max-width: 343px;
margin: 0 auto;
padding: 16px 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 14px;
box-sizing: border-box;
overflow: hidden;
}
.edit-profile-avatar-card {
width: 100%;
height: 112px;
padding: 0 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
box-sizing: border-box;
}
.edit-profile-avatar-text {
width: 190px;
max-width: 100%;
min-width: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
overflow: hidden;
}
.edit-profile-avatar-title {
color: rgba(255, 255, 255, 0.95);
font-size: 16px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 22px;
}
.edit-profile-avatar-desc {
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
}
.edit-profile-avatar-wrap {
position: relative;
width: 64px;
height: 64px;
flex-shrink: 0;
}
.edit-profile-avatar {
width: 64px;
height: 64px;
border-radius: 999px;
object-fit: cover;
display: block;
border: 1px solid rgba(250, 220, 68, 0.35);
}
.edit-profile-field {
width: 100%;
height: 76px;
min-height: 76px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
box-sizing: border-box;
}
.edit-profile-field--bio {
height: 96px;
min-height: 96px;
}
.edit-profile-field-label {
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 18px;
}
.edit-profile-field-input {
width: 100%;
padding: 0;
border: none;
background: transparent;
box-shadow: none;
outline: none;
outline-color: transparent;
-webkit-appearance: none;
appearance: none;
color: rgba(255, 255, 255, 0.95);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 20px;
box-sizing: border-box;
}
.edit-profile-field-input:focus,
.edit-profile-field-input:focus-visible {
border: none;
outline: none;
outline-color: transparent;
box-shadow: none;
}
.edit-profile-field-input::placeholder {
color: rgba(255, 255, 255, 0.40);
}
.edit-profile-field-input--bio {
resize: none;
min-height: 40px;
}
.edit-profile-submit {
width: 100%;
height: 50px;
background: #EC237F;
border: none;
border-radius: 14px;
color: #171717;
font-size: 16px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 22px;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-sizing: border-box;
overflow: hidden;
}
.edit-profile-submit:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.edit-profile-tip {
margin: 0;
color: rgba(255, 255, 255, 0.40);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
}
@media (min-width: 768px) {
.edit-profile-page {
padding: 36px 0 60px;
gap: 18px;
}
.edit-profile-pc {
width: 100%;
max-width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.edit-profile-h5 {
width: 100%;
max-width: calc(100% - 32px);
}
.edit-profile-h5-nav {
display: none;
}
.edit-profile-h5-body {
width: 100%;
max-width: 480px;
margin: 0;
padding-top: 0;
}
}
@media (max-width: 767px) {
body.is-edit-profile-page {
background: #0A0A0A;
padding-bottom: 0;
}
body.is-edit-profile-page .site-header,
body.is-edit-profile-page .footer-box,
body.is-edit-profile-page .floating-action {
display: none !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
.edit-profile-pc {
display: none;
}
.edit-profile-h5 {
max-width: 100%;
min-height: 100vh;
}
}
+1 -1
View File
@@ -615,7 +615,7 @@
}
.site-shell .brand-logo {
content: url("/static/images/logo-h5.png");
content: url("/static/images/logo-h5.png?v=3");
width: 92px;
height: 24px;
object-fit: contain;
-552
View File
@@ -1,552 +0,0 @@
.history-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.history-layout .site-header,
.history-layout .footer-box {
flex-shrink: 0;
}
.history-page {
width: 100%;
flex: 1 1 auto;
min-height: 0;
padding: 36px 0;
background: #0A0A0A;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
box-sizing: border-box;
}
.history-pc {
width: min(1800px, calc(100% - 120px));
max-width: none;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.history-nav {
display: none;
}
.history-main {
width: min(1800px, calc(100% - 120px));
max-width: none;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
}
.history-breadcrumb {
align-self: stretch;
overflow: hidden;
display: inline-flex;
justify-content: flex-start;
align-items: center;
gap: 10px;
}
.history-breadcrumb-link {
color: #EC237F;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
text-decoration: none;
}
.history-breadcrumb-sep {
color: #666666;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.history-breadcrumb-current {
color: #8A8A8A;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.history-head {
align-self: stretch;
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 6px;
}
.history-head-text {
flex: 1 1 0;
min-width: 0;
overflow: hidden;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 12px;
}
.history-title {
margin: 0;
color: white;
font-size: 30px;
font-family: PingFang SC;
font-weight: 400;
line-height: 36px;
flex-shrink: 0;
}
.history-desc {
margin: 0;
color: #A3A3A3;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.history-clear-btn {
width: 96px;
height: 36px;
background: rgba(255, 255, 255, 0.10);
overflow: hidden;
border: none;
border-radius: 18px;
color: #F5F5F5;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
cursor: pointer;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
.history-clear-btn:hover {
background: rgba(255, 255, 255, 0.14);
color: #fff;
}
.history-confirm-mask {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
padding: 24px;
box-sizing: border-box;
}
.history-confirm-mask.is-open {
display: flex;
}
.history-confirm-mask[hidden] {
display: none !important;
}
.history-confirm-card {
width: 420px;
max-width: 100%;
padding: 32px 28px 28px;
position: relative;
background: linear-gradient(90deg, #21142E 0%, #0A0A17 100%);
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.46);
border-radius: 20px;
border: 1px solid rgba(241, 43, 158, 0.44);
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
box-sizing: border-box;
}
.history-confirm-close {
width: 28px;
height: 28px;
right: 14px;
top: 14px;
position: absolute;
overflow: hidden;
border: none;
padding: 0;
background: transparent;
cursor: pointer;
}
.history-confirm-close-ring {
width: 26px;
height: 26px;
left: 1px;
top: 1px;
position: absolute;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%;
box-sizing: border-box;
}
.history-confirm-close-x {
width: 8px;
height: 8px;
left: 10px;
top: 10px;
position: absolute;
box-sizing: border-box;
}
.history-confirm-close-x::before,
.history-confirm-close-x::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 10px;
height: 1.5px;
background: #C5C0D0;
border-radius: 1px;
}
.history-confirm-close-x::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.history-confirm-close-x::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.history-confirm-title {
margin-top: 4px;
text-align: center;
color: #fff;
font-size: 20px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 700;
line-height: 28px;
}
.history-confirm-desc {
text-align: center;
color: #B2ABC4;
font-size: 14px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 400;
line-height: 20px;
}
.history-confirm-actions {
width: 100%;
margin-top: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.history-confirm-btn {
flex: 1 1 0;
height: 44px;
border: none;
border-radius: 12px;
font-size: 14px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 500;
line-height: 20px;
cursor: pointer;
}
.history-confirm-btn--cancel {
background: rgba(255, 255, 255, 0.08);
color: #C5C0D0;
}
.history-confirm-btn--cancel:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.history-confirm-btn--ok {
background: #EC237F;
color: #181818;
}
.history-confirm-btn--ok:hover {
background: #ffe56a;
}
.history-grid {
align-self: stretch;
width: 100%;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
justify-content: stretch;
align-items: stretch;
gap: 14px 16px;
}
.history-card.poster-card {
width: 100%;
min-width: 0;
overflow: hidden;
border-radius: 14px;
text-decoration: none;
color: inherit;
}
.history-empty {
width: 100%;
}
.history-card .poster-card-spacer {
flex: 1 1 0;
min-width: 8px;
height: 9px;
}
.history-progress {
color: #808080 !important;
}
.history-info {
display: none;
}
.history-pagination {
align-self: stretch;
width: 100%;
overflow: hidden;
margin-top: 0;
}
@media (max-width: 767px) {
body.is-history-page {
background: #0A0A0A;
}
body.is-history-page .site-header,
body.is-history-page .footer-box,
body.is-history-page .floating-action,
body.is-history-page .download-fab {
display: none !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
body.is-history-page .history-layout {
min-height: 100vh;
background: #0A0A0A;
}
body.is-history-page .history-page {
padding: 0;
background: #0A0A0A;
align-items: stretch;
justify-content: flex-start;
gap: 0;
min-height: 100vh;
}
body.is-history-page .history-pc {
display: none;
}
body.is-history-page .history-nav {
position: sticky;
top: 0;
z-index: 40;
width: 100%;
height: 48px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
background: #181818;
}
body.is-history-page .history-nav-back {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
body.is-history-page .history-nav-back img {
width: 24px;
height: 24px;
display: block;
opacity: 0.65;
filter: brightness(0) invert(1);
}
body.is-history-page .history-nav-title {
margin: 0;
flex: 1 1 0;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 16px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 22px;
}
body.is-history-page .history-nav-clear {
min-width: 24px;
height: 24px;
padding: 0;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 18px;
cursor: pointer;
flex-shrink: 0;
-webkit-tap-highlight-color: transparent;
}
body.is-history-page .history-main {
width: calc(100% - 32px);
max-width: 343px;
margin: 0 auto;
padding: 12px 0;
align-items: stretch;
gap: 12px;
box-sizing: border-box;
}
body.is-history-page .history-grid {
width: 100%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 12px;
}
body.is-history-page .history-card.poster-card {
width: 100%;
max-width: none;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 12px;
box-sizing: border-box;
}
body.is-history-page .history-card .poster-cover {
width: 90px;
height: 120px;
padding-bottom: 0;
border-radius: 10px;
overflow: hidden;
position: relative;
flex-shrink: 0;
}
body.is-history-page .history-card .poster-cover-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
body.is-history-page .history-card .free-badge-large,
body.is-history-page .history-card .hot-badge-large,
body.is-history-page .history-card .community-badge,
body.is-history-page .history-card .poster-corner-badges,
body.is-history-page .history-card .episode-badge,
body.is-history-page .history-card .poster-card-body,
body.is-history-page .history-pagination {
display: none !important;
}
body.is-history-page .history-info {
flex: 1 1 0;
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
gap: 4px;
}
body.is-history-page .history-name {
width: 100%;
color: rgba(255, 255, 255, 0.95);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 20px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
body.is-history-page .history-intro {
width: 100%;
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
body.is-history-page .history-watch {
width: 100%;
color: #EC237F;
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
}
body.is-history-page .history-watch--done {
color: rgba(255, 255, 255, 0.45);
}
body.is-history-page .history-time {
color: rgba(255, 255, 255, 0.45);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
}
}
-205
View File
@@ -1,205 +0,0 @@
.login-dialog-mask {
position: fixed;
inset: 0;
z-index: 1100;
display: none;
align-items: center;
justify-content: center;
overflow-y: auto;
padding: 24px;
box-sizing: border-box;
background: rgba(0, 0, 0, 0.7);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.login-dialog-mask.is-open { display: flex; }
.login-dialog-stack {
width: 440px;
max-width: 100%;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.login-dialog-card {
width: 440px;
max-width: 100%;
padding: 36px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
color: #fff;
background: #0d0d0d;
border: 1px solid rgba(255,255,255,.2);
border-radius: 40px;
font-family: "PingFang SC", "Noto Sans SC", sans-serif;
}
.login-dialog-head {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.login-dialog-title {
color: #fff;
font-size: 24px;
font-weight: 600;
line-height: 32px;
text-align: left;
}
.login-dialog-title-h5 { display: none; }
.login-dialog-desc {
color: rgba(255,255,255,.55);
font-size: 14px;
font-weight: 400;
line-height: 20px;
text-align: left;
}
.login-dialog-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}
.login-dialog-field-group {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.login-dialog-label {
display: block;
color: rgba(255,255,255,.55);
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
.login-dialog-field {
width: 100%;
min-height: 60px;
padding: 18px;
box-sizing: border-box;
display: flex;
align-items: center;
overflow: hidden;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.05);
border-radius: 6px;
}
.login-dialog-field--password { justify-content: space-between; gap: 10px; }
.login-dialog-sr {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
.login-dialog-input {
width: 100%;
min-width: 0;
padding: 0;
color: #fff;
font: 400 16px/22px "PingFang SC", "Noto Sans SC", sans-serif;
background: transparent;
border: 0;
outline: 0;
}
.login-dialog-input::placeholder { color: rgba(255,255,255,.35); opacity: 1; }
.login-dialog-eye {
width: 20px;
height: 20px;
padding: 0;
flex: 0 0 20px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 0;
cursor: pointer;
}
.login-dialog-eye img { display: block; width: 20px; height: 20px; }
.login-dialog-error {
margin: 0;
color: #ef4444;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.login-dialog-error[hidden] { display: none; }
.login-dialog-submit {
width: 100%;
min-height: 50px;
padding: 13px 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font: 600 18px/24px "PingFang SC", "Noto Sans SC", sans-serif;
background: #ec237f;
border: 0;
border-radius: 12px;
cursor: pointer;
}
.login-dialog-submit.is-loading,
.login-dialog-submit:disabled { opacity: .75; cursor: not-allowed; }
.login-dialog-register {
width: 100%;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.login-dialog-register-prefix,
.login-dialog-register-link {
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.login-dialog-register-prefix { color: rgba(255,255,255,.55); }
.login-dialog-register-link { color: #ec237f; text-decoration: none; }
.login-dialog-register-link:hover { color: #ff5b9f; }
.login-dialog-register-link--h5 { display: none; }
.login-dialog-close {
width: 36px;
height: 36px;
padding: 0;
flex: 0 0 36px;
border: 0;
background: transparent;
cursor: pointer;
}
.login-dialog-close img { display: block; width: 36px; height: 36px; }
@media (max-width: 767px) {
.login-dialog-mask {
padding: 16px;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
}
.login-dialog-stack { width: 300px; }
.login-dialog-card { width: 300px; padding: 24px 20px; border-radius: 20px; }
.login-dialog-title-pc { display: none; }
.login-dialog-title-h5 { display: inline; }
.login-dialog-title { font-size: 16px; line-height: 22px; }
.login-dialog-desc { font-size: 12px; line-height: 18px; }
.login-dialog-head { gap: 6px; }
.login-dialog-form { gap: 12px; }
.login-dialog-field-group { gap: 6px; }
.login-dialog-label { font-size: 14px; line-height: 20px; }
.login-dialog-field { min-height: 54px; padding: 16px 18px; }
.login-dialog-input { font-size: 14px; line-height: 20px; }
.login-dialog-submit { font-weight: 400; }
.login-dialog-register { height: 24px; }
.login-dialog-register-prefix,
.login-dialog-register-link { font-size: 12px; line-height: 18px; }
.login-dialog-register-link--pc { display: none; }
.login-dialog-register-link--h5 { display: inline; }
}
+66 -248
View File
@@ -739,37 +739,70 @@ a.play-info-tag:focus-visible {
display: none;
}
.play-ep-lock,
.play-ep-coin {
.play-ep-lock {
position: absolute;
right: 6px;
right: 4px;
left: auto;
top: 6px;
width: 16px;
top: 4px;
height: 16px;
padding: 0;
background: none;
border-radius: 0;
padding: 0 4px;
background: linear-gradient(135deg, #FF5F62 0%, #EC237F 100%);
border-radius: 4px;
display: block;
overflow: visible;
box-sizing: border-box;
pointer-events: none;
z-index: 1;
object-fit: contain;
color: #fff;
font-size: 10px;
font-weight: 500;
line-height: 16px;
white-space: nowrap;
}
.play-ep-coin img {
width: 16px;
height: 16px;
object-fit: contain;
display: block;
}
.play-ep--active .play-ep-coin,
.play-ep--active .play-ep-lock {
display: none;
}
/* 付费剧集:播放器内引导下载 APP。 */
.play-player-locked {
position: absolute;
left: 50%;
top: 50%;
z-index: 4;
transform: translate(-50%, -50%);
width: calc(100% - 32px);
max-width: 320px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
text-align: center;
}
.play-player-locked-text {
margin: 0;
color: rgba(255, 255, 255, 0.85);
font-size: 14px;
line-height: 20px;
}
.play-player-locked-btn {
padding: 10px 24px;
border: none;
border-radius: 999px;
background: #ec237f;
color: #fff;
font-size: 15px;
font-weight: 500;
line-height: 22px;
cursor: pointer;
white-space: nowrap;
}
.play-player-locked-btn:hover {
opacity: 0.9;
}
@media (max-width: 1200px) {
.play-layout,
.play-main,
@@ -1202,11 +1235,6 @@ a.play-info-tag:focus-visible {
transform-origin: center;
}
.play-h5-side-item [data-h5-like-icon],
.play-h5-side-item [data-h5-collect-icon] {
transform: scale(0.8);
}
.play-h5-side-item[data-play-comment-open] img {
transform: scale(1.09);
}
@@ -1215,12 +1243,6 @@ a.play-info-tag:focus-visible {
transform: scale(1);
}
.play-h5-side-item--active img,
.play-h5-side-item img[src*="h5_liked"],
.play-h5-side-item img[src*="h5_collected"] {
filter: none;
}
.play-h5-meta {
display: inline-flex;
position: absolute;
@@ -1609,26 +1631,6 @@ a.play-info-tag:focus-visible {
line-height: 18px;
}
.play-ep-sheet-collect {
flex-shrink: 0;
padding: 4px 12px;
border: none;
border-radius: 999px;
background: #ec237f;
color: #fff;
font-size: 10px;
font-family: PingFang SC;
font-weight: 400;
line-height: 16px;
cursor: pointer;
text-align: center;
}
.play-ep-sheet-collect.is-collected {
background: rgba(255, 255, 255, 0.10);
color: #ec237f;
}
.play-ep-sheet-tabs {
width: 100%;
display: flex;
@@ -1724,33 +1726,26 @@ a.play-info-tag:focus-visible {
font-weight: 400;
}
.play-ep-sheet-coin,
.play-ep-sheet-lock {
position: absolute;
right: 6px;
right: 4px;
left: auto;
top: 6px;
width: 16px;
top: 4px;
height: 16px;
padding: 0;
background: none;
border-radius: 0;
padding: 0 4px;
background: linear-gradient(135deg, #FF5F62 0%, #EC237F 100%);
border-radius: 4px;
display: block;
overflow: visible;
z-index: 2;
box-sizing: border-box;
pointer-events: none;
object-fit: contain;
color: #fff;
font-size: 10px;
font-weight: 500;
line-height: 16px;
white-space: nowrap;
}
.play-ep-sheet-coin img {
width: 16px;
height: 16px;
object-fit: contain;
display: block;
}
.play-ep-sheet-item--active .play-ep-sheet-coin,
.play-ep-sheet-item--active .play-ep-sheet-lock {
display: none;
}
@@ -1861,7 +1856,7 @@ a.play-info-tag:focus-visible {
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px 16px 0;
padding: 18px 16px calc(12px + env(safe-area-inset-bottom, 0px));
min-height: 0;
flex: 1;
overflow: hidden;
@@ -2190,70 +2185,6 @@ a.play-info-tag:focus-visible {
font-size: 12px;
}
.play-comment-inline-composer {
width: 100%;
margin-top: 8px;
padding: 8px 10px;
background: rgba(255, 255, 255, 0.06);
border-radius: 8px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 6px;
}
.play-comment-inline-row {
display: flex;
align-items: center;
gap: 8px;
}
.play-comment-inline-input {
flex: 1;
min-width: 0;
height: 34px;
padding: 0 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 17px;
background: #0A0A0A;
color: #fff;
font-size: 13px;
font-family: PingFang SC;
box-sizing: border-box;
}
.play-comment-inline-input::placeholder {
color: rgba(255, 255, 255, 0.35);
}
.play-comment-inline-input:focus,
.play-comment-inline-input:focus-visible {
border-color: rgba(236, 35, 127, 0.55);
outline: none;
outline-color: transparent;
box-shadow: none;
}
.play-comment-inline-send {
flex-shrink: 0;
height: 34px;
padding: 0 12px;
border: none;
border-radius: 17px;
background: #ec237f;
color: #fff;
font-size: 13px;
font-family: PingFang SC;
font-weight: 500;
cursor: pointer;
}
.play-comment-inline-send:disabled,
.play-comment-inline-send.is-loading {
opacity: 0.6;
cursor: not-allowed;
}
.play-comment-replies-all {
background: none;
border: none;
@@ -2267,119 +2198,6 @@ a.play-info-tag:focus-visible {
text-align: left;
}
.play-comment-composer {
width: 100%;
height: calc(58px + env(safe-area-inset-bottom, 0px));
padding: 6px 16px calc(6px + env(safe-area-inset-bottom, 0px));
background: #1A1A1A;
box-sizing: border-box;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 10px;
}
.play-comment-composer-box {
align-self: stretch;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
min-height: 36px;
padding: 6px 12px;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
box-sizing: border-box;
text-decoration: none;
color: inherit;
}
.play-comment-composer-placeholder,
.play-comment-composer-login {
flex: 1;
min-width: 0;
color: rgba(255, 255, 255, 0.35);
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
text-decoration: none;
word-wrap: break-word;
}
.play-comment-composer-input {
flex: 1 1 0;
width: auto;
min-width: 0;
min-height: 0;
height: 18px;
max-height: 18px;
margin: 0;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
outline: none;
outline-color: transparent;
resize: none;
overflow: hidden;
-webkit-appearance: none;
appearance: none;
-webkit-tap-highlight-color: transparent;
color: #fff;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
caret-color: #fff;
}
.play-comment-composer-input:focus,
.play-comment-composer-input:focus-visible,
.play-comment-composer-input:active {
border: none;
outline: none;
outline-color: transparent;
box-shadow: none;
}
.play-comment-composer-input::placeholder {
color: rgba(255, 255, 255, 0.35);
opacity: 1;
}
.play-comment-composer-emoji {
width: 24px;
height: 24px;
padding: 0;
margin: 0;
border: none;
background: none;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: relative;
overflow: hidden;
}
.play-comment-composer-emoji.is-loading {
opacity: 0.5;
pointer-events: none;
}
.play-comment-composer-emoji img {
width: 24px;
height: 24px;
object-fit: contain;
display: block;
pointer-events: none;
}
body.play-comment-sheet-open {
overflow: hidden;
}
@@ -2859,11 +2677,12 @@ a.play-info-tag:focus-visible {
color: #ec237f;
}
.play-ep-coin {
width: calc(16px * var(--play-s));
.play-ep-lock {
height: calc(16px * var(--play-s));
top: calc(6px * var(--play-s));
right: calc(6px * var(--play-s));
font-size: calc(10px * var(--play-s));
line-height: calc(16px * var(--play-s));
}
.play-comments { width: 100%; }
@@ -3000,6 +2819,5 @@ a.play-info-tag:focus-visible {
.play-h5-bottom { background: #181818; }
.play-speed-sheet-option.is-active,
.play-ep-sheet-tab--active,
.play-ep-sheet-collect.is-collected { color: #ec237f; }
.play-ep-sheet-tab--active { color: #ec237f; }
}
-191
View File
@@ -1,191 +0,0 @@
.register-dialog-mask {
position: fixed;
inset: 0;
z-index: 1100;
display: none;
align-items: center;
justify-content: center;
overflow-y: auto;
padding: 24px;
box-sizing: border-box;
background: rgba(0,0,0,.7);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.register-dialog-mask.is-open { display: flex; }
.register-dialog-stack {
width: 440px;
max-width: 100%;
margin: auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.register-dialog-card {
width: 440px;
max-width: 100%;
padding: 36px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
color: #fff;
background: #0d0d0d;
border: 1px solid rgba(255,255,255,.2);
border-radius: 40px;
font-family: "PingFang SC", "Noto Sans SC", sans-serif;
}
.register-dialog-head {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.register-dialog-title {
color: #fff;
font-size: 24px;
font-weight: 600;
line-height: 32px;
text-align: left;
}
.register-dialog-desc {
color: rgba(255,255,255,.55);
font-size: 14px;
font-weight: 400;
line-height: 20px;
text-align: left;
}
.register-dialog-desc-h5 { display: none; }
.register-dialog-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 24px;
}
.register-dialog-field-group {
width: 100%;
display: flex;
flex-direction: column;
gap: 12px;
}
.register-dialog-label {
display: block;
color: rgba(255,255,255,.55);
font-size: 16px;
font-weight: 400;
line-height: 22px;
}
.register-dialog-field {
width: 100%;
min-height: 60px;
padding: 18px;
box-sizing: border-box;
display: flex;
align-items: center;
overflow: hidden;
background: rgba(255,255,255,.05);
border: 1px solid rgba(255,255,255,.05);
border-radius: 6px;
}
.register-dialog-sr {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
.register-dialog-input {
width: 100%;
min-width: 0;
padding: 0;
color: #fff;
font: 400 16px/22px "PingFang SC", "Noto Sans SC", sans-serif;
background: transparent;
border: 0;
outline: 0;
}
.register-dialog-input::placeholder { color: rgba(255,255,255,.35); opacity: 1; }
.register-dialog-error {
margin: 0;
color: #ef4444;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.register-dialog-error[hidden] { display: none; }
.register-dialog-submit {
width: 100%;
min-height: 50px;
padding: 13px 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font: 600 18px/24px "PingFang SC", "Noto Sans SC", sans-serif;
background: #ec237f;
border: 0;
border-radius: 12px;
cursor: pointer;
}
.register-dialog-submit.is-loading,
.register-dialog-submit:disabled { opacity: .75; cursor: not-allowed; }
.register-dialog-login {
width: 100%;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.register-dialog-login-prefix,
.register-dialog-login-link {
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.register-dialog-login-prefix { display: inline; color: rgba(255,255,255,.55); }
.register-dialog-login-link { color: #ec237f; text-decoration: none; }
.register-dialog-login-link:hover { color: #ff5b9f; }
.register-dialog-login-link--h5 { display: none; }
.register-dialog-close {
width: 36px;
height: 36px;
padding: 0;
flex: 0 0 36px;
border: 0;
background: transparent;
cursor: pointer;
}
.register-dialog-close img { display: block; width: 36px; height: 36px; }
@media (max-width: 767px) {
.register-dialog-mask {
padding: 16px;
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
}
.register-dialog-stack { width: 300px; }
.register-dialog-card { width: 300px; padding: 24px 20px; border-radius: 20px; }
.register-dialog-title { font-size: 16px; line-height: 22px; }
.register-dialog-desc { font-size: 12px; line-height: 18px; }
.register-dialog-desc-pc { display: none; }
.register-dialog-desc-h5 { display: inline; }
.register-dialog-head { gap: 6px; }
.register-dialog-form { gap: 12px; }
.register-dialog-field-group { gap: 6px; }
.register-dialog-label { font-size: 14px; line-height: 20px; }
.register-dialog-field { min-height: 54px; padding: 16px 18px; }
.register-dialog-input { font-size: 14px; line-height: 20px; }
.register-dialog-submit { font-weight: 400; }
.register-dialog-login { height: 24px; }
.register-dialog-login-prefix,
.register-dialog-login-link { font-size: 12px; line-height: 18px; }
.register-dialog-login-link--pc { display: none; }
.register-dialog-login-link--h5 { display: inline; }
}
-721
View File
@@ -1,721 +0,0 @@
.setting-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #0A0A0A;
}
.setting-layout .site-header,
.setting-layout .footer-box {
flex-shrink: 0;
}
.setting-page {
width: 100%;
flex: 1 1 auto;
min-height: 0;
padding: 36px 0;
background: #0A0A0A;
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
box-sizing: border-box;
}
.setting-pc {
width: 100%;
max-width: calc(100% - 32px);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 18px;
}
.setting-nav {
display: none;
}
.setting-main {
width: 100%;
max-width: calc(100% - 32px);
height: 370px;
display: flex;
justify-content: flex-start;
align-items: flex-start;
gap: 18px;
box-sizing: border-box;
}
.setting-breadcrumb {
display: inline-flex;
align-items: center;
gap: 10px;
overflow: hidden;
}
.setting-breadcrumb-link {
color: #EC237F;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
text-decoration: none;
}
.setting-breadcrumb-sep {
color: #666666;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.setting-breadcrumb-current {
color: #8A8A8A;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.setting-head {
align-self: stretch;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
overflow: hidden;
}
.setting-title {
margin: 0;
color: white;
font-size: 30px;
font-family: PingFang SC;
font-weight: 400;
line-height: 36px;
}
.setting-desc {
margin: 0;
color: #A3A3A3;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.setting-panel,
.setting-aside {
height: 100%;
min-height: 0;
padding: 24px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.10);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
box-sizing: border-box;
}
.setting-profile-pc {
width: 560px;
flex-shrink: 0;
}
.setting-profile-mobile {
display: none;
}
.setting-aside {
flex: 1 1 0;
align-self: stretch;
min-width: 0;
}
.setting-panel-title,
.setting-aside-title {
margin: 0;
color: white;
font-size: 20px;
font-family: PingFang SC;
font-weight: 400;
line-height: 26px;
}
.setting-section-label {
display: none;
}
.setting-avatar-row {
display: flex;
align-items: center;
gap: 14px;
overflow: hidden;
}
.setting-avatar {
width: 64px;
height: 64px;
border-radius: 999px;
object-fit: cover;
flex-shrink: 0;
}
.setting-avatar-text {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
overflow: hidden;
}
.setting-avatar-name {
color: white;
font-size: 18px;
font-family: PingFang SC;
font-weight: 400;
line-height: 24px;
}
.setting-avatar-change {
padding: 0;
border: none;
background: none;
color: #EC237F;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
cursor: pointer;
text-decoration: none;
}
.setting-field {
align-self: stretch;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
overflow: visible;
}
.setting-label {
color: #A3A3A3;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.setting-input {
align-self: stretch;
width: 100%;
min-height: 44px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.10);
color: #F5F5F5;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
box-sizing: border-box;
-webkit-appearance: none;
appearance: none;
box-shadow: none;
outline: none;
outline-color: transparent;
opacity: 1;
-webkit-text-fill-color: #F5F5F5;
}
.setting-input--static {
display: flex;
align-items: center;
min-height: 44px;
}
.setting-input:focus,
.setting-input:focus-visible {
border-color: rgba(250, 220, 68, 0.55);
outline: none;
outline-color: transparent;
box-shadow: none;
}
.setting-input:read-only,
.setting-input[readonly] {
opacity: 1;
color: #F5F5F5;
-webkit-text-fill-color: #F5F5F5;
cursor: default;
}
.setting-input::placeholder {
color: #A3A3A3;
-webkit-text-fill-color: #A3A3A3;
opacity: 1;
}
.setting-save-btn {
width: 128px;
height: 40px;
background: #EC237F;
border: none;
border-radius: 20px;
color: #181818;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
box-sizing: border-box;
}
.setting-group {
align-self: stretch;
display: flex;
flex-direction: column;
align-items: stretch;
}
.setting-group--security {
margin: 0;
}
.setting-item {
align-self: stretch;
padding: 12px 0;
border: none;
background: transparent;
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas:
"label action"
"hint action";
align-items: center;
column-gap: 12px;
row-gap: 2px;
box-sizing: border-box;
text-align: left;
text-decoration: none;
color: inherit;
cursor: pointer;
font: inherit;
-webkit-tap-highlight-color: transparent;
overflow: hidden;
}
.setting-group--notify .setting-item {
display: flex;
padding: 8px 0;
cursor: default;
}
.setting-item-label {
grid-area: label;
color: white;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
}
.setting-group--notify .setting-item-label {
color: #DBD7E4;
}
.setting-item-hint {
grid-area: hint;
color: #8A8A8A;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
}
.setting-item-action {
grid-area: action;
padding: 0;
border: none;
background: none;
color: #EC237F;
font-size: 12px;
font-family: PingFang SC;
font-weight: 400;
line-height: 18px;
flex-shrink: 0;
}
.setting-item-right,
.setting-item-value,
.setting-item-arrow,
.setting-item-avatar {
display: none;
}
.setting-switch {
width: 42px;
height: 24px;
margin-left: auto;
position: relative;
background: rgba(255, 255, 255, 0.12);
border: none;
border-radius: 12px;
cursor: pointer;
flex-shrink: 0;
padding: 0;
}
.setting-switch::after {
content: "";
width: 18px;
height: 18px;
position: absolute;
left: 3px;
top: 3px;
background: white;
border-radius: 9999px;
transition: left 0.15s ease;
}
.setting-switch--on {
background: #EC237F;
}
.setting-switch--on::after {
left: 21px;
}
.setting-logout {
width: 130px;
height: 40px;
background: rgba(255, 255, 255, 0.05);
overflow: hidden;
border-radius: 20px;
border: 1px solid #EC237F;
display: inline-flex;
justify-content: center;
align-items: center;
color: #EC237F;
font-size: 14px;
font-family: PingFang SC;
font-weight: 400;
line-height: 20px;
text-decoration: none;
box-sizing: border-box;
cursor: pointer;
}
@media (max-width: 1100px) {
.setting-main {
height: auto;
flex-wrap: wrap;
}
.setting-profile-pc,
.setting-aside {
width: 100%;
height: auto;
}
}
@media (max-width: 767px) {
body.is-setting-page {
background: #0A0A0A;
padding-bottom: 0;
}
body.is-setting-page .site-header,
body.is-setting-page .footer-box,
body.is-setting-page .floating-action,
body.is-setting-page .download-fab {
display: none !important;
height: 0 !important;
min-height: 0 !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
.setting-layout {
min-height: 100vh;
background: #0A0A0A;
}
.setting-page {
padding: 0;
background: #0A0A0A;
align-items: stretch;
justify-content: flex-start;
gap: 0;
min-height: 100vh;
height: auto;
overflow: visible;
flex: 1 1 auto;
}
.setting-pc {
display: none;
}
.setting-nav {
position: sticky;
top: 0;
z-index: 40;
width: 100%;
height: 48px;
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
background: #181818;
overflow: hidden;
}
.setting-nav-back {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.setting-nav-back img {
width: 24px;
height: 24px;
display: block;
opacity: 0.55;
filter: brightness(0) invert(1);
}
.setting-nav-title {
margin: 0;
flex: 1 1 0;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
font-size: 16px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 22px;
overflow: hidden;
}
.setting-nav-spacer {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.setting-main {
width: calc(100% - 32px);
max-width: 343px;
height: auto;
margin: 0 auto;
padding: 14px 0;
flex: 1 1 auto;
flex-direction: column;
align-items: stretch;
gap: 10px;
box-sizing: border-box;
min-height: 0;
overflow: hidden;
}
.setting-profile-pc {
display: none;
}
.setting-profile-mobile {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
flex: none;
width: 100%;
overflow: hidden;
}
.setting-aside {
flex: none;
width: 100%;
min-height: 0;
height: auto;
padding: 0;
background: transparent;
border: none;
border-radius: 0;
overflow: visible;
gap: 10px;
align-self: stretch;
}
.setting-aside-title {
display: none;
}
.setting-section-label {
display: block;
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 18px;
}
.setting-group {
width: 100%;
background: rgba(255, 255, 255, 0.05);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
overflow: hidden;
}
.setting-item {
width: 100%;
height: 52px;
padding: 0 14px;
border-bottom: 1px rgba(255, 255, 255, 0.10) solid;
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
box-sizing: border-box;
overflow: hidden;
}
.setting-group .setting-item:last-child {
border-bottom: none;
}
.setting-item--avatar {
height: 76px;
}
.setting-item-hint,
.setting-item-action {
display: none;
}
.setting-item-label {
color: rgba(255, 255, 255, 0.90);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 20px;
flex-shrink: 0;
}
.setting-item-right {
min-width: 0;
display: flex;
justify-content: flex-start;
align-items: center;
gap: 6px;
overflow: hidden;
}
.setting-item-value {
display: block;
color: rgba(255, 255, 255, 0.60);
font-size: 12px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 400;
line-height: 18px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.setting-item-avatar {
width: 48px;
height: 48px;
border-radius: 999px;
object-fit: cover;
display: block;
flex-shrink: 0;
border: 1px solid rgba(250, 220, 68, 0.35);
}
.setting-item-arrow {
width: 18px;
height: 18px;
display: block;
flex-shrink: 0;
opacity: 0.55;
filter: brightness(0) invert(1);
}
.setting-group--security .setting-item {
height: 52px;
}
.setting-group--notify .setting-item {
height: 52px;
padding: 0 14px;
cursor: default;
}
.setting-group--notify .setting-item-label {
color: rgba(255, 255, 255, 0.90);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 20px;
}
.setting-switch {
width: 44px;
height: 24px;
margin-left: 0;
background: rgba(255, 255, 255, 0.12);
}
.setting-switch::after {
width: 20px;
height: 20px;
left: 2px;
top: 2px;
background: #fff;
}
.setting-switch--on {
background: #EC237F;
}
.setting-switch--on::after {
left: 22px;
}
.setting-logout {
width: 100%;
height: 48px;
margin-top: 0;
background: rgba(255, 255, 255, 0.05);
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.90);
font-size: 14px;
font-family: "Noto Sans SC", PingFang SC, sans-serif;
font-weight: 700;
line-height: 20px;
}
}
File diff suppressed because it is too large Load Diff
-241
View File
@@ -1,241 +0,0 @@
.vip-dialog-mask {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
padding: 24px;
}
.vip-dialog-mask.is-open {
display: flex;
}
.vip-dialog-card {
width: 456px;
max-width: 100%;
padding: 36px;
position: relative;
background: #0A0A0A;
box-shadow: 0px 20px 48px rgba(0, 0, 0, 0.46);
overflow: hidden;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.10);
flex-direction: column;
justify-content: center;
align-items: center;
gap: 18px;
display: inline-flex;
box-sizing: border-box;
}
.vip-dialog-close {
width: 28px;
height: 28px;
right: 20px;
top: 20px;
position: absolute;
overflow: hidden;
border: none;
padding: 0;
background: transparent;
cursor: pointer;
}
.vip-dialog-close-ring {
width: 26px;
height: 26px;
left: 1px;
top: 1px;
position: absolute;
background: #fff;
border: 1px solid #fff;
border-radius: 50%;
box-sizing: border-box;
}
.vip-dialog-close-x {
width: 8px;
height: 8px;
left: 10px;
top: 10px;
position: absolute;
box-sizing: border-box;
}
.vip-dialog-close-x::before,
.vip-dialog-close-x::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 10px;
height: 1.5px;
background: #808080;
border-radius: 1px;
}
.vip-dialog-close-x::before {
transform: translate(-50%, -50%) rotate(45deg);
}
.vip-dialog-close-x::after {
transform: translate(-50%, -50%) rotate(-45deg);
}
.vip-dialog-icon {
width: 52px;
height: 52px;
position: relative;
overflow: hidden;
border-radius: 12px;
flex-shrink: 0;
color: #EC237F;
}
.vip-dialog-icon-bg {
width: 52px;
height: 52px;
left: 0;
top: 0;
position: absolute;
opacity: 0.10;
background: #EC237F;
}
.vip-dialog-icon-crown {
width: 24px;
height: 21px;
left: 14px;
top: 16px;
position: absolute;
display: block;
}
.vip-dialog-title {
text-align: center;
color: white;
font-size: 24px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 700;
line-height: 30px;
word-wrap: break-word;
}
.vip-dialog-desc {
text-align: center;
color: #8A8A8A;
font-size: 14px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 400;
line-height: 20px;
word-wrap: break-word;
}
.vip-dialog-benefits {
text-align: center;
color: #F59E0B;
font-size: 12px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 700;
line-height: 18px;
word-wrap: break-word;
}
.vip-dialog-cta {
align-self: stretch;
width: 100%;
height: 48px;
background: #EC237F;
overflow: hidden;
border-radius: 24px;
justify-content: center;
align-items: center;
display: inline-flex;
color: #181818;
font-size: 16px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 700;
line-height: 22px;
word-wrap: break-word;
text-decoration: none;
box-sizing: border-box;
}
.vip-dialog-skip {
text-align: center;
color: #8A8A8A;
font-size: 12px;
font-family: "Noto Sans SC", "PingFang SC", sans-serif;
font-weight: 400;
line-height: 18px;
word-wrap: break-word;
border: none;
background: transparent;
padding: 0;
cursor: pointer;
}
@media (max-width: 767px) {
.vip-dialog-mask {
background: rgba(0, 0, 0, 0.70);
padding: 16px;
}
.vip-dialog-card {
width: 300px;
max-width: calc(100vw - 32px);
padding: 32px 24px;
background: rgba(23, 23, 23, 0.98);
box-shadow: 0px 16px 32px rgba(0, 0, 0, 0.45);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.10);
}
.vip-dialog-close {
right: 12px;
top: 12px;
background: rgba(255, 255, 255, 0.08);
border-radius: 14px;
}
.vip-dialog-close-ring {
display: none;
}
.vip-dialog-close-x::before,
.vip-dialog-close-x::after {
background: rgba(255, 255, 255, 0.60);
}
.vip-dialog-icon {
color: #fff;
border-radius: 0;
}
.vip-dialog-icon-bg {
opacity: 1;
background: linear-gradient(135deg, #F12B9E 0%, #6D4BFF 100%);
}
.vip-dialog-desc {
align-self: stretch;
color: rgba(255, 255, 255, 0.60);
}
.vip-dialog-benefits {
width: 100%;
max-width: 287px;
color: #EC237F;
}
.vip-dialog-cta {
color: #171717;
}
.vip-dialog-skip {
color: rgba(255, 255, 255, 0.60);
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 29 KiB

+1 -74
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

@@ -1,13 +0,0 @@
<svg width="20" height="23" viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.2676 0.87793C14.7429 -0.424404 17.0448 0.409539 17.3868 2.35254L19.3145 13.2998L19.3399 13.4785C19.5386 15.3219 17.7536 16.7952 15.9844 16.1416L15.6866 16.0322C14.9051 15.7438 14.0307 15.8824 13.3809 16.4072C13.0329 16.688 12.6759 17.0789 12.3321 17.6191C10.6263 20.2994 7.54206 21.5733 4.93073 22.1826C3.61958 22.4885 2.41365 22.6303 1.53619 22.6953C1.09742 22.7278 0.739667 22.7417 0.491272 22.7471C0.367292 22.7497 0.270439 22.75 0.204163 22.75C0.171046 22.75 0.145398 22.7502 0.127991 22.75C0.119286 22.7499 0.112085 22.7491 0.107483 22.749H0.100647L0.0547485 22.2627L0.0527954 22.2549H0.0547485C0.0561389 22.2546 0.0583894 22.2537 0.0615845 22.2529C0.0682333 22.2514 0.0786545 22.2486 0.0928345 22.2451C0.121207 22.2381 0.16429 22.2269 0.220764 22.2109C0.333963 22.1788 0.501483 22.1279 0.710999 22.0508C1.13007 21.8966 1.71918 21.6401 2.39264 21.2354C3.73903 20.4261 5.42329 19.0214 6.7569 16.6387C7.05382 16.1082 7.24255 15.6269 7.34967 15.1924C7.63805 14.0223 6.99486 12.8221 5.8819 12.4111C4.08173 11.746 3.65211 9.36597 5.09283 8.09375L13.2676 0.87793Z" fill="url(#paint0_linear_40217_177083)" stroke="url(#paint1_linear_40217_177083)" stroke-width="0.5"/>
<defs>
<linearGradient id="paint0_linear_40217_177083" x1="9.60553" y1="0.5" x2="9.60553" y2="22.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#0C0C0C"/>
<stop offset="1" stop-color="#767676"/>
</linearGradient>
<linearGradient id="paint1_linear_40217_177083" x1="9.60553" y1="0.5" x2="9.60553" y2="22.5" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,29 +0,0 @@
<svg width="343" height="81" viewBox="0 0 343 81" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_40108_172028)">
<rect width="343" height="81" fill="url(#paint0_linear_40108_172028)"/>
<g clip-path="url(#clip1_40108_172028)">
<path d="M212.547 15L176.179 45.9539H164L200.451 15H212.547Z" fill="#2D2D2D"/>
<path d="M176.179 45.9539H164V81.94H176.179V45.9539Z" fill="#202020"/>
<path d="M212.545 15V81.94H176.178V45.9539L212.545 15Z" fill="#1B1B1B"/>
<path d="M224.722 26.614L188.357 57.5699H176.178L212.629 26.614H224.722Z" fill="#2D2D2D"/>
<path d="M188.357 57.5699H176.178V93.556H188.357V57.5699Z" fill="#202020"/>
<path d="M224.723 26.614V93.556H188.357V57.5699L224.723 26.614Z" fill="#1B1B1B"/>
<path d="M236.902 39.7516L200.535 70.7055H188.357L224.809 39.7516H236.902Z" fill="#2D2D2D"/>
<path d="M200.537 70.7055H188.357V106.692H200.537V70.7055Z" fill="#202020"/>
<path d="M236.903 39.7516V106.694H200.535V70.7055L236.903 39.7516Z" fill="#1B1B1B"/>
</g>
</g>
<path d="M18 0.5H325C334.665 0.5 342.5 8.33502 342.5 18V63C342.5 72.665 334.665 80.5 325 80.5H18C8.33502 80.5 0.5 72.665 0.5 63V18C0.5 8.33502 8.33502 0.5 18 0.5Z" stroke="white" stroke-opacity="0.1"/>
<defs>
<linearGradient id="paint0_linear_40108_172028" x1="8.41068e-08" y1="40.5" x2="342.002" y2="-7.16334" gradientUnits="userSpaceOnUse">
<stop stop-color="#020202"/>
<stop offset="1" stop-color="#2F2F2F"/>
</linearGradient>
<clipPath id="clip0_40108_172028">
<path d="M0 18C0 8.05887 8.05888 0 18 0H325C334.941 0 343 8.05888 343 18V63C343 72.9411 334.941 81 325 81H18C8.05888 81 0 72.9411 0 63V18Z" fill="white"/>
</clipPath>
<clipPath id="clip1_40108_172028">
<rect width="72.9028" height="91.6936" fill="white" transform="translate(164 15)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

@@ -1,5 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.3685 18.931L20.8386 17.6856C20.3372 17.5087 20 17.019 20 16.4681V12.5319C20 11.981 20.3372 11.4913 20.8386 11.3144L24.3685 10.069C25.1692 9.78652 26 10.4062 26 11.2865V17.7135C26 18.5938 25.1692 19.2135 24.3685 18.931Z" fill="white" fill-opacity="0.3"/>
<path d="M6.67446 7H20.3255C22.1085 7 23 7.87641 23 9.62922V19.3708C23 21.1236 22.1085 22 20.3255 22H6.67446C4.89149 22 4 21.1236 4 19.3708V9.62922C4 7.87641 4.89149 7 6.67446 7Z" fill="#EC237F"/>
<path d="M16.5216 13.2508L12.4352 11.1172C11.7974 10.7842 11 11.2004 11 11.8665V16.1334C11 16.7995 11.7974 17.216 12.4352 16.8827L16.5216 14.749C17.1595 14.416 17.1595 13.5833 16.5216 13.2503V13.2508Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 784 B

@@ -1,8 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25 23.5C25 24.5 24.422 25 23.2659 25H14.7341C13.578 25 13 24.5 13 23.5C13 22.5 13.578 22 14.7341 22H23.2659C24.422 22 25 22.5 25 23.5Z" fill="white" fill-opacity="0.3"/>
<path d="M5 15.5C5 18.2848 6.10625 20.9555 8.07538 22.9246C10.0445 24.8938 12.7152 26 15.5 26C18.2848 26 20.9555 24.8938 22.9246 22.9246C24.8938 20.9555 26 18.2848 26 15.5C26 12.7152 24.8938 10.0445 22.9246 8.07538C20.9555 6.10625 18.2848 5 15.5 5C12.7152 5 10.0445 6.10625 8.07538 8.07538C6.10625 10.0445 5 12.7152 5 15.5Z" fill="#EC237F"/>
<path d="M13 11C13 11.5304 13.2107 12.0391 13.5858 12.4142C13.9609 12.7893 14.4696 13 15 13C15.5304 13 16.0391 12.7893 16.4142 12.4142C16.7893 12.0391 17 11.5304 17 11C17 10.4696 16.7893 9.96086 16.4142 9.58579C16.0391 9.21071 15.5304 9 15 9C14.4696 9 13.9609 9.21071 13.5858 9.58579C13.2107 9.96086 13 10.4696 13 11Z" fill="black"/>
<path d="M13 19C13 19.5304 13.2107 20.0391 13.5858 20.4142C13.9609 20.7893 14.4696 21 15 21C15.5304 21 16.0391 20.7893 16.4142 20.4142C16.7893 20.0391 17 19.5304 17 19C17 18.4696 16.7893 17.9609 16.4142 17.5858C16.0391 17.2107 15.5304 17 15 17C14.4696 17 13.9609 17.2107 13.5858 17.5858C13.2107 17.9609 13 18.4696 13 19Z" fill="black"/>
<path d="M17 15C17 15.5304 17.2107 16.0391 17.5858 16.4142C17.9609 16.7893 18.4696 17 19 17C19.5304 17 20.0391 16.7893 20.4142 16.4142C20.7893 16.0391 21 15.5304 21 15C21 14.4696 20.7893 13.9609 20.4142 13.5858C20.0391 13.2107 19.5304 13 19 13C18.4696 13 17.9609 13.2107 17.5858 13.5858C17.2107 13.9609 17 14.4696 17 15Z" fill="black"/>
<path d="M9 15C9 15.5304 9.21071 16.0391 9.58579 16.4142C9.96086 16.7893 10.4696 17 11 17C11.5304 17 12.0391 16.7893 12.4142 16.4142C12.7893 16.0391 13 15.5304 13 15C13 14.4696 12.7893 13.9609 12.4142 13.5858C12.0391 13.2107 11.5304 13 11 13C10.4696 13 9.96086 13.2107 9.58579 13.5858C9.21071 13.9609 9 14.4696 9 15Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,5 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="2" width="12" height="4" rx="2" fill="white" fill-opacity="0.3"/>
<path d="M13 4.1547C14.2376 3.44017 15.7624 3.44017 17 4.1547L23.3923 7.8453C24.6299 8.55983 25.3923 9.88034 25.3923 11.3094V18.6906C25.3923 20.1197 24.6299 21.4402 23.3923 22.1547L17 25.8453C15.7624 26.5598 14.2376 26.5598 13 25.8453L6.6077 22.1547C5.37009 21.4402 4.6077 20.1197 4.6077 18.6906V11.3094C4.6077 9.88034 5.37009 8.55983 6.6077 7.8453L13 4.1547Z" fill="#EC237F"/>
<circle cx="15" cy="15" r="4" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 607 B

@@ -1,6 +0,0 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.10521 8.22075L20.7665 5.10946C22.4904 4.71685 23.5491 5.38041 23.9426 7.10014L25.8903 15.6107C26.2838 17.3305 25.6187 18.3866 23.8948 18.7792L10.2335 21.8905C8.50963 22.2831 7.45091 21.6196 7.05735 19.8999L5.10972 11.3893C4.71617 9.66953 5.38133 8.61336 7.10521 8.22075Z" fill="white" fill-opacity="0.3"/>
<path d="M7.67822 9H23.3218C25.1073 9 26 9.87641 26 11.6292V21.3708C26 23.1236 25.1073 24 23.3218 24H7.67822C5.89274 24 5 23.1236 5 21.3708V11.6292C5 9.87641 5.89274 9 7.67822 9Z" fill="#EC237F"/>
<path d="M19.292 14H23.2731C23.6742 14 24 14.2843 24 14.6343V17.3657C24 17.7157 23.6742 18 23.2731 18H19.292C18.0271 18 17 17.1038 17 16C17 14.8962 18.0271 14 19.292 14Z" fill="black"/>
<path d="M18 16C18 16.2652 18.1054 16.5196 18.2929 16.7071C18.4804 16.8946 18.7348 17 19 17C19.2652 17 19.5196 16.8946 19.7071 16.7071C19.8946 16.5196 20 16.2652 20 16C20 15.7348 19.8946 15.4804 19.7071 15.2929C19.5196 15.1054 19.2652 15 19 15C18.7348 15 18.4804 15.1054 18.2929 15.2929C18.1054 15.4804 18 15.7348 18 16Z" fill="#EC237F"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,35 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="12" fill="url(#paint0_linear_40108_173685)"/>
<circle cx="12" cy="12" r="8.75" fill="url(#paint1_linear_40108_173685)" stroke="url(#paint2_linear_40108_173685)" stroke-width="0.5"/>
<g filter="url(#filter0_d_40108_173685)">
<path d="M12.5123 17.1767C12.1999 17.3857 11.7992 17.3857 11.4871 17.1761C10.631 16.6012 10.0239 16.2525 8.89332 15.3057C3.98568 11.525 8.17438 5.73567 11.9992 8.92344C15.8142 5.738 20.0182 11.516 15.105 15.3057C13.9814 16.2476 13.3648 16.6059 12.5123 17.1767Z" fill="url(#paint3_linear_40108_173685)"/>
</g>
<defs>
<filter id="filter0_d_40108_173685" x="4.66602" y="6" width="14.666" height="13.3333" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="1"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_40108_173685"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_40108_173685" result="shape"/>
</filter>
<linearGradient id="paint0_linear_40108_173685" x1="12" y1="0" x2="12" y2="24" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDE0B9"/>
<stop offset="1" stop-color="#FEC691"/>
</linearGradient>
<linearGradient id="paint1_linear_40108_173685" x1="3" y1="3" x2="21" y2="21" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEC997"/>
<stop offset="1" stop-color="#FFAB75"/>
</linearGradient>
<linearGradient id="paint2_linear_40108_173685" x1="3" y1="3" x2="21" y2="21" gradientUnits="userSpaceOnUse">
<stop stop-color="#D4A06D" stop-opacity="0.3"/>
<stop offset="1" stop-color="#D4A06D" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint3_linear_40108_173685" x1="6.99927" y1="8" x2="16.9993" y2="17" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEE9CF"/>
<stop offset="1" stop-color="#FECD9C"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

@@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.0215 18.7807C20.0215 19.7019 19.5609 20.1626 18.6396 20.1626H11.8409C10.9196 20.1626 10.459 19.7019 10.459 18.7807C10.459 17.8594 10.9196 17.3988 11.8409 17.3988H18.6396C19.5609 17.3988 20.0215 17.8594 20.0215 18.7807Z" fill="#FFD495" fill-opacity="0.5"/>
<path d="M3.80273 12.0101C3.80273 14.1857 4.66699 16.2722 6.20537 17.8106C7.74376 19.3489 9.83026 20.2132 12.0059 20.2132C14.1815 20.2132 16.268 19.3489 17.8063 17.8106C19.3447 16.2722 20.209 14.1857 20.209 12.0101C20.209 9.83447 19.3447 7.74797 17.8063 6.20959C16.268 4.6712 14.1815 3.80695 12.0059 3.80695C9.83026 3.80695 7.74376 4.6712 6.20537 6.20959C4.66699 7.74797 3.80273 9.83447 3.80273 12.0101Z" fill="#FFD495"/>
<path d="M10.4121 8.92804C10.4121 9.35384 10.5813 9.76219 10.8823 10.0633C11.1834 10.3644 11.5918 10.5335 12.0176 10.5335C12.4434 10.5335 12.8517 10.3644 13.1528 10.0633C13.4539 9.76219 13.623 9.35384 13.623 8.92804C13.623 8.50224 13.4539 8.09389 13.1528 7.7928C12.8517 7.49172 12.4434 7.32257 12.0176 7.32257C11.5918 7.32257 11.1834 7.49172 10.8823 7.7928C10.5813 8.09389 10.4121 8.50224 10.4121 8.92804Z" fill="#BD8B42"/>
<path d="M10.4121 15.0687C10.4121 15.4945 10.5813 15.9028 10.8823 16.2039C11.1834 16.505 11.5918 16.6741 12.0176 16.6741C12.4434 16.6741 12.8517 16.505 13.1528 16.2039C13.4539 15.9028 13.623 15.4945 13.623 15.0687C13.623 14.6429 13.4539 14.2345 13.1528 13.9334C12.8517 13.6323 12.4434 13.4632 12.0176 13.4632C11.5918 13.4632 11.1834 13.6323 10.8823 13.9334C10.5813 14.2345 10.4121 14.6429 10.4121 15.0687Z" fill="#BD8B42"/>
<path d="M13.4824 11.9984C13.4824 12.4241 13.6516 12.8325 13.9527 13.1336C14.2537 13.4347 14.6621 13.6038 15.0879 13.6038C15.5137 13.6038 15.922 13.4347 16.2231 13.1336C16.5242 12.8325 16.6934 12.4241 16.6934 11.9984C16.6934 11.5726 16.5242 11.1642 16.2231 10.8631C15.922 10.562 15.5137 10.3929 15.0879 10.3929C14.6621 10.3929 14.2537 10.562 13.9527 10.8631C13.6516 11.1642 13.4824 11.5726 13.4824 11.9984Z" fill="#BD8B42"/>
<path d="M7.3418 11.9984C7.3418 12.4241 7.51094 12.8325 7.81203 13.1336C8.11311 13.4347 8.52147 13.6038 8.94727 13.6038C9.37306 13.6038 9.78142 13.4347 10.0825 13.1336C10.3836 12.8325 10.5527 12.4241 10.5527 11.9984C10.5527 11.5726 10.3836 11.1642 10.0825 10.8631C9.78142 10.562 9.37306 10.3929 8.94727 10.3929C8.52147 10.3929 8.11311 10.562 7.81203 10.8631C7.51094 11.1642 7.3418 11.5726 7.3418 11.9984Z" fill="#BD8B42"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6018 9.06354H7.57816L7.21676 5.6119C7.13777 4.85674 7.87652 4.27924 8.5902 4.53846L11.3089 5.52565C11.8135 5.70893 12.3665 5.70893 12.8711 5.52565L15.5898 4.53846C16.3035 4.27924 17.0425 4.85674 16.9632 5.6119L16.6018 9.06354Z" fill="#FFD495" fill-opacity="0.5"/>
<path d="M5.60539 7.17468H18.5743C19.5873 7.17468 20.0938 7.68117 20.0938 8.69414C20.0938 9.7071 19.5873 10.2136 18.5743 10.2136H5.60539C4.59242 10.2136 4.08594 9.7071 4.08594 8.69414C4.08594 7.68117 4.59242 7.17468 5.60539 7.17468Z" fill="#FFD495"/>
<path d="M5.97313 9.1369H18.2066C18.6141 9.1369 18.9453 9.46784 18.9453 9.87565V17.6142C18.9453 18.4608 18.2581 19.148 17.4116 19.148H6.76812C5.92156 19.148 5.23438 18.4608 5.23438 17.6142V9.87565C5.23438 9.46807 5.56531 9.1369 5.97313 9.1369Z" fill="#FFD495"/>
<path d="M11.5074 11.2266H12.6723C13.159 11.2266 13.4023 11.4699 13.4023 11.9566V16.9648C13.4023 17.4515 13.159 17.6948 12.6723 17.6948H11.5074C11.0207 17.6948 10.7773 17.4515 10.7773 16.9648V11.9566C10.7773 11.4699 11.0207 11.2266 11.5074 11.2266Z" fill="#BD8B42"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.7061 20.6182L12.7516 19.2178C12.5524 19.1235 12.3348 19.0746 12.1144 19.0746C11.8941 19.0746 11.6764 19.1235 11.4773 19.2178L8.52275 20.6182C7.96424 20.883 7.3265 20.4511 7.36494 19.8342L7.65978 15.1003C7.70361 14.3984 8.28556 13.8516 8.98869 13.8516H15.2402C15.9435 13.8516 16.5255 14.3984 16.5691 15.1003L16.8639 19.8342C16.9024 20.4511 16.2646 20.883 15.7061 20.6182Z" fill="#FFD495" fill-opacity="0.5"/>
<path d="M5.01953 10.3685C5.01953 11.3001 5.20302 12.2226 5.55953 13.0833C5.91604 13.944 6.43859 14.7261 7.09733 15.3848C7.75608 16.0435 8.53812 16.5661 9.39881 16.9226C10.2595 17.2791 11.182 17.4626 12.1136 17.4626C13.0452 17.4626 13.9677 17.2791 14.8284 16.9226C15.6891 16.5661 16.4711 16.0435 17.1299 15.3848C17.7886 14.7261 18.3111 13.944 18.6677 13.0833C19.0242 12.2226 19.2077 11.3001 19.2077 10.3685C19.2077 8.48707 18.4602 6.68267 17.1299 5.35228C15.7995 4.02188 13.9951 3.27448 12.1136 3.27448C10.2321 3.27448 8.42773 4.02188 7.09733 5.35228C5.76694 6.68267 5.01953 8.48707 5.01953 10.3685Z" fill="#FFD495"/>
<path d="M12.5124 7.63316L13.0995 8.82261C13.1644 8.95386 13.2896 9.04503 13.4344 9.06613L14.7471 9.25691C15.1121 9.30988 15.2579 9.75847 14.9937 10.0158L14.0438 10.9418C13.9921 10.9922 13.9535 11.0543 13.9312 11.1229C13.9089 11.1915 13.9036 11.2645 13.9158 11.3356L14.1401 12.6429C14.2025 13.0064 13.8209 13.2835 13.4944 13.1119L12.3202 12.4946C12.2563 12.461 12.1853 12.4434 12.1131 12.4434C12.041 12.4434 11.9699 12.461 11.9061 12.4946L10.7318 13.1119C10.4054 13.2835 10.0238 13.0064 10.0861 12.6429L10.3104 11.3356C10.3353 11.1912 10.2872 11.044 10.1825 10.9418L9.23254 10.0158C8.9684 9.75847 9.11418 9.30988 9.4791 9.25691L10.7918 9.06613C10.9367 9.04503 11.0621 8.9541 11.1268 8.82261L11.7139 7.63316C11.877 7.30246 12.3486 7.30246 12.5119 7.63316H12.5124Z" fill="#BD8B42"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,3 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.6004 16.5992C19.3828 13.45 20.0303 10.4566 20.0303 10.4566H14.0355V8.25163H21.1652V7.30738H14.0355V4H10.7957V7.30738H4.31341V8.25163H10.7957V10.4566H5.28587V11.4008H16.4679C16.4679 11.559 16.4679 11.559 16.3054 11.7147C16.3054 12.8172 15.4955 14.3918 14.848 15.6524C6.58334 12.5033 4.15345 14.3918 3.50345 14.7082C-2.00634 18.4877 3.17845 23.2115 3.98841 23.0557C9.82319 24.3164 13.548 21.9533 16.1429 18.9598C16.3054 19.118 16.4679 19.118 16.6279 19.118C18.4103 20.0623 27 24 27 24V19.2762C25.7025 19.2762 21.0053 17.7017 17.6004 16.5992ZM13.388 18.0156C9.33569 23.0557 4.47591 21.4812 3.66595 21.1647C1.72102 20.6926 1.07356 17.2295 3.50345 16.1271C7.5558 14.8664 11.1207 16.2853 13.713 17.5434C13.5505 17.8574 13.388 18.0156 13.388 18.0156Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 876 B

@@ -1,35 +0,0 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="30" cy="30" r="30" fill="url(#paint0_linear_40217_177127)"/>
<circle cx="30" cy="30" r="21.875" fill="url(#paint1_linear_40217_177127)" stroke="url(#paint2_linear_40217_177127)" stroke-width="1.25"/>
<g filter="url(#filter0_d_40217_177127)">
<path d="M31.2819 42.9456C30.5011 43.468 29.4992 43.468 28.7191 42.9441C26.5787 41.507 25.0609 40.6352 22.2345 38.2681C9.96542 28.8164 20.4372 14.3431 29.9991 22.3125C39.5366 14.3489 50.0467 28.7939 37.7637 38.2681C34.9547 40.6229 33.4132 41.5186 31.2819 42.9456Z" fill="url(#paint3_linear_40217_177127)"/>
</g>
<defs>
<filter id="filter0_d_40217_177127" x="11.6663" y="15.0039" width="36.6667" height="33.334" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="2.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_40217_177127"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_40217_177127" result="shape"/>
</filter>
<linearGradient id="paint0_linear_40217_177127" x1="30" y1="0" x2="30" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#FDE0B9"/>
<stop offset="1" stop-color="#FEC691"/>
</linearGradient>
<linearGradient id="paint1_linear_40217_177127" x1="7.5" y1="7.5" x2="52.5" y2="52.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEC997"/>
<stop offset="1" stop-color="#FFAB75"/>
</linearGradient>
<linearGradient id="paint2_linear_40217_177127" x1="7.5" y1="7.5" x2="52.5" y2="52.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#D4A06D" stop-opacity="0.3"/>
<stop offset="1" stop-color="#D4A06D" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint3_linear_40217_177127" x1="17.4994" y1="20.0039" x2="42.4994" y2="42.5039" gradientUnits="userSpaceOnUse">
<stop stop-color="#FEE9CF"/>
<stop offset="1" stop-color="#FECD9C"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 3.33203C15.8158 3.33203 18.75 7.4987 18.75 9.9987C18.75 12.4987 15.8158 16.6654 10 16.6654C4.18417 16.6654 1.25 12.4987 1.25 9.9987C1.25 7.4987 4.18417 3.33203 10 3.33203ZM10 4.9987C4.99583 4.9987 2.91667 8.1237 2.91667 9.9987C2.91667 11.8737 4.99583 14.9987 10 14.9987C15.0042 14.9987 17.0833 11.8737 17.0833 9.9987C17.0833 8.1237 15.0042 4.9987 10 4.9987ZM9.93583 7.08453C9.97107 7.08601 10.0052 7.0973 10.0344 7.11715C10.0635 7.13699 10.0865 7.16459 10.1008 7.19683C10.1151 7.22908 10.1201 7.26467 10.1153 7.29961C10.1104 7.33454 10.0959 7.36742 10.0733 7.39453L10.005 7.4812C9.89 7.63536 9.77167 7.83953 9.69167 8.09786C9.47667 8.78453 9.71333 9.4787 10.0792 9.84786C10.4267 10.1979 10.8933 10.5229 11.6667 10.4895C12.0408 10.4579 12.3567 10.292 12.5775 10.137L12.6167 10.1162C12.648 10.1033 12.6822 10.0984 12.7159 10.1022C12.7496 10.106 12.7818 10.1182 12.8096 10.1377C12.8374 10.1572 12.8597 10.1834 12.8747 10.2139C12.8896 10.2444 12.8966 10.2781 12.895 10.312L12.8933 10.3529C12.8926 10.3673 12.8915 10.3818 12.89 10.3962C12.8143 10.9465 12.583 11.4637 12.2233 11.887C11.8637 12.3103 11.3906 12.6221 10.8598 12.7857C10.329 12.9494 9.76247 12.958 9.2269 12.8107C8.69133 12.6634 8.20898 12.3661 7.83656 11.954C7.46414 11.5419 7.21715 11.032 7.12464 10.4843C7.03213 9.93657 7.09794 9.37384 7.31434 8.86226C7.53073 8.35068 7.88871 7.91152 8.34617 7.59645C8.80363 7.28138 9.34155 7.10349 9.89667 7.0837L9.93583 7.08453Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.8726 8.38766V4.55078H6.16309V8.38766H11.9547V10.8944C7.24678 11.1061 3.70166 12.0342 3.70166 13.1476C3.70166 14.2611 7.24675 15.1891 11.9547 15.4009V23.4457H16.081V15.399C20.7708 15.1841 24.2979 14.2577 24.2979 13.1476C24.2979 12.0374 20.7708 11.1107 16.081 10.8961V8.38763H21.8726V8.38766ZM22.5242 12.9209C22.5242 13.6533 19.783 14.2675 16.081 14.4381L15.8418 14.4482H15.8154L15.591 14.4565H15.5667C15.3999 14.4622 15.231 14.4671 15.06 14.471H15.0494L14.82 14.4757H14.7552L14.5742 14.4781H13.4278L13.2487 14.4757H13.1864L12.9724 14.4712H12.9489C12.6952 14.4656 12.4454 14.4577 12.1993 14.4482H12.196L11.951 14.4376C8.23055 14.2697 5.47165 13.6533 5.47165 12.9174C5.47165 12.1815 8.23102 11.5669 11.951 11.3971V13.7242C12.5967 13.7579 13.2837 13.7763 13.9962 13.7763C14.72 13.7763 15.4208 13.7576 16.0775 13.7224V11.404C19.7827 11.5741 22.5242 12.1887 22.5242 12.9209Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1001 B

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_40086_453711)">
<path d="M10.3758 16.4796C10.2358 16.4811 10.0979 16.4391 9.97552 16.3577C9.85312 16.2763 9.75038 16.1583 9.6773 16.0152L9.62427 15.8844L5.43854 9.252C5.41334 9.19285 5.39965 9.12761 5.39851 9.0612C5.40166 8.93749 5.44553 8.8203 5.52055 8.73529C5.59556 8.65028 5.6956 8.60437 5.79878 8.6076C5.88883 8.6076 5.97189 8.6412 6.03893 8.6976L10.6199 11.9772C10.7781 12.093 10.9556 12.1656 11.1398 12.1896C11.324 12.2137 11.5104 12.1887 11.6856 12.1164L21.8162 4.8C19.6408 1.896 16.0605 0 12.0078 0C5.3755 0 0 5.076 0 11.3352C0 14.7504 1.61705 17.8248 4.1487 19.902C4.25086 19.9826 4.33495 20.092 4.39357 20.2207C4.4522 20.3494 4.48357 20.4934 4.48492 20.64C4.48245 20.7387 4.4683 20.8365 4.44289 20.9304L3.90154 23.2152C3.87552 23.3232 3.8365 23.4348 3.8365 23.5464C3.8365 23.7984 4.01661 24 4.23676 24C4.32281 24 4.39386 23.964 4.46691 23.916L7.09662 22.1976C7.29475 22.068 7.50288 21.9876 7.73404 21.9876C7.85712 21.9876 7.97419 22.0092 8.08727 22.0464C9.31306 22.446 10.6369 22.6692 12.0068 22.6692C18.6391 22.668 24.0156 17.592 24.0156 11.3352C24.0156 9.438 23.5183 7.6524 22.6467 6.0828L10.823 16.3248L10.736 16.3812C10.6237 16.4463 10.5005 16.4799 10.3758 16.4796Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_40086_453711">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Some files were not shown because too many files have changed in this diff Show More