diff --git a/app/service/configser/types.go b/app/service/configser/types.go index c39e644..34a1db4 100644 --- a/app/service/configser/types.go +++ b/app/service/configser/types.go @@ -105,6 +105,11 @@ type AdvitiseData struct { TitleList []Advertisement `json:"titleList"` PlayList []Advertisement `json:"playList"` PauseyList []Advertisement `json:"pauseyList"` + // 本站后台(webhg2api)的字段名:横幅 / 播放页小图标 / 首页小图标 + 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 83d4d29..e7bd8bc 100644 --- a/app/service/configser/view.go +++ b/app/service/configser/view.go @@ -8,33 +8,35 @@ import ( // AppConfigVM 注入全站页面的公共配置(字段名与接口 newAddressInfo / seoInfo 对齐)。 type AppConfigVM struct { // Config 为 data.config 原样字段(页脚社交等优先用此对象)。 - Config ConfigInfo `json:"config"` - 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"` - TelegramAdd string `json:"telegramAdd"` - PotaoGroup string `json:"potaoGroup"` - CustomerServiceHost string `json:"customerServiceHost"` - AppAddr string `json:"appAddr"` - OfficialAppAddr string `json:"officialAppAddr"` - PDFAddr string `json:"pdfAddr"` - TwitterAddr string `json:"twitterAddr"` - GitAddr string `json:"gitAddr"` - FreeVPN string `json:"freeVPN"` - 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"` + Config ConfigInfo `json:"config"` + 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"` + TelegramAdd string `json:"telegramAdd"` + PotaoGroup string `json:"potaoGroup"` + CustomerServiceHost string `json:"customerServiceHost"` + AppAddr string `json:"appAddr"` + OfficialAppAddr string `json:"officialAppAddr"` + PDFAddr string `json:"pdfAddr"` + TwitterAddr string `json:"twitterAddr"` + GitAddr string `json:"gitAddr"` + FreeVPN string `json:"freeVPN"` + 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 从上游配置组装前端可用字段;社交/地址优先取 data.config。 @@ -65,11 +67,12 @@ func BuildAppConfigVM(info *ConfigInfoResp) AppConfigVM { FreeVPN: sanitizeURL(cfg.FreeVPN), 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), @@ -142,20 +145,14 @@ func buildAdvertisementList(list []Advertisement) []Advertisement { return out } -// NineGridRow 按首页设计每行返回两个 banner,超出当前行时返回空切片。 -func (c AppConfigVM) NineGridRow(row int) []Advertisement { - if row < 0 { - return nil +// firstAdvertisementList 返回第一个有可用广告的列表(按字段优先级)。 +func firstAdvertisementList(lists ...[]Advertisement) []Advertisement { + for _, list := range lists { + if built := buildAdvertisementList(list); len(built) > 0 { + return built + } } - start := row * 2 - if start >= len(c.NineGridList) { - return nil - } - 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 new file mode 100644 index 0000000..842032f --- /dev/null +++ b/app/service/configser/view_test.go @@ -0,0 +1,47 @@ +package configser + +import "testing" + +func TestBuildAppConfigVMMapsSiteAdSlots(t *testing.T) { + vm := BuildAppConfigVM(&ConfigInfoResp{ + Advitise: AdvitiseData{ + DialogList: []Advertisement{{ID: "dialog", Images: "https://cdn.example.com/dialog.gif"}}, + NineGridList: []Advertisement{{ID: "grid", Images: "https://cdn.example.com/grid.gif"}, {ID: "grid-empty", Images: " "}}, + VideoTopList: []Advertisement{ + {ID: " top ", Images: " https://cdn.example.com/top.gif ", Link: " https://example.com/top "}, + {ID: "top-empty", Images: " "}, + }, + DetailBannerList: []Advertisement{{ID: "legacy-banner", Images: "https://cdn.example.com/legacy.gif"}}, + AppInfoAList: []Advertisement{{ID: "a", Images: "https://cdn.example.com/a.gif"}}, + AList: []Advertisement{{ID: "legacy-a", Images: "https://cdn.example.com/legacy-a.gif"}}, + BList: []Advertisement{{ID: "b", Images: "https://cdn.example.com/b.gif"}}, + }, + }) + + if len(vm.DialogList) != 1 || vm.DialogList[0].ID != "dialog" { + t.Fatalf("DialogList = %#v", vm.DialogList) + } + if len(vm.NineGridList) != 1 || vm.NineGridList[0].ID != "grid" { + t.Fatalf("NineGridList = %#v, want images-only items", vm.NineGridList) + } + // 详情横幅优先本站后台字段 videoTopList,并清洗空格 / 过滤无图 + if len(vm.DetailBannerList) != 1 || vm.DetailBannerList[0].ID != "top" || vm.DetailBannerList[0].Link != "https://example.com/top" { + t.Fatalf("DetailBannerList = %#v", vm.DetailBannerList) + } + // 播放页小图标优先 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) + } +} + +func TestBuildAppConfigVMFallsBackToLegacyBannerField(t *testing.T) { + vm := BuildAppConfigVM(&ConfigInfoResp{ + Advitise: AdvitiseData{DetailBannerList: []Advertisement{{ID: "banner", Images: "https://cdn.example.com/banner.gif"}}}, + }) + if len(vm.DetailBannerList) != 1 || vm.DetailBannerList[0].ID != "banner" { + t.Fatalf("DetailBannerList = %#v, want detailbannerList fallback", vm.DetailBannerList) + } +} diff --git a/app/static/css/appIcons.css b/app/static/css/appIcons.css new file mode 100644 index 0000000..aede0b6 --- /dev/null +++ b/app/static/css/appIcons.css @@ -0,0 +1,83 @@ +/* 小方块推荐(首页小图标 / 播放页小图标):一排方形图标 + 名称,放不下时横向滑动 */ +.app-icons { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + min-width: 0; + padding: 16px 0; + box-sizing: border-box; +} + +.app-icons__label { + font-size: 12px; + line-height: 18px; + color: rgba(255, 255, 255, 0.4); +} + +.app-icons__row { + display: flex; + gap: 16px; + overflow-x: auto; + overscroll-behavior-x: contain; + scrollbar-width: none; +} + +.app-icons__row::-webkit-scrollbar { + display: none; +} + +.app-icons__item { + flex: 0 0 auto; + width: 96px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + color: rgba(255, 255, 255, 0.9); + text-decoration: none; +} + +.app-icons__img { + width: 96px; + height: 96px; + display: block; + object-fit: cover; + border-radius: 16px; + background: #232328; +} + +.app-icons__name { + max-width: 100%; + overflow: hidden; + font-size: 14px; + line-height: 20px; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 768px) { + .app-icons { + gap: 6px; + padding: 12px var(--h5-gutter, 16px); + } + + .app-icons__row { + gap: 10px; + } + + .app-icons__item { + width: 64px; + } + + .app-icons__img { + width: 64px; + height: 64px; + border-radius: 14px; + } + + .app-icons__name { + font-size: 12px; + line-height: 18px; + } +} diff --git a/app/static/css/nineGridDialog.css b/app/static/css/nineGridDialog.css new file mode 100644 index 0000000..6fba66f --- /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 svg { + 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/promo.css b/app/static/css/promo.css index 2a711f1..1c095c1 100644 --- a/app/static/css/promo.css +++ b/app/static/css/promo.css @@ -1,6 +1,5 @@ -/* 公共广告位统一规范 - * PC:双列,固定高 169 - * H5:单列两行,宽度 100%(占满所属内容容器),比例 343/98 +/* 公共广告位统一规范(详情横幅,素材 720×150) + * PC:双列;H5:单列。均按 720:150 等比展示,避免裁切 */ .promo-rail { @@ -22,7 +21,7 @@ width: 100%; max-width: 100%; min-width: 0; - height: 169px; + aspect-ratio: 720 / 150; border-radius: var(--radius-md); background: #232328; box-sizing: border-box; @@ -86,8 +85,7 @@ width: 100%; max-width: 100%; height: auto; - min-height: 98px; - aspect-ratio: 343 / 98; + aspect-ratio: 720 / 150; border-radius: var(--radius-md); border: 1px solid var(--line-soft); } diff --git a/app/static/js/pages/dialogAd.js b/app/static/js/pages/dialogAd.js index 5633aa3..11588bb 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 + @@ -119,7 +126,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() { @@ -234,7 +242,7 @@ } }); - if (isDismissed()) return; + if (isDismissed()) return markDone(); if (currentIndex >= items.length) { finishQueue(); return; diff --git a/app/static/js/pages/nineGridDialog.js b/app/static/js/pages/nineGridDialog.js new file mode 100644 index 0000000..a58729f --- /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.querySelector("[data-eighteen]"); + if (ageGate && ageGate.classList.contains("is-open")) { + document.addEventListener( + "eighteen: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/template/base/ads.html b/app/template/base/ads.html index 77cddf3..6369517 100644 --- a/app/template/base/ads.html +++ b/app/template/base/ads.html @@ -1,8 +1,8 @@ {{define "ads"}} -{{/* 入参为 advitise.detailbannerList 的切片,字段与接口一致:images / link / title +{{/* 详情横幅(后台位置 3,本站字段 videoTopList)的切片,字段:images / link / title 类名避免 ads/ad-slot:夸克等浏览器内置广告拦截会隐藏这类选择器 */}} {{if .}} - +
{{range .}} diff --git a/app/template/base/appIcons.html b/app/template/base/appIcons.html new file mode 100644 index 0000000..8677b39 --- /dev/null +++ b/app/template/base/appIcons.html @@ -0,0 +1,18 @@ +{{define "appIcons"}} +{{/* 小方块推荐:首页小图标(后台位置 13,BList)/ 播放页小图标(后台位置 12,AList) + 入参 dict "Items" 列表 "Label" 无障碍名称;类名避免 ads/ad-slot(夸克等浏览器会拦截) */}} +{{if .Items}} + +
+ 推广 + +
+{{end}} +{{end}} diff --git a/app/template/base/dialogAd.html b/app/template/base/dialogAd.html index f40a081..1d13a76 100644 --- a/app/template/base/dialogAd.html +++ b/app/template/base/dialogAd.html @@ -51,6 +51,6 @@ {{end}} - + {{end}} {{end}} diff --git a/app/template/base/header.html b/app/template/base/header.html index b78de20..3d4e880 100644 --- a/app/template/base/header.html +++ b/app/template/base/header.html @@ -163,6 +163,7 @@ {{template "eighteen" .}} {{template "dialogAd" .}} +{{template "nineGridDialog" .}} {{template "toast" .}} {{end}} diff --git a/app/template/base/nineGridDialog.html b/app/template/base/nineGridDialog.html new file mode 100644 index 0000000..d3c2126 --- /dev/null +++ b/app/template/base/nineGridDialog.html @@ -0,0 +1,28 @@ +{{define "nineGridDialog"}} +{{/* 九宫格弹窗:后台位置 2(nineGridList),在年龄确认、弹窗广告之后弹出 */}} +{{if .appConfig.NineGridList}} + + + +{{end}} +{{end}} diff --git a/app/template/pages/index.html b/app/template/pages/index.html index 1b40ce7..50f49d2 100644 --- a/app/template/pages/index.html +++ b/app/template/pages/index.html @@ -104,6 +104,8 @@
{{end}} + {{template "appIcons" dict "Items" .appConfig.BList "Label" "首页小图标"}} +