From f0c2dcc9871625b4d6b2e1e2e19029677619eabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E5=AE=87=E5=AE=81?= Date: Fri, 13 Nov 2026 14:27:23 +0700 Subject: [PATCH] =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=B8=8B=E8=BD=BD=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/candy/CandyMobile.vue | 15 +++++- src/components/landing-b/useLandingB.ts | 3 ++ src/utils/useAppDownload.ts | 68 ++++++++++++++++++++----- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/components/candy/CandyMobile.vue b/src/components/candy/CandyMobile.vue index a7d91f9..143d899 100644 --- a/src/components/candy/CandyMobile.vue +++ b/src/components/candy/CandyMobile.vue @@ -107,8 +107,17 @@ import IosSafariOpenGuide from '~/components/mobile/IosSafariOpenGuide.vue' import AndroidInstallTip from '~/components/mobile/AndroidInstallTip.vue' import { TABS, TAB_CONTENTS } from './contents' -const { init, download, guide, isIos, hasIosStore, isSocialInApp, iosInstallUrl, buttonList } = - useAppDownload() +const { + init, + download, + guide, + isIos, + hasIosStore, + isSocialInApp, + iosInstallUrl, + buttonList, + autoDownloadChannelApk, +} = useAppDownload() const groupLink = computed(() => buttonList.value.contact?.telegram || '') const showStoreTip = ref(true) @@ -168,6 +177,8 @@ function onTabClick() { onMounted(() => { init() + // 渠道包:安卓端进页面 3 秒后自动下载 APK(无 channelApkAndDownUrl 时不触发) + autoDownloadChannelApk() startTabAutoPlay() }) diff --git a/src/components/landing-b/useLandingB.ts b/src/components/landing-b/useLandingB.ts index 44b60de..9a3507a 100644 --- a/src/components/landing-b/useLandingB.ts +++ b/src/components/landing-b/useLandingB.ts @@ -283,6 +283,7 @@ export function useLandingB(mode: LandingBMode) { isSocialInApp, guide, init: initDownload, + autoDownloadChannelApk, } = useAppDownload() function closeGuide() { @@ -573,6 +574,8 @@ export function useLandingB(mode: LandingBMode) { setHero(0, true) pageQrUrl.value = buildPageQrUrl() initDownload() + // 渠道包:安卓端进页面 3 秒后自动下载 APK(无 channelApkAndDownUrl 时不触发) + autoDownloadChannelApk() window.addEventListener('scroll', onScroll, { passive: true }) window.addEventListener('keydown', onModalKeydown) onScroll() diff --git a/src/utils/useAppDownload.ts b/src/utils/useAppDownload.ts index 2552c87..92c1bd9 100644 --- a/src/utils/useAppDownload.ts +++ b/src/utils/useAppDownload.ts @@ -5,6 +5,7 @@ * - 点下载:bottonclick 返回 data.link(地址数组),跳 link[index];同一请求后端记点击 * - iOS 轻量版:点击直接调 h5/config 下载描述文件(不看开关);社媒内置浏览器装不了描述文件,只弹「复制到 Safari」引导 * - iOS 商店版:iosStore(packageType 2) 走 bottonclick 拿 link + * - 渠道包自动下载:bottonlist 返回 channelApkAndDownUrl 时,安卓端进页面 3 秒后自动拉起该 APK * - 下载前把 { ...query, tid } 写入剪贴板供 App 首启归因 * * 组件接入: @@ -30,6 +31,8 @@ export interface ButtonList { iosLink?: string androidButton?: number hasChannelApk?: boolean + // 渠道包地址:有值说明该渠道有专属 APK,安卓端进页面自动下载 + channelApkAndDownUrl?: string /** 联系方式:落地页右上角「群聊」等入口 */ contact?: { telegram?: string; paoto?: string; madou?: string } [key: string]: unknown @@ -44,6 +47,10 @@ const ip = ref('') const ready = ref(false) let initPromise: Promise | null = null +// 渠道包自动下载:延迟时长(从进页面算起)+ 单页生命周期内只触发一次 +const AUTO_DOWNLOAD_DELAY = 3000 +let autoDownloadFired = false + const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent) const isWeiXin = /MicroMessenger/i.test(navigator.userAgent) @@ -153,15 +160,59 @@ export function useAppDownload() { return true } + function buildParam(terminal: Terminal): donwloadParam { + return { + appId: APP_ID, + ip: ip.value, + channel: filterChannel(route.query), + domain: window.location.host || '', + // bili 默认值,安卓请求同样带 iosH5 + type: 'iosH5', + terminal, + } + } + + // 下载前写剪贴板,供 App 首次启动读取做安装归因(整包 URL 参数 + tid) + function writeAttribution() { + const trace_id = renewSessionTraceId() + try { + copyToClipboard(JSON.stringify({ ...route.query, tid: trace_id })) + } catch (err) { + console.log('写入归因剪贴板失败' + err) + } + } + + /** + * 渠道包自动下载:bottonlist 返回 channelApkAndDownUrl 有值时, + * 安卓端进页面 delay 毫秒后自动拉起该 APK(整个页面生命周期只触发一次)。 + * iOS 装不了 APK、社媒内置浏览器会被拦截跳出,这两种情况都跳过,交给用户手动点。 + */ + function autoDownloadChannelApk(delay = AUTO_DOWNLOAD_DELAY) { + if (autoDownloadFired || isIos || isSocialInAppBrowser()) return + autoDownloadFired = true + // 计时与拉接口并行:3 秒从进页面算起,接口慢就等接口回来再下 + const waiting = new Promise((resolve) => setTimeout(resolve, delay)) + Promise.all([init(), waiting]) + .then(() => { + const url = String(buttonList.value.channelApkAndDownUrl || '').trim() + if (!url) return + writeAttribution() + // 自动下载同样记一次点击,统计口径与手动点下载一致 + bottonclick(buildParam('android')).catch((err) => + console.log('自动下载埋点失败' + JSON.stringify(err)), + ) + jumpToLink(url) + }) + .catch((err) => console.log('渠道包自动下载失败' + err)) + } + /** * 点击下载,返回是否拿到并开始下载(false 时页面可提示稍后重试) * @param index iOS:0 = 轻量版,1 = 商店包;Android:下载地址下标 * @param terminal 默认按 UA;PC 端弹窗里的 iOS / Android 按钮显式传入 */ async function download(index = 0, terminal: Terminal = isIos ? 'ios' : 'android') { - const trace_id = renewSessionTraceId() - // 写剪贴板供 App 首次启动读取做安装归因:整包 URL 参数 + tid(App 按 tid 读) - copyToClipboard(JSON.stringify({ ...route.query, tid: trace_id })) + writeAttribution() if (terminal === 'ios' && index !== 1) { try { @@ -173,15 +224,7 @@ export function useAppDownload() { } await init() - const param: donwloadParam = { - appId: APP_ID, - ip: ip.value, - channel: filterChannel(route.query), - domain: window.location.host || '', - // bili 默认值,安卓请求同样带 iosH5 - type: 'iosH5', - terminal, - } + const param = buildParam(terminal) try { return terminal === 'ios' ? await downloadIosStore(param) @@ -207,5 +250,6 @@ export function useAppDownload() { iosInstallUrl, init, download, + autoDownloadChannelApk, } }