From d8f4a132af4c0af0f5246488773ce22278e7701a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E5=AE=87=E5=AE=81?= Date: Thu, 29 Oct 2026 20:45:09 +0700 Subject: [PATCH] =?UTF-8?q?=E5=B9=BF=E5=91=8A=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/web/ads_click.go | 74 ++++++++++++ app/router/gin.go | 3 + app/service/configser/ads_click.go | 34 ++++++ app/service/configser/types.go | 5 + app/service/configser/view.go | 89 ++++++++------ app/service/configser/view_test.go | 55 +++++---- app/static/css/detail.css | 9 +- app/static/css/homeAdRow.css | 100 ---------------- app/static/css/nineGridDialog.css | 104 ++++++++++++++++ app/static/css/play.css | 1 - app/static/js/common/adClick.js | 53 ++++++++ app/static/js/pages/dialogAd.js | 16 ++- app/static/js/pages/eightPop.js | 9 +- app/static/js/pages/nineGridDialog.js | 160 +++++++++++++++++++++++++ app/templates/base/contentExtras.html | 6 +- app/templates/base/dialogAd.html | 6 +- app/templates/base/eightPop.html | 4 +- app/templates/base/header.html | 3 +- app/templates/base/homeAdRow.html | 30 ----- app/templates/base/nineGridDialog.html | 29 +++++ app/templates/base/squarePromo.html | 1 + app/templates/page/detail.html | 2 +- app/templates/page/index.html | 4 +- app/templates/page/list.html | 10 -- app/templates/page/nav.html | 2 + app/templates/page/play.html | 4 +- app/templates/page/rank.html | 10 -- app/templates/page/series.html | 10 -- app/templates/page/themeIndex.html | 10 -- 29 files changed, 588 insertions(+), 255 deletions(-) create mode 100644 app/api/web/ads_click.go create mode 100644 app/service/configser/ads_click.go delete mode 100644 app/static/css/homeAdRow.css create mode 100644 app/static/css/nineGridDialog.css create mode 100644 app/static/js/common/adClick.js create mode 100644 app/static/js/pages/nineGridDialog.js delete mode 100644 app/templates/base/homeAdRow.html create mode 100644 app/templates/base/nineGridDialog.html diff --git a/app/api/web/ads_click.go b/app/api/web/ads_click.go new file mode 100644 index 0000000..08e9636 --- /dev/null +++ b/app/api/web/ads_click.go @@ -0,0 +1,74 @@ +package web + +import ( + "net/http" + "videoweb/app/service/configser" + "videoweb/pub/common/log" + + "github.com/gin-gonic/gin" +) + +type adsClickBody struct { + AdsID int64 `json:"adsId"` + Location int64 `json:"location"` +} + +// AdsClick 代理上游 POST /api/app/ads/click;appId 由服务端固定填写。 +func AdsClick(c *gin.Context) { + var body adsClickBody + if err := c.ShouldBindJSON(&body); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "code": 1001, + "msg": "参数错误", + "tip": "参数错误", + "data": nil, + }) + return + } + if body.AdsID <= 0 || body.Location <= 0 { + c.JSON(http.StatusOK, gin.H{ + "code": 1001, + "msg": "参数错误", + "tip": "缺少 adsId 或 location", + "data": nil, + }) + return + } + + env, err := configser.AdsClick(c, configser.AdsClickReq{ + AdsID: body.AdsID, + Location: body.Location, + }) + if err != nil { + log.Error("ads click upstream failed", log.E(err)) + c.JSON(http.StatusOK, gin.H{ + "code": 500, + "msg": "ads click failed", + "tip": "上报失败", + "data": nil, + }) + return + } + if env == nil || env.Code != 200 { + code := 500 + msg := "" + if env != nil { + code = env.Code + msg = env.Msg + } + c.JSON(http.StatusOK, gin.H{ + "code": code, + "msg": msg, + "tip": firstNonEmpty(msg, "上报失败"), + "data": nil, + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 200, + "msg": env.Msg, + "tip": firstNonEmpty(env.Msg, "success"), + "data": nil, + }) +} diff --git a/app/router/gin.go b/app/router/gin.go index 942aa6b..12f5409 100644 --- a/app/router/gin.go +++ b/app/router/gin.go @@ -38,6 +38,8 @@ func DHInit() *gin.Engine { engine.StaticFile("/favicon.svg", "./static/favicon.svg") engine.StaticFile("/favicon.png", "./static/favicon.png") engine.GET("/ui/dialog/:name", web.DialogFragment) + // 导航页同样展示弹窗 / 九宫格广告,需要点击上报接口。 + engine.POST("/api/app/ads/click", web.AdsClick) engine.GET("/", web.Nav) engine.GET("/nav/", web.Nav) engine.GET("/dh/", web.Nav) @@ -151,6 +153,7 @@ func registerRoutes(r *gin.Engine) { r.POST("/api/contactus/add", web.AddContactUs) r.GET("/api/comment/list", web.CommentList) r.POST("/api/playlet/forward/heat", web.PlayletForwardHeat) + r.POST("/api/app/ads/click", web.AdsClick) r.GET("/api/playlet/chapter/detail", web.ChapterDetail) r.GET("/api/playlet/chapter/list", web.ChapterList) r.GET("/api/playlet/operate/list", web.OperateList) diff --git a/app/service/configser/ads_click.go b/app/service/configser/ads_click.go new file mode 100644 index 0000000..187496b --- /dev/null +++ b/app/service/configser/ads_click.go @@ -0,0 +1,34 @@ +package configser + +import ( + "fmt" + "videoweb/app/service/upstream" + + "github.com/gin-gonic/gin" +) + +const ( + adsClickPath = "/api/app/ads/click" + // AdsClickAppID 广告点击上报的应用 ID,由广告后台分配,固定为 12。 + AdsClickAppID int64 = 12 +) + +// AdsClickReq 对应上游 POST /api/app/ads/click(appId / adsId / location 均必填)。 +type AdsClickReq struct { + AppID int64 `json:"appId"` + AdsID int64 `json:"adsId"` + Location int64 `json:"location"` +} + +// AdsClick 上报一次广告点击。 +func AdsClick(c *gin.Context, req AdsClickReq) (*APIEnvelope[any], error) { + if req.AdsID <= 0 || req.Location <= 0 { + return nil, fmt.Errorf("ads click invalid adsId=%d location=%d", req.AdsID, req.Location) + } + req.AppID = AdsClickAppID + var env APIEnvelope[any] + if _, err := upstream.PostJSON(c, adsClickPath, req, &env); err != nil { + return nil, err + } + return &env, nil +} diff --git a/app/service/configser/types.go b/app/service/configser/types.go index 8796101..c97e7c4 100644 --- a/app/service/configser/types.go +++ b/app/service/configser/types.go @@ -87,6 +87,11 @@ type AdvitiseData struct { TitleList []Advertisement `json:"titleList"` PlayList []Advertisement `json:"playList"` PauseyList []Advertisement `json:"pauseyList"` + // 本站后台(webhgdjapi)的字段名:横幅 / 播放页小图标 / 首页小图标 + VideoTopList []Advertisement `json:"videoTopList"` + VideoDownList []Advertisement `json:"videoDownList"` + AppInfoAList []Advertisement `json:"appInfoAList"` + AppInfoBList []Advertisement `json:"appInfoBList"` } // Advertisement 对应 swagger proxy.AdvitiseInfo。 diff --git a/app/service/configser/view.go b/app/service/configser/view.go index 7d75eeb..0da4551 100644 --- a/app/service/configser/view.go +++ b/app/service/configser/view.go @@ -7,29 +7,31 @@ import ( // AppConfigVM 注入全站页面的公共配置(字段名与接口 newAddressInfo / seoInfo 对齐)。 type AppConfigVM struct { - ForeverURLAddr string `json:"foreverUrlAddr"` - NewURLAddr string `json:"newUrlAddr"` - NewURLAddr1 string `json:"newUrlAddr1"` - NewURLAddr2 string `json:"newUrlAddr2"` - GmailAddr string `json:"gmailAddr"` - QQ string `json:"qq"` - TGGroup string `json:"tgGroup"` - Telegram string `json:"telegram"` - PotaoGroup string `json:"potaoGroup"` - CustomerServiceHost string `json:"customerServiceHost"` - AppAddr string `json:"appAddr"` - OfficialAppAddr string `json:"officialAppAddr"` - PDFAddr string `json:"pdfAddr"` - TwitterAddr string `json:"twitterAddr"` - SeoInfo SeoInfo `json:"seoInfo,omitempty"` - SeoNavInfo SeoNavInfo `json:"seoNavInfo,omitempty"` - NavPageList []NavPageItem `json:"navPageList,omitempty"` - Advitise AdvitiseData `json:"advitise"` - DetailTopList []Advertisement `json:"detailTopList,omitempty"` - NineGridList []Advertisement `json:"nineGridList,omitempty"` - DetailBannerList []Advertisement `json:"detailbannerList,omitempty"` - BottomBanner []Advertisement `json:"bottomBanner,omitempty"` - DialogList []Advertisement `json:"dialogList,omitempty"` + ForeverURLAddr string `json:"foreverUrlAddr"` + NewURLAddr string `json:"newUrlAddr"` + NewURLAddr1 string `json:"newUrlAddr1"` + NewURLAddr2 string `json:"newUrlAddr2"` + GmailAddr string `json:"gmailAddr"` + QQ string `json:"qq"` + TGGroup string `json:"tgGroup"` + Telegram string `json:"telegram"` + PotaoGroup string `json:"potaoGroup"` + CustomerServiceHost string `json:"customerServiceHost"` + AppAddr string `json:"appAddr"` + OfficialAppAddr string `json:"officialAppAddr"` + PDFAddr string `json:"pdfAddr"` + TwitterAddr string `json:"twitterAddr"` + SeoInfo SeoInfo `json:"seoInfo,omitempty"` + SeoNavInfo SeoNavInfo `json:"seoNavInfo,omitempty"` + NavPageList []NavPageItem `json:"navPageList,omitempty"` + Advitise AdvitiseData `json:"advitise"` + // 网页只用后台「站点」的 5 个广告位: + // 弹窗 / 9宫格(弹窗广告后弹出)/ 详情横幅 / 播放页小图标 / 首页小图标 + DialogList []Advertisement `json:"dialogList,omitempty"` + NineGridList []Advertisement `json:"nineGridList,omitempty"` + DetailBannerList []Advertisement `json:"detailbannerList,omitempty"` + AList []Advertisement `json:"AList,omitempty"` + BList []Advertisement `json:"BList,omitempty"` } // BuildAppConfigVM 从上游配置组装前端可用字段。 @@ -55,11 +57,12 @@ func BuildAppConfigVM(info *ConfigInfoResp) AppConfigVM { TwitterAddr: sanitizeURL(addr.TwitterAddr), NavPageList: buildNavPageList(info.NavPageList), Advitise: info.Advitise, - DetailTopList: buildAdvertisementList(info.Advitise.DetailTopList), - NineGridList: buildAdvertisementList(info.Advitise.NineGridList), - DetailBannerList: buildAdvertisementList(info.Advitise.DetailBannerList), - BottomBanner: buildAdvertisementList(info.Advitise.BottomBanner), DialogList: buildAdvertisementList(info.Advitise.DialogList), + NineGridList: buildAdvertisementList(info.Advitise.NineGridList), + // 本站后台用 videoTopList / appInfoAList / appInfoBList;兼容 detailbannerList / AList / BList 命名 + DetailBannerList: firstAdvertisementList(info.Advitise.VideoTopList, info.Advitise.DetailBannerList), + AList: firstAdvertisementList(info.Advitise.AppInfoAList, info.Advitise.AList), + BList: firstAdvertisementList(info.Advitise.AppInfoBList, info.Advitise.BList), SeoInfo: SeoInfo{ Title: strings.TrimSpace(info.SeoInfo.Title), Description: strings.TrimSpace(info.SeoInfo.Description), @@ -94,20 +97,28 @@ func buildAdvertisementList(list []Advertisement) []Advertisement { return out } -// NineGridRow 按首页设计每行返回两个 banner,超出当前行时返回空切片。 -func (c AppConfigVM) NineGridRow(row int) []Advertisement { - if row < 0 { - return nil +// ClickID 广告点击上报用的数字 ID(上游下发为数字字符串);非正整数时返回 0。 +func (a Advertisement) ClickID() int64 { + id, err := strconv.ParseInt(strings.TrimSpace(a.ID), 10, 64) + if err != nil || id <= 0 { + return 0 } - start := row * 2 - if start >= len(c.NineGridList) { - return nil + return id +} + +// ClickTrackable 广告 ID 与广告位均有效时才输出点击上报数据(data-ads-id / data-ads-location)。 +func (a Advertisement) ClickTrackable() bool { + return a.ClickID() > 0 && a.Location > 0 +} + +// firstAdvertisementList 返回第一个有可用广告的列表(按字段优先级)。 +func firstAdvertisementList(lists ...[]Advertisement) []Advertisement { + for _, list := range lists { + if built := buildAdvertisementList(list); len(built) > 0 { + return built + } } - end := start + 2 - if end > len(c.NineGridList) { - end = len(c.NineGridList) - } - return c.NineGridList[start:end] + return nil } // QRText 下载弹窗二维码、跳转及复制链接内容:仅使用应用下载地址 appAddr。 diff --git a/app/service/configser/view_test.go b/app/service/configser/view_test.go index 3ce05f9..d32290d 100644 --- a/app/service/configser/view_test.go +++ b/app/service/configser/view_test.go @@ -18,8 +18,7 @@ func TestBuildAppConfigVMIncludesValidAdvertisements(t *testing.T) { NewAddressInfo: NewAddressInfo{Telegram: " https://t.me/business ", QQ: " 2168045324 ", OfficialAppAddr: " https://app.example.com/download\\ "}, Config: ConfigInfo{PotaoGroup: " https://potato.example.com/group\\ "}, Advitise: AdvitiseData{ - HomeList: []Advertisement{{ID: "home", Images: "https://cdn.example.com/home.gif"}}, - DetailTopList: []Advertisement{{ID: "top", Images: " https://cdn.example.com/top.gif "}}, + HomeList: []Advertisement{{ID: "home", Images: "https://cdn.example.com/home.gif"}}, NineGridList: []Advertisement{ {ID: "banner-1", Images: "https://cdn.example.com/banner-1.gif"}, {ID: "banner-empty", Images: " "}, @@ -27,10 +26,13 @@ func TestBuildAppConfigVMIncludesValidAdvertisements(t *testing.T) { {ID: "banner-3", Images: "https://cdn.example.com/banner-3.gif"}, }, DetailBannerList: []Advertisement{{ID: "detail", Images: "https://cdn.example.com/detail.gif"}}, - BottomBanner: []Advertisement{ - {ID: " bottom ", Images: " https://cdn.example.com/bottom.gif ", Link: " https://example.com/bottom "}, - {ID: "bottom-empty", Images: " "}, + VideoTopList: []Advertisement{ + {ID: " video-top ", Images: " https://cdn.example.com/video-top.gif ", Link: " https://example.com/top "}, + {ID: "video-top-empty", Images: " "}, }, + AList: []Advertisement{{ID: "a-legacy", Images: "https://cdn.example.com/a.gif"}}, + AppInfoAList: []Advertisement{{ID: "a", Images: "https://cdn.example.com/app-a.gif"}}, + BList: []Advertisement{{ID: "b", Images: "https://cdn.example.com/b.gif"}}, DialogList: []Advertisement{ {ID: " 1 ", Images: " https://cdn.example.com/ad.webp\\ ", Link: " https://example.com ", Title: " 广告一 "}, {ID: "2", Images: " ", Title: "无图广告"}, @@ -56,17 +58,19 @@ func TestBuildAppConfigVMIncludesValidAdvertisements(t *testing.T) { if len(vm.Advitise.NineGridList) != 4 { t.Fatalf("Advitise.NineGridList length = %d, want raw length 4", len(vm.Advitise.NineGridList)) } - if len(vm.DetailTopList) != 1 || vm.DetailTopList[0].ID != "top" { - t.Fatalf("DetailTopList = %#v", vm.DetailTopList) - } if len(vm.NineGridList) != 3 { t.Fatalf("NineGridList length = %d, want 3", len(vm.NineGridList)) } - if len(vm.DetailBannerList) != 1 || vm.DetailBannerList[0].ID != "detail" { + // 详情横幅优先 videoTopList(本站后台字段),无效图片被过滤 + if len(vm.DetailBannerList) != 1 || vm.DetailBannerList[0].ID != "video-top" || vm.DetailBannerList[0].Images != "https://cdn.example.com/video-top.gif" { t.Fatalf("DetailBannerList = %#v", vm.DetailBannerList) } - if len(vm.BottomBanner) != 1 || vm.BottomBanner[0].ID != "bottom" || vm.BottomBanner[0].Images != "https://cdn.example.com/bottom.gif" { - t.Fatalf("BottomBanner = %#v", vm.BottomBanner) + // 播放页小图标优先 appInfoAList;首页小图标无 appInfoBList 时回退 BList + if len(vm.AList) != 1 || vm.AList[0].ID != "a" { + t.Fatalf("AList = %#v", vm.AList) + } + if len(vm.BList) != 1 || vm.BList[0].ID != "b" { + t.Fatalf("BList = %#v", vm.BList) } if len(vm.DialogList) != 1 { t.Fatalf("DialogList length = %d, want 1", len(vm.DialogList)) @@ -77,15 +81,26 @@ func TestBuildAppConfigVMIncludesValidAdvertisements(t *testing.T) { } } -func TestNineGridRowReturnsTwoItemsPerRow(t *testing.T) { - cfg := AppConfigVM{NineGridList: []Advertisement{{ID: "1"}, {ID: "2"}, {ID: "3"}}} - if got := cfg.NineGridRow(0); len(got) != 2 || got[0].ID != "1" || got[1].ID != "2" { - t.Fatalf("NineGridRow(0) = %#v", got) +func TestAdvertisementClickTrackable(t *testing.T) { + cases := []struct { + ad Advertisement + wantID int64 + trackable bool + }{ + {Advertisement{ID: "1138200002", Location: 2}, 1138200002, true}, + {Advertisement{ID: " 1138200012 ", Location: 12}, 1138200012, true}, + {Advertisement{ID: "1138200003", Location: 0}, 1138200003, false}, + {Advertisement{ID: "", Location: 3}, 0, false}, + {Advertisement{ID: "0", Location: 3}, 0, false}, + {Advertisement{ID: "-5", Location: 3}, 0, false}, + {Advertisement{ID: "abc", Location: 3}, 0, false}, } - if got := cfg.NineGridRow(1); len(got) != 1 || got[0].ID != "3" { - t.Fatalf("NineGridRow(1) = %#v", got) - } - if got := cfg.NineGridRow(2); len(got) != 0 { - t.Fatalf("NineGridRow(2) = %#v, want empty", got) + for _, tc := range cases { + if got := tc.ad.ClickID(); got != tc.wantID { + t.Fatalf("ClickID(%q) = %d, want %d", tc.ad.ID, got, tc.wantID) + } + if got := tc.ad.ClickTrackable(); got != tc.trackable { + t.Fatalf("ClickTrackable(%q, %d) = %v, want %v", tc.ad.ID, tc.ad.Location, got, tc.trackable) + } } } diff --git a/app/static/css/detail.css b/app/static/css/detail.css index dd95c3e..aef02bf 100644 --- a/app/static/css/detail.css +++ b/app/static/css/detail.css @@ -737,13 +737,13 @@ width: 100%; } -/* Figma: 1431×178 banner frame — stretch, 178px, radius 12, image cover-crop */ +/* 详情横幅:按素材 720×150 等比展示,避免裁切 */ .content-ad-stack a { display: block; position: relative; align-self: stretch; width: 100%; - height: 178px; + aspect-ratio: 720 / 150; overflow: hidden; border-radius: 12px; } @@ -865,7 +865,6 @@ .content-ad-stack { gap: calc(12px * var(--detail-s)); } .content-ad-stack a { - height: calc(166px * var(--detail-s)); border-radius: calc(12px * var(--detail-s)); } @@ -1001,9 +1000,11 @@ .detail-h5-top-actions, .square-promo--pc-only, .content-official-info, - .content-ad-stack, .content-column-divider { display: none !important; } + .content-ad-stack { gap: 10px; } + .content-ad-stack a { border-radius: 8px; } + .detail-layout, .detail-main, .detail-sidebar { display: contents; } diff --git a/app/static/css/homeAdRow.css b/app/static/css/homeAdRow.css deleted file mode 100644 index f6a39c0..0000000 --- a/app/static/css/homeAdRow.css +++ /dev/null @@ -1,100 +0,0 @@ -/* Shared home banner ad row (PC 2-col flex / H5 stack). */ - -/* PC Figma: stretch row, 94px, gap 36, two equal flex slots */ -.home-ad-row { - align-self: stretch; - display: flex; - flex-wrap: wrap; - justify-content: flex-start; - align-items: flex-start; - gap: 36px; - width: 100%; - flex-shrink: 0; - box-sizing: border-box; -} - -.home-ad-row.home-ad-row--mobile { - display: none; -} - -.home-ad-row .home-ad-banner { - position: relative; - flex: 1 1 calc((100% - 36px) / 2); - width: auto; - min-width: 0; - max-width: calc((100% - 36px) / 2); - height: 94px; - overflow: hidden; - border-radius: 12px; - box-sizing: border-box; -} - -.home-ad-row .home-ad-banner img { - position: absolute; - left: 0; - top: 0; - display: block; - width: 100%; - height: 100%; - object-fit: cover; -} - -@media (min-width: 768px) { - .home-ad-row { - gap: 36px; - } - - .home-ad-row .home-ad-banner { - flex: 1 1 calc((100% - 36px) / 2); - max-width: calc((100% - 36px) / 2); - height: 94px; - border-radius: 12px; - } - - .home-ad-row .home-ad-banner img { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - object-fit: cover; - } -} - -@media (max-width: 767px) { - .home-ad-row, - .home-ad-row.home-ad-row--mobile { - height: auto; - display: flex; - flex-direction: column; - flex-wrap: nowrap; - align-self: stretch; - gap: 12px; - } - - .home-ad-row.home-ad-row--desktop { - display: none; - } - - /* H5 Figma: 343×43 banner — stretch, 43px, radius 6, image cover-crop */ - .home-ad-row .home-ad-banner { - position: relative; - align-self: stretch; - flex: 0 0 43px; - width: 100%; - max-width: none; - height: 43px; - overflow: hidden; - border-radius: 6px; - } - - .home-ad-row .home-ad-banner img { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - object-fit: cover; - background: #464646; - } -} diff --git a/app/static/css/nineGridDialog.css b/app/static/css/nineGridDialog.css new file mode 100644 index 0000000..8b7edf3 --- /dev/null +++ b/app/static/css/nineGridDialog.css @@ -0,0 +1,104 @@ +/* 九宫格弹窗:深色模糊遮罩 + 4 列应用图标 + 底部关闭按钮(对齐 huangguocg.com) */ +.nine-grid-mask { + position: fixed; + inset: 0; + z-index: 1900; + padding: 40px 24px; + display: none; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.85); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + box-sizing: border-box; +} + +.nine-grid-mask.is-open { + display: flex; +} + +.nine-grid-panel { + width: min(520px, 100%); + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; +} + +.nine-grid-list { + width: 100%; + max-height: 70vh; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 28px 24px; + overflow-y: auto; + overscroll-behavior: contain; +} + +.nine-grid-item { + min-width: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + color: #fff; + text-decoration: none; +} + +.nine-grid-item[hidden] { + display: none; +} + +.nine-grid-icon { + width: 100%; + max-width: 80px; + aspect-ratio: 1; + display: block; + object-fit: cover; + border-radius: 18px; +} + +.nine-grid-name { + max-width: 100%; + overflow: hidden; + font-size: 15px; + line-height: 22px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.nine-grid-close { + width: 36px; + height: 36px; + padding: 0; + background: transparent; + border: 0; + border-radius: 50%; + cursor: pointer; +} + +.nine-grid-close img { + width: 100%; + height: 100%; + display: block; +} + +@media (max-width: 767px) { + .nine-grid-panel { + width: min(360px, 100%); + } + + .nine-grid-list { + gap: 20px 12px; + } + + .nine-grid-icon { + max-width: 70px; + border-radius: 16px; + } + + .nine-grid-name { + font-size: 13px; + line-height: 18px; + } +} diff --git a/app/static/css/play.css b/app/static/css/play.css index 2a3cfe8..5a27c56 100644 --- a/app/static/css/play.css +++ b/app/static/css/play.css @@ -2784,7 +2784,6 @@ a.play-info-tag:focus-visible { .square-promo--pc-only, .content-official-info, - .content-ad-stack, .content-column-divider, .play-sidebar, .play-download-btn { display: none !important; } diff --git a/app/static/js/common/adClick.js b/app/static/js/common/adClick.js new file mode 100644 index 0000000..e416b52 --- /dev/null +++ b/app/static/js/common/adClick.js @@ -0,0 +1,53 @@ +/** + * 广告点击上报:POST /api/app/ads/click(本站 Go 服务代理到上游,appId 由服务端固定为 12)。 + * - 广告元素由服务端模板输出 data-ads-id / data-ads-location,ID 无效时不输出,即不上报 + * - document 捕获阶段监听 click 与 auxclick(中键),不阻止默认行为、不影响广告链接打开 + * - 不依赖 request.js(导航页未加载),失败静默 + */ +(function () { + if (window.__adClickBound) return; + window.__adClickBound = true; + + var ENDPOINT = "/api/app/ads/click"; + + function positiveInt(value) { + var text = String(value == null ? "" : value).trim(); + if (!/^\d+$/.test(text)) return 0; + var number = Number(text); + return Number.isSafeInteger(number) && number > 0 ? number : 0; + } + + function payloadFrom(target) { + if (!target || typeof target.closest !== "function") return null; + var el = target.closest("[data-ads-id]"); + if (!el) return null; + var adsId = positiveInt(el.getAttribute("data-ads-id")); + var location = positiveInt(el.getAttribute("data-ads-location")); + return adsId && location ? { adsId: adsId, location: location } : null; + } + + function report(payload) { + try { + fetch(ENDPOINT, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify(payload), + credentials: "same-origin", + keepalive: true, + }).catch(function () {}); + } catch (error) { + // 上报失败不影响广告跳转。 + } + } + + function onClick(event) { + if (event.type === "auxclick" && event.button !== 1) return; + var target = event.target; + if (target && target.nodeType !== 1) target = target.parentElement; + var payload = payloadFrom(target); + if (payload) report(payload); + } + + document.addEventListener("click", onClick, true); + document.addEventListener("auxclick", onClick, true); +})(); diff --git a/app/static/js/pages/dialogAd.js b/app/static/js/pages/dialogAd.js index 9acacc4..7ed4085 100644 --- a/app/static/js/pages/dialogAd.js +++ b/app/static/js/pages/dialogAd.js @@ -1,14 +1,21 @@ (function () { + // 弹窗广告队列结束(含没有 / 已关闭过)后通知九宫格弹窗:年龄确认 → 弹窗广告 → 九宫格 + function markDone() { + if (window.__dialogAdDone) return; + window.__dialogAdDone = true; + document.dispatchEvent(new CustomEvent("dialogad:done")); + } + var QUEUE_VERSION = "2"; var DISMISSED_KEY = "dialogAdDismissed"; var PROGRESS_KEY = "dialogAdProgress"; var mask = document.querySelector("[data-dialog-ad]"); var previousBodyOverflow = ""; var hasDisplayedItem = false; - if (!mask) return; + if (!mask) return markDone(); var items = Array.prototype.slice.call(mask.querySelectorAll("[data-dialog-ad-item]")); - if (!items.length) return; + if (!items.length) return markDone(); var queueSignature = QUEUE_VERSION + @@ -111,7 +118,8 @@ mask.classList.remove("is-open"); mask.hidden = true; mask.setAttribute("hidden", ""); - document.body.style.overflow = previousBodyOverflow; + document.body.style.overflow = previousBodyOverflow === "hidden" ? "" : previousBodyOverflow; + markDone(); } function showWhenAgeGateAllows() { @@ -215,7 +223,7 @@ } }); - if (isDismissed()) return; + if (isDismissed()) return markDone(); if (currentIndex >= items.length) { finishQueue(); return; diff --git a/app/static/js/pages/eightPop.js b/app/static/js/pages/eightPop.js index f739b4e..8372bd4 100644 --- a/app/static/js/pages/eightPop.js +++ b/app/static/js/pages/eightPop.js @@ -47,11 +47,12 @@ } } - var confirmBtn = mask.querySelector("[data-eight-pop-confirm]"); + // 确认按钮和右下角关闭按钮(X)都视为确认年龄并进入网站。 + var confirmBtns = mask.querySelectorAll("[data-eight-pop-confirm]"); var exitBtns = mask.querySelectorAll("[data-eight-pop-exit]"); - if (confirmBtn) { - confirmBtn.addEventListener("click", confirmAge); - } + confirmBtns.forEach(function (button) { + button.addEventListener("click", confirmAge); + }); exitBtns.forEach(function (button) { button.addEventListener("click", exitSite); }); diff --git a/app/static/js/pages/nineGridDialog.js b/app/static/js/pages/nineGridDialog.js new file mode 100644 index 0000000..45c38d0 --- /dev/null +++ b/app/static/js/pages/nineGridDialog.js @@ -0,0 +1,160 @@ +(function () { + // 九宫格弹窗(对齐 huangguocg.com):年龄确认 → 弹窗广告 → 九宫格。 + // 点任一应用 / 关闭按钮 / 空白处 / Esc 即关闭;同一会话内容不变时不再弹出。 + var DISMISSED_KEY = "gridAdDismissed"; + var mask = document.querySelector("[data-nine-grid]"); + if (!mask) return; + + var items = Array.prototype.slice.call(mask.querySelectorAll("[data-nine-grid-item]")); + if (!items.length) return; + + var signature = JSON.stringify( + items.map(function (item) { + var image = item.querySelector("[data-nine-grid-src]"); + return [ + item.getAttribute("data-creative-id") || "", + image ? image.getAttribute("data-nine-grid-src") : "", + item.getAttribute("href") || "", + ]; + }), + ); + + function readSession(key) { + try { + return window.sessionStorage.getItem(key) || ""; + } catch (error) { + return ""; + } + } + + function writeSession(key, value) { + try { + window.sessionStorage.setItem(key, value); + } catch (error) { + // sessionStorage 不可用时仅本页关闭。 + } + } + + if (readSession(DISMISSED_KEY) === signature) return; + + function waitForImage(image) { + return new Promise(function (resolve, reject) { + if (image.complete && image.naturalWidth > 0 && image.src.indexOf("placeholder") === -1) { + resolve(); + return; + } + var timeout = window.setTimeout(function () { + cleanup(); + reject(new Error("image load timeout")); + }, 12000); + function cleanup() { + window.clearTimeout(timeout); + image.removeEventListener("load", handleLoad); + image.removeEventListener("error", handleError); + } + function handleLoad() { + cleanup(); + if (image.naturalWidth > 0) resolve(); + else reject(new Error("image decode failed")); + } + function handleError() { + cleanup(); + reject(new Error("image load failed")); + } + image.addEventListener("load", handleLoad); + image.addEventListener("error", handleError); + }); + } + + function loadIcon(image) { + var source = image.getAttribute("data-nine-grid-src") || ""; + if (!source) return Promise.reject(new Error("image source is empty")); + if (typeof window.processImage === "function") { + return window.processImage(source, image).then(function (result) { + if (!result) throw new Error("image decrypt failed"); + return waitForImage(image); + }); + } + image.src = source; + return waitForImage(image); + } + + var opened = false; + + function open() { + if (opened) return; + opened = true; + mask.hidden = false; + mask.removeAttribute("hidden"); + mask.classList.add("is-open"); + document.body.style.overflow = "hidden"; + } + + function close() { + if (!opened) return; + opened = false; + writeSession(DISMISSED_KEY, signature); + mask.classList.remove("is-open"); + mask.hidden = true; + mask.setAttribute("hidden", ""); + document.body.style.overflow = ""; + } + + // 依次等待:年龄确认关闭 → 弹窗广告队列结束(没有弹窗广告时直接放行) + function whenStartupReady(callback) { + var ageGate = document.getElementById("eightPop"); + if (ageGate && ageGate.classList.contains("is-open")) { + document.addEventListener( + "eightpop:closed", + function () { + whenStartupReady(callback); + }, + { once: true }, + ); + return; + } + var hasDialogAd = !!document.querySelector("[data-dialog-ad]"); + if (hasDialogAd && !window.__dialogAdDone) { + document.addEventListener("dialogad:done", callback, { once: true }); + return; + } + callback(); + } + + // 只展示图标加载成功的应用;一个都没加载成功则不弹 + var ready = Promise.all( + items.map(function (item) { + var image = item.querySelector("[data-nine-grid-src]"); + if (!image) return Promise.resolve(false); + return loadIcon(image) + .then(function () { + item.hidden = false; + item.removeAttribute("hidden"); + return true; + }) + .catch(function () { + return false; + }); + }), + ); + + whenStartupReady(function () { + ready.then(function (loaded) { + if (loaded.some(Boolean)) open(); + }); + }); + + mask.addEventListener("click", function (event) { + if (event.target === mask || event.target.closest("[data-nine-grid-close]") || event.target.closest("[data-nine-grid-item]")) { + close(); + } + }); + + mask.querySelector(".nine-grid-panel").addEventListener("click", function (event) { + if (event.target === event.currentTarget) close(); + }); + + document.addEventListener("keydown", function (event) { + if (event.key === "Escape" && opened) close(); + }); +})(); diff --git a/app/templates/base/contentExtras.html b/app/templates/base/contentExtras.html index c1fa744..b5cfb64 100644 --- a/app/templates/base/contentExtras.html +++ b/app/templates/base/contentExtras.html @@ -1,5 +1,6 @@ {{define "contentPromo"}} -{{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "detail_top" "SlotName" "详情顶部广告" "Label" "推广内容" "PCOnly" "1"}} +{{/* 播放页小图标:后台位置 12(AList) */}} +{{template "squarePromo" dict "Ads" .appConfig.AList "SlotKey" "play_icons" "SlotName" "播放页小图标" "Label" "推广"}} {{end}} {{define "contentOfficialInfo"}} @@ -34,11 +35,12 @@ {{end}} {{end}} +{{/* 详情横幅:后台位置 3(本站字段 videoTopList),素材 720×150 */}} {{define "contentAds"}} {{if .appConfig.DetailBannerList}}
{{range $ad := .appConfig.DetailBannerList}} - + {{$ad.Title}} {{end}} diff --git a/app/templates/base/dialogAd.html b/app/templates/base/dialogAd.html index b12d47e..1f7efe5 100644 --- a/app/templates/base/dialogAd.html +++ b/app/templates/base/dialogAd.html @@ -20,11 +20,11 @@ hidden > {{if $dialog.Link}} - + {{$dialog.Title}} {{else}} -
+
{{$dialog.Title}}
{{end}} @@ -34,6 +34,6 @@
{{end}}
- + {{end}} {{end}} diff --git a/app/templates/base/eightPop.html b/app/templates/base/eightPop.html index 8962e94..afe0037 100644 --- a/app/templates/base/eightPop.html +++ b/app/templates/base/eightPop.html @@ -13,10 +13,10 @@ - - + {{end}} diff --git a/app/templates/base/header.html b/app/templates/base/header.html index e530121..f60ffb1 100644 --- a/app/templates/base/header.html +++ b/app/templates/base/header.html @@ -166,7 +166,8 @@ + -{{template "toast" .}} {{template "eightPop" .}} {{template "dialogAd" .}} {{end}} +{{template "toast" .}} {{template "eightPop" .}} {{template "dialogAd" .}} {{template "nineGridDialog" .}} {{end}} diff --git a/app/templates/base/homeAdRow.html b/app/templates/base/homeAdRow.html deleted file mode 100644 index e1e4bd3..0000000 --- a/app/templates/base/homeAdRow.html +++ /dev/null @@ -1,30 +0,0 @@ -{{define "homeAdRow"}} -{{$ads := .Ads}} -{{if $ads}} - -
- {{range $ad := $ads}} - - {{$ad.Title}} - - {{end}} -
-{{end}} -{{end}} diff --git a/app/templates/base/nineGridDialog.html b/app/templates/base/nineGridDialog.html new file mode 100644 index 0000000..1928514 --- /dev/null +++ b/app/templates/base/nineGridDialog.html @@ -0,0 +1,29 @@ +{{define "nineGridDialog"}} +{{/* 九宫格弹窗:后台位置 2(nineGridList),在年龄确认、弹窗广告之后弹出 */}} +{{if .appConfig.NineGridList}} + + + +{{end}} +{{end}} diff --git a/app/templates/base/squarePromo.html b/app/templates/base/squarePromo.html index 3c7aa95..7605bf9 100644 --- a/app/templates/base/squarePromo.html +++ b/app/templates/base/squarePromo.html @@ -18,6 +18,7 @@ {{else}}aria-disabled="true" {{end}} data-creative-id="{{$ad.ID}}" + {{if $ad.ClickTrackable}}data-ads-id="{{$ad.ClickID}}" data-ads-location="{{$ad.Location}}"{{end}} > {{.jsonLD}} {{end}} - + diff --git a/app/templates/page/index.html b/app/templates/page/index.html index 2aee56c..1c20fec 100644 --- a/app/templates/page/index.html +++ b/app/templates/page/index.html @@ -92,7 +92,8 @@ {{end}} - {{template "squarePromo" dict "Ads" .appConfig.DetailTopList "SlotKey" "home_detail_top" "SlotName" "首页方块广告" "Label" "广告"}} + {{/* 首页小图标:后台位置 13(BList) */}} + {{template "squarePromo" dict "Ads" .appConfig.BList "SlotKey" "home_icons" "SlotName" "首页小图标" "Label" "推广"}} {{range $catIndex, $category := .homeCategoryData}} {{if contains $category.Name "热榜"}} @@ -177,7 +178,6 @@ {{end}} - {{template "homeAdRow" dict "Ads" ($.appConfig.NineGridRow $catIndex) "SlotKey" "home_nine_grid" "SlotName" "首页横幅广告" "Label" "广告"}} {{end}} {{if .homeRecommendThemes}} diff --git a/app/templates/page/list.html b/app/templates/page/list.html index 8d4751c..bc9f955 100644 --- a/app/templates/page/list.html +++ b/app/templates/page/list.html @@ -61,16 +61,6 @@ {{end}} {{template "pagination" .}} - {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "list_nine_grid" "SlotName" "片库横幅广告" "Label" "广告"}} - {{if .appConfig.BottomBanner}} -
- {{range $ad := .appConfig.BottomBanner}} - - {{$ad.Title}} - - {{end}} -
- {{end}} {{template "footer" .}} diff --git a/app/templates/page/nav.html b/app/templates/page/nav.html index f3820f3..d3140e8 100644 --- a/app/templates/page/nav.html +++ b/app/templates/page/nav.html @@ -106,7 +106,9 @@ {{template "pageConsole" .}} + {{template "dialogAd" .}} +{{template "nineGridDialog" .}} diff --git a/app/templates/page/play.html b/app/templates/page/play.html index 4b7903b..1e72a92 100644 --- a/app/templates/page/play.html +++ b/app/templates/page/play.html @@ -26,8 +26,8 @@ {{end}} - - + + diff --git a/app/templates/page/rank.html b/app/templates/page/rank.html index 457fcd2..a19cbb9 100644 --- a/app/templates/page/rank.html +++ b/app/templates/page/rank.html @@ -66,16 +66,6 @@ {{end}} {{template "pagination" .}} - {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "rank_nine_grid" "SlotName" "排行榜横幅广告" "Label" "广告"}} - {{if .appConfig.BottomBanner}} -
- {{range $ad := .appConfig.BottomBanner}} - - {{$ad.Title}} - - {{end}} -
- {{end}} {{template "footer" .}} diff --git a/app/templates/page/series.html b/app/templates/page/series.html index 1dfaae1..b4770e0 100644 --- a/app/templates/page/series.html +++ b/app/templates/page/series.html @@ -57,16 +57,6 @@ {{end}} {{template "pagination" .}} - {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "series_nine_grid" "SlotName" "剧集横幅广告" "Label" "广告"}} - {{if .appConfig.BottomBanner}} -
- {{range $ad := .appConfig.BottomBanner}} - - {{$ad.Title}} - - {{end}} -
- {{end}} {{template "footer" .}} diff --git a/app/templates/page/themeIndex.html b/app/templates/page/themeIndex.html index 30a7c03..3ef1686 100644 --- a/app/templates/page/themeIndex.html +++ b/app/templates/page/themeIndex.html @@ -47,17 +47,7 @@ {{template "pagination" .}} - {{template "homeAdRow" dict "Ads" .appConfig.NineGridList "SlotKey" "topic_nine_grid" "SlotName" "专题横幅广告" "Label" "广告"}} - {{if .appConfig.BottomBanner}} -
- {{range $ad := .appConfig.BottomBanner}} - - {{$ad.Title}} - - {{end}} -
- {{end}} {{template "footer" .}}