This commit is contained in:
谢宇宁
2026-09-15 18:00:56 +07:00
parent 2d63d37930
commit b9c8a59b6c
33 changed files with 232 additions and 2393 deletions
-19
View File
@@ -45,22 +45,3 @@
<script type="module" src="/src/main.js"></script>
</body>
</html>
<!-- LandingSDK 埋点加载(v2.0.0,与 jha-505 / bili2 同版;初始化见 src/App.vue initTracker -->
<script>
(function (i, s, o, g, r) {
i[r] =
i[r] ||
function () {
var args = Array.prototype.slice.call(arguments);
return new Promise(function (resolve, reject) {
(i[r].q = i[r].q || []).push({ args: args, resolve: resolve, reject: reject });
});
};
i[r].l = 1 * new Date();
var a = s.createElement(o);
var m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m);
})(window, document, "script", "/landing-sdk-v2.0.0.js", "tracker");
</script>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-34
View File
@@ -1,37 +1,3 @@
<script setup>
import { onMounted } from 'vue'
function initTracker() {
var urlParams = new URLSearchParams(window.location.search)
var config = {
appId: 'JHA-209',
domain: window.location.origin,
// 渠道参数优先级 dc → ch → pc(与 d10vry7axuojk2 版落地页一致)
channel: urlParams.get('dc') || urlParams.get('ch') || urlParams.get('pc') || '',
batch: true,
}
var timer = setInterval(function () {
if (window.LandingSDK) {
clearInterval(timer)
window.LandingSDK.init(config)
var queued = (window.tracker && window.tracker.q) || []
var real = window.LandingSDK.track
real.q = []
window.tracker = real
queued.forEach(function (item) {
real.apply(null, item.args).then(item.resolve).catch(item.reject)
})
queued.length = 0
}
}, 30)
}
// 黄果落地页埋点 appId = JHA-209(下载接口用数字 209,见 config/app.ts);channel 取 URL 的 dc 渠道参数
onMounted(() => {
initTracker()
})
</script>
<template>
<div id="app">
<router-view></router-view>
+14 -14
View File
@@ -1,28 +1,28 @@
/*
* @Author:
* @Mail:
* @Date: 2023-12-14 17:27:41
* @LastEditTime: 2023-12-14 20:58:14
* @LastEditors: Please set LastEditors
* @FilePath: /ksLandPage/api/index.ts
*/
import { useHttp } from '../utils/userFetchReq'
import type { donwloadParam, ButtonListParam, H5param } from './interface'
// 下载相关接口对齐 bili-landpage-client,域名写死(接口已开 CORS: *)
const STAT_API = 'https://blsp01.com/api/stat/version'
// 按钮开关:iosDown / iosStore / iosTF / iosH5 / androidButton,不含下载地址
export const getButtonList = (data: ButtonListParam) => {
return useHttp.post('/plm/okn/ijb/nuS0EDXSczKgf3Xh', data, {})
return useHttp.post(`${STAT_API}/bottonlist`, data, {})
}
// 点击下载:返回 data.link(下载地址数组),同时后端记录点击
export const bottonclick = (param: donwloadParam) => {
return useHttp.post('/plm/okn/ijb/vl0gpuBNWklJwcv1', param, {
})
return useHttp.post(`${STAT_API}/bottonclick`, param, {})
}
export const downloadH5 = (data: H5param) => {
return useHttp.get('/qaz/wsx/edc/mobileconfig/bili', data, {
// iOS H5 描述文件:返回 .mobileconfig 原文(非 JSON),不走 useHttp
export const downloadH5 = async (data: H5param): Promise<string> => {
const res = await fetch(`${STAT_API}/h5/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error('获取描述文件失败 ' + res.status)
return res.text()
}
export const getCustomerServiceUrl = (data: {
+2 -6
View File
@@ -9,9 +9,6 @@ export type ButtonListParam = {
appId: number
ip?: string
channel?: string | channel //渠道
domain?: string
version?: string
tid?: string
}
export type channel = {
dc?: string
@@ -23,10 +20,9 @@ export type donwloadParam = {
ip: string
channel: string | channel //渠道
domain: string
type: string
type: string // iosStore / iosDown / iosTF / iosH5
terminal: string //终端 ios或者android
packageType?: number
version?: string
packageType?: number // 1 企业签 / 2 商店包 / 3 TF
}
export type H5param = {
appId: number
+1 -1
View File
@@ -2,7 +2,7 @@
import LandingBPc from './LandingBPc.vue'
import LandingBMobile from './LandingBMobile.vue'
// 与 A 版 / hjll2 一致:仅按 UA 分流,不按宽度切换
// 仅按 UA 分流,不按宽度切换
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
)
+17 -66
View File
@@ -2,7 +2,6 @@
import { useLandingB } from './useLandingB'
const {
SOCIAL_LINKS,
FEATURED,
GENRES,
FAQS,
@@ -104,21 +103,6 @@ const {
<symbol id="i-si-android" viewBox="0 0 24 24">
<path fill="currentColor" d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z" />
</symbol>
<symbol id="i-si-x" viewBox="0 0 24 24">
<path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</symbol>
<symbol id="i-si-telegram" viewBox="0 0 24 24">
<path fill="currentColor" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</symbol>
<symbol id="i-si-instagram" viewBox="0 0 24 24">
<path fill="currentColor" d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077" />
</symbol>
<symbol id="i-si-tiktok" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z" />
</symbol>
<symbol id="i-si-youtube" viewBox="0 0 24 24">
<path fill="currentColor" d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</symbol>
</svg>
<!-- ============ NAV 导航栏 ============ -->
@@ -129,7 +113,7 @@ const {
<img class="wm" :src="assetUrl('wordmark-white-1line.png')" alt="黄果短剧" />
</a>
<div class="right">
<button class="btn glass xs" data-track="download" @click="handleDownload()">
<button class="btn glass xs" @click="handleDownload()">
<svg class="ic"><use href="#i-download" /></svg>
下载 APP
</button>
@@ -188,7 +172,7 @@ const {
<template v-if="hasIosShopPackage">
<button
class="btn primary has-rec"
data-track="download"
@click="handleIosStoreAction"
>
<svg class="ic brandico"><use href="#i-si-apple" /></svg>
@@ -197,7 +181,7 @@ const {
</button>
<button
class="btn outline"
data-track="download"
@click="handleDownload('ios-lite', currentHero)"
>
<svg class="ic brandico"><use href="#i-si-apple" /></svg>
@@ -207,7 +191,7 @@ const {
<button
v-else
class="btn primary"
data-track="download"
@click="handleDownload('ios-lite', currentHero)"
>
<svg class="ic brandico"><use href="#i-si-apple" /></svg>
@@ -223,7 +207,7 @@ const {
<template v-else>
<button
class="btn primary"
data-track="download"
@click="handleDownload('android', currentHero)"
>
<svg class="ic brandico"><use href="#i-si-android" /></svg>
@@ -280,16 +264,16 @@ const {
<!-- 题材筛选器点击不切换筛选统一触发下载 -->
<div class="filters" data-reveal>
<div class="seg lg">
<button class="on" data-track="download" @click="handleFilterClick">全部</button>
<button data-track="download" @click="handleFilterClick">短剧</button>
<button data-track="download" @click="handleFilterClick">影集</button>
<button class="on" @click="handleFilterClick">全部</button>
<button @click="handleFilterClick">短剧</button>
<button @click="handleFilterClick">影集</button>
</div>
<div class="gchips">
<button
v-for="g in GENRES"
:key="g"
class="chip"
data-track="download"
@click="handleFilterClick"
>
{{ g }}
@@ -308,7 +292,7 @@ const {
>
<div class="rc">
<span class="rank">{{ i + 1 }}</span>
<div class="poster" data-track="download" @click="handleDownload(undefined, d)">
<div class="poster" @click="handleDownload(undefined, d)">
<img :src="assetUrl(d.cover)" :alt="d.t" loading="lazy" />
<span class="badge" :class="{ dark: d.kind === '影集' }">
{{ d.kind === '影集' ? '影集' : '原创' }}
@@ -441,7 +425,7 @@ const {
<div class="btns">
<button
class="btn primary"
data-track="download"
@click="handleDownload('ios-lite')"
>
<svg class="ic brandico"><use href="#i-si-apple" /></svg>
@@ -450,7 +434,7 @@ const {
<button
v-if="hasIosShopPackage"
class="btn outline"
data-track="download"
@click="handleDownload('ios-store')"
>
<svg class="ic brandico"><use href="#i-si-apple" /></svg>
@@ -458,7 +442,7 @@ const {
</button>
<button
class="btn glass"
data-track="download"
@click="handleDownload('android')"
>
<svg class="ic brandico"><use href="#i-si-android" /></svg>
@@ -484,11 +468,6 @@ const {
<div class="left" data-reveal>
<span class="kicker">常见问题</span>
<h2 class="h2">关于黄果<br />你想知道的</h2>
<p>还有其他问题欢迎随时联络我们</p>
<a class="link brand" :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener">
联络我们
<svg class="ic"><use href="#i-arrow-right" /></svg>
</a>
</div>
<div class="acc" data-reveal>
<div
@@ -583,29 +562,11 @@ const {
<img class="ico" :src="assetUrl('logo-icon-activity.png')" alt="黄果短剧" />
<img class="wm" :src="assetUrl('wordmark-white-2line.png')" alt="黄果短剧 huangguo.com" />
</div>
<div class="soc">
<a class="circ" :href="SOCIAL_LINKS.x" target="_blank" rel="noopener" aria-label="X">
<svg class="ic brandico"><use href="#i-si-x" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener" aria-label="Telegram">
<svg class="ic brandico"><use href="#i-si-telegram" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.instagram" target="_blank" rel="noopener" aria-label="Instagram">
<svg class="ic brandico"><use href="#i-si-instagram" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.tiktok" target="_blank" rel="noopener" aria-label="TikTok">
<svg class="ic brandico"><use href="#i-si-tiktok" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.youtube" target="_blank" rel="noopener" aria-label="YouTube">
<svg class="ic brandico"><use href="#i-si-youtube" /></svg>
</a>
</div>
<div class="fl">
<a href="#works" @click.prevent="switchView('landing', '#works')">作品精选</a>
<a href="#about" @click.prevent="switchView('landing', '#about')">关于黄果</a>
<a href="#faq" @click.prevent="switchView('landing', '#faq')">常见问题</a>
<a href="#" data-track="download" @click.prevent="handleDownload()">下载 APP</a>
<a :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener">联系我们</a>
<a href="#" @click.prevent="handleDownload()">下载 APP</a>
</div>
<div class="legal">
<b>18 U.S.C. 2257 记录保存要求合规声明</b>
@@ -624,7 +585,7 @@ const {
<b>黄果短剧 App</b>
<span>免费下载&nbsp;&nbsp;海量短剧每日更新</span>
</div>
<button class="btn primary" data-track="download" @click="handleDownload()">
<button class="btn primary" @click="handleDownload()">
<svg class="ic"><use href="#i-download" /></svg>
下载 APP
</button>
@@ -763,7 +724,7 @@ const {
<div class="bar2">
<button
class="btn primary md block"
data-track="download"
type="button"
@click="handleInstallGuidePrimary"
>
@@ -823,7 +784,7 @@ const {
<div class="bar2">
<button
class="btn primary md block"
data-track="download"
type="button"
@click="handleInstallGuidePrimary"
>
@@ -2316,16 +2277,6 @@ footer .lockup img.wm {
width: auto;
}
footer .soc {
display: flex;
gap: 14px;
}
footer .soc .circ {
width: 44px;
height: 44px;
}
footer .fl {
display: flex;
gap: 32px;
+22 -71
View File
@@ -3,7 +3,6 @@ import QrcodeVue from 'qrcode.vue'
import { useLandingB } from './useLandingB'
const {
SOCIAL_LINKS,
FEATURED,
GENRES,
FAQS,
@@ -107,21 +106,6 @@ const {
<symbol id="i-si-android" viewBox="0 0 24 24">
<path fill="currentColor" d="M18.4395 5.5586c-.675 1.1664-1.352 2.3318-2.0274 3.498-.0366-.0155-.0742-.0286-.1113-.043-1.8249-.6957-3.484-.8-4.42-.787-1.8551.0185-3.3544.4643-4.2597.8203-.084-.1494-1.7526-3.021-2.0215-3.4864a1.1451 1.1451 0 0 0-.1406-.1914c-.3312-.364-.9054-.4859-1.379-.203-.475.282-.7136.9361-.3886 1.5019 1.9466 3.3696-.0966-.2158 1.9473 3.3593.0172.031-.4946.2642-1.3926 1.0177C2.8987 12.176.452 14.772 0 18.9902h24c-.119-1.1108-.3686-2.099-.7461-3.0683-.7438-1.9118-1.8435-3.2928-2.7402-4.1836a12.1048 12.1048 0 0 0-2.1309-1.6875c.6594-1.122 1.312-2.2559 1.9649-3.3848.2077-.3615.1886-.7956-.0079-1.1191a1.1001 1.1001 0 0 0-.8515-.5332c-.5225-.0536-.9392.3128-1.0488.5449zm-.0391 8.461c.3944.5926.324 1.3306-.1563 1.6503-.4799.3197-1.188.0985-1.582-.4941-.3944-.5927-.324-1.3307.1563-1.6504.4727-.315 1.1812-.1086 1.582.4941zM7.207 13.5273c.4803.3197.5506 1.0577.1563 1.6504-.394.5926-1.1038.8138-1.584.4941-.48-.3197-.5503-1.0577-.1563-1.6504.4008-.6021 1.1087-.8106 1.584-.4941z" />
</symbol>
<symbol id="i-si-x" viewBox="0 0 24 24">
<path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z" />
</symbol>
<symbol id="i-si-telegram" viewBox="0 0 24 24">
<path fill="currentColor" d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
</symbol>
<symbol id="i-si-instagram" viewBox="0 0 24 24">
<path fill="currentColor" d="M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077" />
</symbol>
<symbol id="i-si-tiktok" viewBox="0 0 24 24">
<path fill="currentColor" d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z" />
</symbol>
<symbol id="i-si-youtube" viewBox="0 0 24 24">
<path fill="currentColor" d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
</symbol>
</svg>
<!-- ============ NAV 导航栏 ============ -->
@@ -155,7 +139,7 @@ const {
</a>
</nav>
<div class="right">
<button class="btn glass sm" data-track="download" @click="handleDownload()">
<button class="btn glass sm" @click="handleDownload()">
<svg class="ic"><use href="#i-download" /></svg>
下载 APP
</button>
@@ -226,7 +210,7 @@ const {
<div class="dlbtns">
<button
class="btn primary lg dl-main"
data-track="download"
type="button"
@click="handleDownload(undefined, currentHero)"
>
@@ -237,15 +221,15 @@ const {
<button
v-if="hasIosShopPackage"
type="button"
data-track="download"
@click="handleDownload('ios-store', currentHero)"
>
iOS 商店版
</button>
<button type="button" data-track="download" @click="handleDownload('ios-lite', currentHero)">
<button type="button" @click="handleDownload('ios-lite', currentHero)">
iOS 轻量版
</button>
<button type="button" data-track="download" @click="handleDownload('android', currentHero)">
<button type="button" @click="handleDownload('android', currentHero)">
Android
</button>
</div>
@@ -306,7 +290,7 @@ const {
<span class="kicker">作品精选</span>
<h2 class="h2">本周热播榜</h2>
</div>
<a class="link" href="#" data-track="download" @click.prevent="handleDownload()">
<a class="link" href="#" @click.prevent="handleDownload()">
查看全部作品
<svg class="ic"><use href="#i-arrow-right" /></svg>
</a>
@@ -315,9 +299,9 @@ const {
<!-- 题材筛选器点击不切换筛选统一触发下载 -->
<div class="filters" style="margin-top: 32px" data-reveal>
<div class="seg lg">
<button class="on" data-track="download" @click="handleFilterClick">全部</button>
<button data-track="download" @click="handleFilterClick">短剧</button>
<button data-track="download" @click="handleFilterClick">影集</button>
<button class="on" @click="handleFilterClick">全部</button>
<button @click="handleFilterClick">短剧</button>
<button @click="handleFilterClick">影集</button>
</div>
<span class="sep"></span>
<div class="gchips">
@@ -325,7 +309,7 @@ const {
v-for="g in GENRES"
:key="g"
class="chip"
data-track="download"
@click="handleFilterClick"
>
{{ g }}
@@ -344,7 +328,7 @@ const {
>
<div class="rc">
<span class="rank">{{ i + 1 }}</span>
<div class="poster" data-track="download" @click="handleDownload(undefined, d)">
<div class="poster" @click="handleDownload(undefined, d)">
<img :src="assetUrl(d.cover)" :alt="d.t" loading="lazy" />
<span class="badge" :class="{ dark: d.kind === '影集' }">
{{ d.kind === '影集' ? '影集' : '原创' }}
@@ -367,7 +351,7 @@ const {
<!-- 最新上架 -->
<div class="subhead" data-reveal>
<h3>最新上架</h3>
<a class="link" href="#" data-track="download" @click.prevent="handleDownload()">
<a class="link" href="#" @click.prevent="handleDownload()">
查看更多
<svg class="ic"><use href="#i-chevron-right" /></svg>
</a>
@@ -492,7 +476,7 @@ const {
<template v-if="hasIosShopPackage">
<button
class="btn primary lg has-rec"
data-track="download"
type="button"
@click="handleDownload('ios-store')"
>
@@ -502,7 +486,7 @@ const {
</button>
<button
class="btn outline lg"
data-track="download"
type="button"
@click="handleDownload('ios-lite')"
>
@@ -513,7 +497,7 @@ const {
<button
v-else
class="btn primary lg"
data-track="download"
type="button"
@click="handleDownload('ios-lite')"
>
@@ -522,7 +506,7 @@ const {
</button>
<button
class="btn outline lg"
data-track="download"
type="button"
@click="handleDownload('android')"
>
@@ -549,11 +533,6 @@ const {
<div class="left" data-reveal>
<span class="kicker">常见问题</span>
<h2 class="h2">关于黄果<br />你想知道的</h2>
<p>还有其他问题欢迎随时联络我们</p>
<a class="link brand" :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener">
联络我们
<svg class="ic"><use href="#i-arrow-right" /></svg>
</a>
</div>
<div class="acc" data-reveal>
<div
@@ -648,29 +627,11 @@ const {
<img class="ico" :src="assetUrl('logo-icon-activity.png')" alt="黄果短剧" />
<img class="wm" :src="assetUrl('wordmark-white-2line.png')" alt="黄果短剧 huangguo.com" />
</div>
<div class="soc">
<a class="circ" :href="SOCIAL_LINKS.x" target="_blank" rel="noopener" aria-label="X">
<svg class="ic brandico"><use href="#i-si-x" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener" aria-label="Telegram">
<svg class="ic brandico"><use href="#i-si-telegram" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.instagram" target="_blank" rel="noopener" aria-label="Instagram">
<svg class="ic brandico"><use href="#i-si-instagram" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.tiktok" target="_blank" rel="noopener" aria-label="TikTok">
<svg class="ic brandico"><use href="#i-si-tiktok" /></svg>
</a>
<a class="circ" :href="SOCIAL_LINKS.youtube" target="_blank" rel="noopener" aria-label="YouTube">
<svg class="ic brandico"><use href="#i-si-youtube" /></svg>
</a>
</div>
<div class="fl">
<a href="#works" @click.prevent="switchView('landing', '#works')">作品精选</a>
<a href="#about" @click.prevent="switchView('landing', '#about')">关于黄果</a>
<a href="#faq" @click.prevent="switchView('landing', '#faq')">常见问题</a>
<a href="#" data-track="download" @click.prevent="handleDownload()">下载 APP</a>
<a :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener">联系我们</a>
<a href="#" @click.prevent="handleDownload()">下载 APP</a>
</div>
<div class="legal">
<b>18 U.S.C. 2257 记录保存要求合规声明</b>
@@ -698,7 +659,7 @@ const {
<div class="c">
<b>扫码下载黄果短剧 App</b>
<span>支援 iOS / Android</span>
<button class="btn primary" data-track="download" @click="handleDownload()">
<button class="btn primary" @click="handleDownload()">
<svg class="ic"><use href="#i-download" /></svg>
下载 APP
</button>
@@ -768,7 +729,7 @@ const {
<b>iOS 轻量版</b>
<span> Safari 打开本页允许下载描述文件后依引导安装</span>
</div>
<button class="btn outline sm" data-track="download" @click="handleCopyIosLiteLink">
<button class="btn outline sm" @click="handleCopyIosLiteLink">
<svg class="ic"><use href="#i-copy" /></svg>
复制链接
</button>
@@ -779,7 +740,7 @@ const {
<b>iOS 商店版</b>
<span>前往 App Store 下载安装适合无法安装描述文件的设备</span>
</div>
<button class="btn outline sm" data-track="download" @click="handleIosStoreAction">
<button class="btn outline sm" @click="handleIosStoreAction">
<svg class="ic"><use href="#i-external-link" /></svg>
前往商店
</button>
@@ -790,7 +751,7 @@ const {
<b>Android </b>
<span>下载 APK 安装包允许安装未知来源应用后打开</span>
</div>
<button class="btn outline sm" data-track="download" @click="handlePcAndroidDownload">
<button class="btn outline sm" @click="handlePcAndroidDownload">
<svg class="ic"><use href="#i-download" /></svg>
下载 APK
</button>
@@ -883,7 +844,7 @@ const {
<div class="bar2">
<button
class="btn primary block"
data-track="download"
@click="handleInstallGuideDownload"
>
<svg class="ic brandico"><use :href="installGuideTab === 'ios' ? '#i-si-apple' : '#i-si-android'" /></svg>
@@ -2405,16 +2366,6 @@ footer .lockup img.wm {
width: auto;
}
footer .soc {
display: flex;
gap: 14px;
}
footer .soc .circ {
width: 44px;
height: 44px;
}
footer .fl {
display: flex;
gap: 32px;
+26 -47
View File
@@ -2,11 +2,9 @@ import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useRoute } from 'vue-router'
import rawDramas from '~/data/b-land/dramas.json'
import rawFeatured from '~/data/b-land/featured.json'
import { SOCIAL_LINKS } from '~/config/variants'
import { useAppDownload } from '~/utils/useAppDownload'
import { copyToClipboard } from '~/utils/clipboard'
import { getLandingVersion } from '~/utils/landingVersion'
import { useEventTracker } from '~/utils/useEventTracker'
export interface Drama {
t: string
@@ -83,10 +81,6 @@ const FAQS = [
q: '内容多久更新一次?',
a: '每日更新。短剧与影集双轨并行,新作上线会同步显示在「最新上架」与 App 首页。',
},
{
q: '如何联系客服或洽谈合作?',
a: '可通过页尾「联系我们」或 App 内在线客服联络;商务合作请留下联系方式,我们会在 1 至 2 个工作日内回复。',
},
]
const assetUrl = (path: string) => `/b-land/assets/${path}`
@@ -280,11 +274,16 @@ export function useLandingB(mode: LandingBMode) {
return url.toString()
}
const { buttonList, isIos, download, iosInstallUrl, isSocialInApp, guide, init: initDownload } =
useAppDownload()
useEventTracker()
const hasIosShopPackage = computed(() => !!buttonList.value.shopIosDownUrl)
const {
buttonList,
isIos,
download,
hasIosStore: hasIosShopPackage,
iosInstallUrl,
isSocialInApp,
guide,
init: initDownload,
} = useAppDownload()
function closeGuide() {
guide.value = null
@@ -433,56 +432,37 @@ export function useLandingB(mode: LandingBMode) {
handleCopyIosLiteLink()
return
}
// 引导页内再次点击:同步跳转,避免 setTimeout 丢掉 iOS 用户手势导致「无反应」
const url = iosInstallUrl.value || buttonList.value.iosDownUrl
if (url) {
window.location.href = url
return
}
if (!buttonList.value.storeIosDownUrl) {
showToast('下载链接获取中,请稍后重试')
return
}
download(0)
download(0, 'ios').then((ok) => {
if (!ok) showToast('下载链接获取中,请稍后重试')
})
return
}
const apk =
buttonList.value.hkAndroidDownUrl ||
buttonList.value.channelApkAndDownUrl ||
buttonList.value.androidDownUrl ||
buttonList.value.storeAndroidDownUrl
if (apk) {
window.location.href = apk
return
}
showToast('下载链接获取中,请稍后重试')
download(0, 'android').then((ok) => {
if (!ok) showToast('下载链接获取中,请稍后重试')
})
}
function handleCopyIosLiteLink() {
const url = iosInstallUrl.value || window.location.href
copyToClipboard(url)
copyToClipboard(iosInstallUrl.value)
showToast('已复制链接')
}
function handleIosStoreAction() {
const url = buttonList.value.shopIosDownUrl
if (url) {
// iOS 真机 window.open 常被拦截,H5 用同步跳转
if (isMobileMode) window.location.href = url
else window.open(url, '_blank')
} else {
if (!hasIosShopPackage.value) {
showToast('商店版链接获取中,请扫码下载')
return
}
// 链接由 bottonclick 返回,拿到后同页跳转(异步后 window.open 会被拦截)
download(1, 'ios').then((ok) => {
if (!ok) showToast('商店版链接获取中,请扫码下载')
})
}
function handlePcAndroidDownload() {
if (buttonList.value.androidDownUrl || buttonList.value.hkAndroidDownUrl) {
const url = buttonList.value.hkAndroidDownUrl || buttonList.value.androidDownUrl!
window.location.href = url
} else {
showToast('正在获取 Android 安装包,请扫码下载')
}
download(0, 'android').then((ok) => {
if (!ok) showToast('正在获取 Android 安装包,请扫码下载')
})
}
function handleOpenAppDeepLink() {
@@ -616,7 +596,6 @@ export function useLandingB(mode: LandingBMode) {
})
return {
SOCIAL_LINKS,
DRAMAS,
FEATURED,
GENRES,
-729
View File
@@ -1,729 +0,0 @@
<template>
<div class="m-page">
<!-- 整页背景背景-4hero 紫光 + WHO 人物列 + STAY 剧照一整张 -->
<div class="page-bg">
<picture>
<source type="image/webp" :srcset="pageBgW" />
<img :src="pageBg" alt="" />
</picture>
</div>
<!-- Hero -->
<section class="hero">
<swiper
class="hero-bg"
:modules="modules"
:speed="500"
:loop="true"
:grab-cursor="true"
:allow-touch-move="true"
:autoplay="{ delay: 4000, disableOnInteraction: false }"
@slide-change="onSlide"
>
<swiper-slide v-for="(item, i) in PERSONS" :key="i">
<picture>
<source type="image/webp" :srcset="item.w" />
<img :src="item.p" alt="" />
</picture>
</swiper-slide>
</swiper>
<!-- 顶部 logo -->
<div class="top">
<picture class="logo">
<source type="image/webp" :srcset="logoMW" />
<img :src="logoM" alt="黄果短剧" />
</picture>
</div>
<!-- 底部文案 + 圆点 + CTA -->
<div class="hero-bottom">
<div class="theme">
<h1 class="theme-title">每一帧<br /><span>都能</span><span class="hl">解你的色</span></h1>
<p class="theme-desc">黄果短剧每日更新海量高清成人短剧成人影集在线观看禁忌诱惑霸总逆袭穿越精彩黄剧尽在黄果</p>
</div>
<div class="dots">
<i v-for="i in PERSONS.length" :key="i" class="dot" :class="{ active: i - 1 === activeIndex }" />
</div>
<!-- Figma 743:561正式发布胶囊 + 下载按钮间距 25 -->
<div class="download-block">
<p class="launch-pill" role="status">
<span class="launch-dot" aria-hidden="true" />
<span>正式发布 · Officially launched</span>
</p>
<template v-if="isIos">
<div v-if="buttonList.shopIosDownUrl" class="cta-row">
<a class="cta cta-lite" data-track="download" role="button" @click="download(0)">轻量版下载</a>
<a class="cta cta-recommend" data-track="download" role="button" @click="download(1)">
商店包下载
<img class="cta-badge" :src="badgeRecommend" alt="推荐" width="56" height="56" />
</a>
</div>
<a v-else class="cta" data-track="download" role="button" @click="download(0)">轻量版下载</a>
</template>
<a v-else class="cta" data-track="download" role="button" @click="download()">{{ downloadLabel }}</a>
</div>
<div class="ctas">
<p class="install-tip">*下载后开启飞行模式安装*安装包报毒解决方案</p>
<a class="cta-dark" :href="SOCIAL_LINKS.x" target="_blank" rel="noopener">
<svg class="cta-ico" viewBox="0 0 60 60" aria-hidden="true">
<path
d="M38.7806 16.2891L43.4325 16.2891L33.2695 27.9047L45.2255 43.7109L35.8641 43.7109L28.5319 34.1245L20.1422 43.7109L15.4875 43.7109L26.3578 31.2867L14.8884 16.2891L24.4875 16.2891L31.1152 25.0514L38.7806 16.2891ZM37.148 40.9266L39.7256 40.9266L23.0869 18.9272L20.3207 18.9272L37.148 40.9266Z"
fill="currentColor"
/>
</svg>
<span>关注 X @huangguodrama</span>
</a>
<a class="cta-dark" href="https://t.me/huangguoguoguo" target="_blank" rel="noopener">
<svg class="cta-ico" viewBox="0 0 36 36" aria-hidden="true">
<path
d="M25.325 27.8352L29.0351 10.3477C29.3628 8.80869 28.4792 8.20356 27.4701 8.58103L5.66737 16.9839C4.17814 17.5662 4.2046 18.3974 5.41537 18.7749L10.9922 20.5161L23.9379 12.3632C24.5441 11.9598 25.0994 12.1874 24.6463 12.5908L14.174 22.053L13.7706 27.8077C13.9899 27.8091 14.2065 27.7597 14.4034 27.6632C14.6004 27.5668 14.7723 27.426 14.9056 27.2518L17.63 24.6275L23.2825 28.7908C24.3175 29.3732 25.0491 29.0693 25.3276 27.8315L25.325 27.8352Z"
fill="currentColor"
/>
</svg>
<span>加入 TG @huangguoguoguo</span>
</a>
</div>
</div>
</section>
<!-- WHO WE ARE -->
<section class="who">
<div class="who-inner">
<p class="eyebrow">WHO WE ARE</p>
<h2 class="who-title">黄果一开全射出来<br />这是我们对每一位用户的承诺</h2>
<div class="paras">
<p>上千部精彩作品任你挑选热门主题一应俱全</p>
<p>从暧昧拉扯到禁忌反转从碎片时间到深夜独处随时打开都能快速沉浸越看越停不下来</p>
<p>现在就打开这颗黄果尝尝里面究竟藏着什么让人上瘾的滋味</p>
</div>
<div class="cards">
<div class="card">
<h3>多元齐全</h3>
<p>多元的题材海量的短剧与影集我们把最好看的留给你</p>
</div>
<div class="card">
<h3>天天上新</h3>
<p>持续更新的内容节奏每天打开都有新惊喜追剧永不断档</p>
</div>
<div class="card">
<h3>沉浸体验</h3>
<p>高清画质震撼音效每个动作每个画面全心投入剧情</p>
</div>
</div>
</div>
</section>
<!-- STAY TUNED -->
<section class="stay">
<div class="stay-inner">
<p class="eyebrow">STAY TUNED</p>
<h2 class="stay-title">第一时间 不错过上新</h2>
<div class="paras2 center">
<p>加入官方 Telegram 频道与官方社群</p>
<p>早鸟优惠等促销活动即时掌握</p>
</div>
<a class="tg-btn" :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener">
<img class="tg-icon" :src="iconTelegram" alt="" />
<span>加入 Telegram 频道</span>
</a>
<div class="socials">
<a class="social" :href="SOCIAL_LINKS.x" target="_blank" rel="noopener" aria-label="X"><img :src="iconX" alt="X" /></a>
<a class="social" :href="SOCIAL_LINKS.instagram" target="_blank" rel="noopener" aria-label="Instagram"><img :src="iconInstagram" alt="Instagram" /></a>
<a class="social" :href="SOCIAL_LINKS.tiktok" target="_blank" rel="noopener" aria-label="TikTok"><img :src="iconTiktok" alt="TikTok" /></a>
</div>
<div class="foot">
<picture class="logo">
<source type="image/webp" :srcset="logoMW" />
<img :src="logoM" alt="黄果短剧" />
</picture>
<p>© 2026 黄果短剧 HUANGGUO. All rights reserved.</p>
</div>
</div>
</section>
<!-- 下载后的安装引导Twitter/IG/TikTok 复制到 Safari其它 Figma 三步轻量版安装引导 -->
<IosSafariOpenGuide v-if="isSocialInApp" :show="guide === 'ios'" :url="iosInstallUrl" @close="guide = null" />
<IosInstallGuide v-else :show="guide === 'ios'" @close="guide = null" />
<AndroidInstallTip :show="guide === 'android'" @close="guide = null" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay } from 'swiper/modules'
import type { Swiper as SwiperClass } from 'swiper/types'
import 'swiper/css'
import { SOCIAL_LINKS } from '~/config/variants'
import { useAppDownload } from '~/utils/useAppDownload'
import IosInstallGuide from '~/components/mobile/IosInstallGuide.vue'
import IosSafariOpenGuide from '~/components/mobile/IosSafariOpenGuide.vue'
import AndroidInstallTip from '~/components/mobile/AndroidInstallTip.vue'
import logoM from '~/assets/image/hg/common/logo-m.png'
import logoMW from '~/assets/image/hg/common/logo-m.webp'
import person1 from '~/assets/image/hg/mobile/person1.png'
import person1W from '~/assets/image/hg/mobile/person1.webp'
import person2 from '~/assets/image/hg/mobile/person2.png'
import person2W from '~/assets/image/hg/mobile/person2.webp'
import person3 from '~/assets/image/hg/mobile/person3.png'
import person3W from '~/assets/image/hg/mobile/person3.webp'
import person4 from '~/assets/image/hg/mobile/person4.png'
import person4W from '~/assets/image/hg/mobile/person4.webp'
import person5 from '~/assets/image/hg/mobile/person5.png'
import person5W from '~/assets/image/hg/mobile/person5.webp'
import pageBg from '~/assets/image/hg/mobile/pagebg.jpg'
import pageBgW from '~/assets/image/hg/mobile/pagebg.webp'
import iconTelegram from '~/assets/image/hg/common/icon-telegram.svg'
import iconX from '~/assets/image/hg/common/icon-x.svg'
import iconInstagram from '~/assets/image/hg/common/icon-instagram.svg'
import iconTiktok from '~/assets/image/hg/common/icon-tiktok.svg'
import badgeRecommend from '~/assets/image/hg/mobile/cta/badge-recommend.png'
const modules = [Autoplay]
const activeIndex = ref(0)
// Hero 五张人物图(透明羽化边,叠在固定颜色层上轮播)
const PERSONS = [
{ p: person1, w: person1W },
{ p: person2, w: person2W },
{ p: person3, w: person3W },
{ p: person4, w: person4W },
{ p: person5, w: person5W },
]
function onSlide(sw: SwiperClass) {
activeIndex.value = sw.realIndex
}
// 下载:进页面即拉取下载地址(appId=209,整包 query 作为渠道交后端),点击按平台分流
const { isIos, isSocialInApp, guide, iosInstallUrl, buttonList, init: initDownload, download } = useAppDownload()
const downloadLabel = isIos ? '立即下载 iOS 版' : '立即下载 Android 版'
onMounted(() => {
initDownload()
})
</script>
<style lang="scss" scoped>
@use 'sass:math';
// 设计稿 375 宽,main.js 设 1rem = 屏宽/7.5375 屏 = 50px);故设计 px → rem 为 /50
@function r($px) {
@return math.div($px, 50) * 1rem;
}
img {
-webkit-user-drag: none;
}
.m-page {
user-select: none;
position: fixed;
inset: 0;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
background: #1a0d1f;
.eyebrow {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 18px;
line-height: 1;
color: #ffb92b;
}
.paras {
display: flex;
flex-direction: column;
gap: r(12);
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: r(14);
line-height: 1.6;
color: #fff;
}
&.center p {
text-align: center;
}
}
.paras2 {
display: flex;
flex-direction: column;
gap: r(24);
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: r(16);
line-height: 1.6;
color: #fff;
}
&.center p {
text-align: center;
}
}
}
// 整页背景:一整张 背景-4,按 375 宽自然高度铺满、随内容滚动,三段透明叠在上面
.page-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 0;
img {
width: 100%;
height: auto;
display: block;
}
}
// ① Hero
.hero {
position: relative;
width: 100%;
height: 100vh;
height: 100dvh;
overflow: hidden;
// 人物轮播(前景,顶部对齐;紫光由整页背景 .page-bg 提供)
.hero-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
height: r(560);
z-index: 1;
}
:deep(.swiper-slide) {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center top;
}
}
.top {
position: absolute;
top: 0;
left: 0;
right: 0;
height: r(70);
padding: 0 r(24);
display: flex;
align-items: center;
z-index: 3;
pointer-events: none;
}
// 文案层整块盖在轮播上,默认不接触摸(透给下面的 swiper 横滑),只给按钮恢复
.hero-bottom {
position: absolute;
left: 0;
right: 0;
bottom: r(35);
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
.cta,
.cta-dark,
.cta-row,
.download-block {
pointer-events: auto;
}
.theme {
align-self: flex-start;
margin: 0 r(40);
width: r(295);
display: flex;
flex-direction: column;
gap: r(16);
.theme-title {
color: #FFF;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-size: 36px;
font-style: normal;
font-weight: 900;
line-height: normal;
.hl {
background: linear-gradient(90deg, #F9D742 -40%, #FF3276 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-size: 36px;
font-style: normal;
font-weight: 900;
line-height: normal;
}
}
.theme-desc {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: r(15);
line-height: 1.6;
color: #fff;
}
}
.dots {
// 标题底 y518 → 圆点 y546 ≈ 28
margin-top: r(28);
display: flex;
align-items: center;
gap: r(10);
.dot {
width: r(8);
height: r(8);
border-radius: 50%;
background: rgba(255, 255, 255, 0.35);
transition: all 0.3s ease;
&.active {
width: r(20);
border-radius: r(4);
background: #fff;
}
}
}
// Figma 743:561:胶囊(29) + 间距25 + 按钮(39) = 93
.download-block {
position: relative;
// 圆点底 y554 → 按钮组 y573 ≈ 19
margin-top: r(19);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: r(25);
overflow: visible;
}
// Figma 743:562「正式发布 · Officially launched」
.launch-pill {
display: inline-flex;
align-items: center;
gap: r(6);
margin: 0;
padding: r(6) r(10);
box-sizing: border-box;
border-radius: r(999);
border: 1px solid #ffbb2c;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 700;
font-size: r(12);
line-height: normal;
color: #ffba2b;
white-space: nowrap;
pointer-events: none;
.launch-dot {
width: r(5);
height: r(5);
border-radius: 50%;
background: #ffba2b;
flex-shrink: 0;
}
}
.ctas {
margin-top: r(12);
display: flex;
flex-direction: column;
align-items: center;
gap: r(12);
overflow: visible;
}
// 下载按钮下的安装提示(文案与参考版一致)
.install-tip {
margin: 0;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: r(12);
line-height: 1.4;
color: rgba(255, 255, 255, 0.72);
white-space: nowrap;
}
// iOS 双下载:左白「轻量版」右黄「商店包」(Figma 743:565,宽243 / 左右66
.cta-row {
display: flex;
align-items: stretch;
gap: r(18);
width: r(243);
margin: 0;
overflow: visible;
.cta {
flex: 1 1 0;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
height: r(39);
padding: r(8) r(12);
box-sizing: border-box;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: r(16);
white-space: nowrap;
}
}
.cta-recommend {
position: relative;
// Figma 777:794:相对按钮组 top17 / 按钮 top54 → 相对按钮 top -37
.cta-badge {
position: absolute;
top: r(-37);
right: r(-32);
width: r(56);
height: r(56);
display: block;
pointer-events: none;
z-index: 2;
}
}
.cta {
padding: r(10) r(28);
border-radius: r(999);
background: #ffb92b;
box-shadow: 0 r(8) r(16) rgba(255, 187, 43, 0.25);
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 525;
font-size: r(14);
line-height: normal;
color: #2a0a2e;
cursor: pointer;
}
.cta-lite {
background: #fff;
box-shadow: none;
}
.cta-dark {
display: inline-flex;
align-items: center;
gap: r(8);
padding: r(10) r(20);
border-radius: r(999);
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 525;
font-size: r(14);
line-height: normal;
color: #fff;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
.cta-ico {
width: r(20);
height: r(20);
display: block;
}
}
}
}
// logo 图(hero 顶部 + footer 共用):参考站手机版横图(图标 + 黄果短劇 + 副标),原图 252×60,按 30px 高 = 2x 整数缩放
.logo {
display: block;
img {
height: r(30);
width: auto;
display: block;
}
}
// WHO / STAY:本身透明,整页背景 .page-bg 在底层透出,内容叠在上面
.who,
.stay {
position: relative;
width: 100%;
z-index: 1;
}
// ② WHO WE ARE
.who-inner {
position: relative;
z-index: 1;
padding: r(36) r(30);
display: flex;
flex-direction: column;
gap: r(20);
.who-title {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 900;
font-size: r(30);
line-height: 1.4;
color: #fff;
}
.cards {
margin-top: r(4);
display: flex;
flex-direction: column;
gap: r(14);
.card {
display: flex;
flex-direction: column;
gap: r(5);
padding: r(18) r(12);
align-self: stretch;
border-radius: r(24);
border: 1px solid rgba(255, 210, 63, 0.3);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
h3 {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 700;
font-size: r(20);
line-height: 1.2;
color: #ffb92b;
margin-bottom: 12px;
}
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 12px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.85);
}
}
}
}
// ③ STAY TUNED
.stay-inner {
position: relative;
z-index: 1;
padding: r(36) r(30);
display: flex;
flex-direction: column;
align-items: center;
gap: r(24);
.eyebrow {
text-align: center;
}
.stay-title {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 900;
font-size: r(28);
line-height: 1.2;
color: #fff;
text-align: center;
}
.tg-btn {
display: inline-flex;
align-items: center;
gap: r(6);
padding: 6px 22px;
border-radius: r(999);
background: #229ed9;
box-shadow: 0 r(8) r(16) rgba(34, 158, 217, 0.25);
text-decoration: none;
margin: 12px 0 12px 0;
.tg-icon {
width: r(36);
height: r(36);
display: block;
}
span {
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 700;
font-size: 14px;
line-height: 1;
color: #fff;
white-space: nowrap;
}
}
.socials {
display: flex;
gap: r(18);
.social {
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
img {
width: 36px;
height: 36px;
display: block;
}
}
}
.foot {
margin-top: r(8);
display: flex;
flex-direction: column;
align-items: center;
gap: r(12);
.logo {
justify-content: center;
}
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 12px;
line-height: 1.5;
color: #fff;
text-align: center;
}
}
}
</style>
-51
View File
@@ -1,51 +0,0 @@
<template>
<div class="pc-deck">
<!-- A hero 轮播 -->
<section class="deck-page">
<PcLanding />
</section>
<!-- B WHO WE ARE -->
<section class="deck-page">
<PcLandingB />
</section>
<!-- 分享页 -->
<section class="deck-page">
<Share />
</section>
</div>
</template>
<script lang="ts" setup>
import PcLanding from '~/components/landing/PcLanding.vue'
import PcLandingB from '~/components/landing/PcLandingB.vue'
import Share from '~/pages/share.vue'
</script>
<style lang="scss" scoped>
.pc-deck {
width: 100%;
height: 100vh;
overflow-y: scroll;
overflow-x: hidden;
// 整屏强制翻页:每屏吸附、一次滚动只切一屏
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
// 隐藏滚动条
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.deck-page {
width: 100%;
height: 100vh;
overflow: hidden;
scroll-snap-align: start;
scroll-snap-stop: always;
}
</style>
-358
View File
@@ -1,358 +0,0 @@
<template>
<div class="fit" ref="fitEl">
<div class="hg-pc">
<!-- 五页轮播只切换背景大图 -->
<swiper
class="hero"
:modules="modules"
effect="fade"
:speed="800"
:loop="true"
:allow-touch-move="false"
:autoplay="{ delay: 4000, disableOnInteraction: false }"
@swiper="onSwiper"
>
<swiper-slide v-for="(item, i) in PC_VARIANTS" :key="i">
<div class="slide-bg">
<!-- 首张是首屏第一眼提优先级其余保持默认避免轮播切过去时还没下完 -->
<picture>
<source type="image/webp" :srcset="item.bgW" />
<img :src="item.bg" alt="" :fetchpriority="i === 0 ? 'high' : 'auto'" />
</picture>
</div>
</swiper-slide>
</swiper>
<!-- 固定 chrome 覆盖层5 页共用居中 1920 舞台 -->
<div class="stage">
<!-- 顶部 LOGO白色横版 -->
<div class="top">
<picture>
<source type="image/webp" :srcset="logoHW" />
<img class="logo-h" :src="logoH" alt="黄果短剧" />
</picture>
</div>
<!-- 主文案 -->
<div class="theme">
<h1 class="theme-title">
<span class="line">每一帧</span>
<span class="line"><span>都能</span><span class="hl">解你的色</span></span>
</h1>
<p class="theme-desc">黄果短剧每日更新海量高清成人短剧成人影集在线观看禁忌诱惑霸总逆袭穿越精彩黄剧尽在黄果</p>
</div>
<!-- 行动区 = 二维码框 = 扫码说明Figma 659:2 二维码部分 -->
<div class="cta">
<div class="qr-frame">
<div class="qr-inner">
<DesktopQrcode :size="164" />
</div>
</div>
<div class="qr-copy">
<p class="qr-title">使用相机或者浏览器扫码下载</p>
<p class="qr-tip">
安装提示下载好安装包以后请断开网络由于行业原因
APP会被某些杀毒软件视为病毒请放心下载使用本APP有广告和会员收益安装本APP不会对您的手机造成任何损害
</p>
</div>
</div>
<!-- 右下角封面条整条图固定不换5 张封面各盖一个热区点击切到对应轮播页 -->
<div class="covers">
<picture>
<source type="image/webp" :srcset="PC_COVERS.srcW" />
<img :src="PC_COVERS.src" alt="" fetchpriority="high" />
</picture>
<button
v-for="(item, i) in PC_VARIANTS"
:key="i"
class="cover-hit"
:style="{ left: `${i * COVER_STEP}px` }"
type="button"
:aria-label="`查看第 ${i + 1} 部作品`"
@click="goSlide(i)"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, shallowRef, onMounted, onBeforeUnmount } from 'vue'
import { Swiper, SwiperSlide } from 'swiper/vue'
import { Autoplay, EffectFade } from 'swiper/modules'
import type { Swiper as SwiperClass } from 'swiper'
import 'swiper/css'
import 'swiper/css/effect-fade'
import { PC_VARIANTS, PC_COVERS } from '~/config/variants'
import DesktopQrcode from '~/components/desktop/qrcode.vue'
import logoH from '~/assets/image/hg/common/logo-h.png'
import logoHW from '~/assets/image/hg/common/logo-h.webp'
const modules = [Autoplay, EffectFade]
// 封面条几何(同 Figma「轮动」:封面 110 宽、间距 14),热区据此逐张定位
const COVER_STEP = 124
const swiperRef = shallowRef<SwiperClass | null>(null)
function onSwiper(swiper: SwiperClass) {
swiperRef.value = swiper
}
// loop 模式下按真实索引跳转,跳完 autoplay 继续(disableOnInteraction: false
function goSlide(i: number) {
swiperRef.value?.slideToLoop(i)
}
// 1920×1080 画布按视口 cover 缩放(不再用 body.zoom,避免和翻页滚动冲突)
const fitEl = ref<HTMLElement | null>(null)
function fit() {
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080)
fitEl.value?.style.setProperty('--fit', String(s))
}
onMounted(() => {
fit()
window.addEventListener('resize', fit)
})
onBeforeUnmount(() => window.removeEventListener('resize', fit))
</script>
<style lang="scss" scoped>
img {
-webkit-user-drag: none;
}
.fit {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #000;
}
.hg-pc {
user-select: none;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
// 轮播铺满整屏(背景全幅,cover 独立铺满,不随舞台缩放)
.hero {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
}
:deep(.swiper-slide) {
width: 100%;
height: 100%;
}
.slide-bg {
position: absolute;
inset: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
// 固定 chrome:居中 1920 舞台,超过 1920 不左偏
.stage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(var(--fit, 1));
width: 1920px;
height: 1080px;
z-index: 2;
pointer-events: none;
}
// 顶部 LOGO
.top {
position: absolute;
left: 220px;
top: 38px;
.logo-h {
width: 220px;
height: 77px;
object-fit: contain;
display: block;
}
}
.theme {
position: absolute;
left: 244px;
top: 323px;
width: 630px;
display: flex;
flex-direction: column;
gap: 24px;
.theme-title {
margin: 0;
display: flex;
flex-direction: column;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 900;
font-size: 105px;
line-height: 1.44;
white-space: nowrap;
color: #fff;
.line {
display: flex;
gap: 10px;
}
.hl {
background: linear-gradient(90deg, #f9d742, #ff3276);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
}
.theme-desc {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 24px;
line-height: 1.42;
color: #fff;
}
}
// 行动区:二维码 + 文案(Figma 659:2
.cta {
position: absolute;
left: 244px;
top: 777px;
display: flex;
align-items: center;
gap: 31px;
pointer-events: auto;
// 220 圆角框:渐变描边(稿面自上而下橙 → 玫红)
.qr-frame {
flex-shrink: 0;
width: 220px;
height: 220px;
border-radius: 24px;
padding: 2px;
box-sizing: border-box;
background: linear-gradient(180deg, #f9d742 0%, #cb865f 45%, #f75562 100%);
}
.qr-inner {
width: 100%;
height: 100%;
border-radius: 22px;
background: #000;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
:deep(.qrContent) {
height: 188px;
.left {
width: 188px;
height: 188px;
margin-right: 0;
border-radius: 12px;
overflow: hidden;
.qr {
width: 188px;
height: 188px;
padding: 12px;
box-sizing: border-box;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
.qrcode {
padding: 0;
}
}
}
}
}
.qr-copy {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
font-family: 'PingFang SC', 'Noto Serif SC', 'Songti SC', sans-serif;
font-style: normal;
line-height: 1.6;
}
.qr-title {
margin: 0;
font-weight: 500;
font-size: 24px;
color: #f75562;
white-space: nowrap;
}
.qr-tip {
margin: 0;
width: 505px;
font-weight: 500;
font-size: 18px;
color: #fff;
}
}
// 右下角封面条(5 页共用同一张)
.covers {
position: absolute;
left: 1184px;
top: 778px;
width: 606px;
height: 152px;
pointer-events: auto;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
// 盖在整条图上的 5 个点击热区,与图内封面一一对齐
.cover-hit {
position: absolute;
top: 0;
width: 110px;
height: 152px;
padding: 0;
border: 1px solid transparent;
border-radius: 12px;
background: transparent;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
&:hover {
border-color: rgba(255, 255, 255, 0.75);
background: rgba(255, 255, 255, 0.16);
}
}
}
}
</style>
-223
View File
@@ -1,223 +0,0 @@
<template>
<div class="fit" ref="fitEl">
<div class="hg-pc-b">
<!-- 整页背景倾斜剧照拼贴 + 暗化遮罩已在设计中烘焙为一张图 -->
<div class="bg">
<!-- 第二屏资源滚到才加载不占首屏带宽视口内时 lazy 会立即加载单独访问不受影响 -->
<picture>
<source type="image/webp" :srcset="bgW" />
<img :src="bg" alt="" loading="lazy" fetchpriority="low" />
</picture>
</div>
<!-- 居中内容左侧介绍 + 右侧三卡 -->
<div class="stage">
<div class="content">
<div class="intro">
<div class="head">
<p class="eyebrow">WHO WE ARE</p>
<h2 class="title">黄果一开全射出来<br />这是我们对每一位用户的承诺</h2>
</div>
<div class="paras">
<p>上千部精彩作品任你挑选热门主题一应俱全</p>
<p>从暧昧拉扯到禁忌反转从碎片时间到深夜独处随时打开都能快速沉浸越看越停不下来</p>
<p>现在就打开这颗黄果尝尝里面究竟藏着什么让人上瘾的滋味</p>
</div>
</div>
<div class="cards">
<div class="card">
<h3>多元齐全</h3>
<p>多元的题材海量的短剧与影集我们把最好看的留给你</p>
</div>
<div class="card">
<h3>天天上新</h3>
<p>持续更新的内容节奏每天打开都有新惊喜追剧永不断档</p>
</div>
<div class="card">
<h3>沉浸体验</h3>
<p>高清画质震撼音效每个动作每个画面全心投入剧情</p>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import bg from '~/assets/image/hg/pc/b-bg.jpg'
import bgW from '~/assets/image/hg/pc/b-bg.webp'
// 1920×1080 画布按视口 cover 缩放(不再用 body.zoom
const fitEl = ref<HTMLElement | null>(null)
function fit() {
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080)
fitEl.value?.style.setProperty('--fit', String(s))
}
onMounted(() => {
fit()
window.addEventListener('resize', fit)
})
onBeforeUnmount(() => window.removeEventListener('resize', fit))
</script>
<style lang="scss" scoped>
img {
-webkit-user-drag: none;
}
.fit {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #000;
}
.hg-pc-b {
user-select: none;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #000;
.bg {
position: absolute;
inset: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.stage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(var(--fit, 1));
width: 1920px;
height: 1080px;
z-index: 2;
}
.content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 52px;
}
.intro {
width: 840px;
display: flex;
flex-direction: column;
gap: 60px;
.head {
display: flex;
flex-direction: column;
gap: 24px;
}
.eyebrow {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 36px;
line-height: 1;
color: #ffb92b;
}
.title {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 900;
font-size: 60px;
line-height: 1.4;
color: #fff;
}
.paras {
display: flex;
flex-direction: column;
gap: 18px;
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 20px;
line-height: 1.6;
color: #fff;
}
}
}
.cards {
display: flex;
align-items: center;
gap: 20px;
.card {
position: relative;
width: 230px;
box-sizing: border-box;
padding: 70px 35px;
border-radius: 30px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 15px;
// 外框渐变描边:只画 2px 边框一圈(顶部亮金 → 底部渐隐),内部仍是透明玻璃
&::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(180deg, #ffd23f 0%, rgba(255, 210, 63, 0.5) 40%, rgba(255, 210, 63, 0.08) 100%);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
h3 {
margin: 0;
width: 160px;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 700;
font-size: 36px;
line-height: 1.2;
color: #ffb92b;
}
p {
margin: 0;
width: 160px;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 22px;
line-height: 1.5;
text-align: justify;
color: #fff;
opacity: 0.6;
}
}
}
}
</style>
+1 -1
View File
@@ -46,7 +46,7 @@
</div>
</div>
<p class="foot">官方备用地址&nbsp;&nbsp;huangguo.com&nbsp;&nbsp;遇到问题请联系在线客服</p>
<p class="foot">官方备用地址&nbsp;&nbsp;huangguo.com</p>
</div>
</div>
</template>
+2 -2
View File
@@ -1,2 +1,2 @@
// 黄果 App 在落地页后端的 appId(bili 是 126,勿混用)
export const APP_ID = 209
// 黄果 App 在下载接口(blsp01.com /api/stat/version)的 appId
export const APP_ID = 2
-19
View File
@@ -26,22 +26,3 @@ export const PC_VARIANTS: PcVariant[] = [
// 右下角封面条:5 页共用同一张,不随轮播切换
export const PC_COVERS = { src: covers, srcW: coversW }
// 导航链接:官方群组、商务、渠道暂时统一跳转官方 TG,待正式地址再替换
export const NAV_LINKS = {
group: 'https://t.me/huangguoshortdrama',
biz: 'https://t.me/huangguoshortdrama',
channel: 'https://t.me/huangguoshortdrama',
}
// 手机版「立即下载」目标:暂时跳转官方 TG,待 APK / 下载页提供后替换
export const DOWNLOAD_URL = 'https://t.me/huangguoshortdrama'
// 分享页社交链接:Telegram 暂用官方频道,其余待正式账号提供后替换(# 占位)
export const SOCIAL_LINKS = {
telegram: 'https://t.me/huangguoshortdrama',
x: 'https://x.com/huangguodrama',
instagram: 'https://www.instagram.com/huangguoshortdrama/',
tiktok: 'https://www.tiktok.com/@huangguodrama',
youtube: 'https://www.youtube.com/@huangguodrama',
}
+1 -53
View File
@@ -1,61 +1,9 @@
<script setup>
import PcDeck from '~/components/landing/PcDeck.vue'
import MobileLanding from '~/components/landing/MobileLanding.vue'
import LandingB from '~/components/landing-b/LandingB.vue'
import { computed, onBeforeMount, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import { useEventTracker, getSessionTraceId } from '~/utils/useEventTracker'
import { getLandingVersion } from '~/utils/landingVersion'
const route = useRoute()
const isMobile = computed(() =>
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
)
// A/B 分流:与 getButtonList 共用同一份版本(见 landingVersion.ts
const landingVersion = getLandingVersion(route.query, route.path)
const isVersionB = landingVersion === 'B'
const landingComponent = computed(() => {
if (isVersionB) return LandingB
return isMobile.value ? MobileLanding : PcDeck
})
const LANDING_PAGE_ID = isVersionB ? 'JHA-209-B' : 'JHA-209'
const { trackLandingPageView, trackLandingPageClick, generateSessionId } = useEventTracker()
const handleClick = (e) => {
// 优先读最近祖先的 data-track(下载按钮标 download),其次 img alt,最后取文字
const rawText = e?.target?.closest?.('[data-track]')?.getAttribute('data-track') || e?.target?.alt || e?.target?.innerText || ''
const tabKey = rawText.replace(/[^A-Za-z0-9_一-鿿]/g, '_').replace(/_{2,}/g, '_').replace(/^_+|_+$/g, '') || ''
const isDownload = tabKey.toLowerCase() === 'download'
trackLandingPageClick({
landing_page_id: LANDING_PAGE_ID,
tab_key: tabKey,
tab_name: rawText,
click_coordinates_x: e.clientX,
click_coordinates_y: e.clientY,
click_x_percent: Math.round((e.clientX / window.innerWidth) * 100),
click_y_percent: Math.round((e.clientY / window.innerHeight) * 100),
screen_width: window.innerWidth,
screen_height: window.innerHeight,
trace_id: isDownload ? getSessionTraceId() : generateSessionId(),
})
}
onBeforeMount(() => {
trackLandingPageView({ landing_page_id: LANDING_PAGE_ID })
window.addEventListener('click', handleClick)
})
onBeforeUnmount(() => {
window.removeEventListener('click', handleClick)
})
</script>
<template>
<component :is="landingComponent" />
<LandingB />
</template>
<style scoped></style>
-43
View File
@@ -1,48 +1,5 @@
<script setup>
import LandingB from '~/components/landing-b/LandingB.vue'
import { onBeforeMount, onBeforeUnmount } from 'vue'
import { useEventTracker, getSessionTraceId } from '~/utils/useEventTracker'
import { getLandingVersion } from '~/utils/landingVersion'
// 独立 /b 入口:固定 B,保证 getButtonList 带 version=B
getLandingVersion({}, '/b')
const LANDING_PAGE_ID = 'JHA-209-B'
const { trackLandingPageView, trackLandingPageClick, generateSessionId } = useEventTracker()
const handleClick = (e) => {
const rawText =
e?.target?.closest?.('[data-track]')?.getAttribute('data-track') ||
e?.target?.alt ||
e?.target?.innerText ||
''
const tabKey = rawText
.replace(/[^A-Za-z0-9_一-鿿]/g, '_')
.replace(/_{2,}/g, '_')
.replace(/^_+|_+$/g, '') || ''
const isDownload = tabKey.toLowerCase() === 'download'
trackLandingPageClick({
landing_page_id: LANDING_PAGE_ID,
tab_key: tabKey,
tab_name: rawText,
click_coordinates_x: e.clientX,
click_coordinates_y: e.clientY,
click_x_percent: Math.round((e.clientX / window.innerWidth) * 100),
click_y_percent: Math.round((e.clientY / window.innerHeight) * 100),
screen_width: window.innerWidth,
screen_height: window.innerHeight,
trace_id: isDownload ? getSessionTraceId() : generateSessionId(),
})
}
onBeforeMount(() => {
trackLandingPageView({ landing_page_id: LANDING_PAGE_ID })
window.addEventListener('click', handleClick)
})
onBeforeUnmount(() => {
window.removeEventListener('click', handleClick)
})
</script>
<template>
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<!-- iOS 安装引导整页版正常下载流程由 MobileLanding 页内弹层展示本路由保留给 QA / 外链直达 -->
<!-- iOS 安装引导整页版正常下载流程由 LandingB 页内弹层展示本路由保留给 QA / 外链直达 -->
<!-- ?guide=safari 社媒内复制到 Safari引导默认 三步轻量版安装引导 -->
<IosSafariOpenGuide v-if="isSafariOpenGuide" :show="true" @close="goBack" />
<IosInstallGuide v-else :show="true" @close="goBack" />
+1 -1
View File
@@ -31,7 +31,7 @@ import iconRecruit from '~/assets/image/mebile-index-v3/icon-recruit.png'
import btnFast from '~/assets/image/mebile-index-v3/btn-fast.png'
import btnBackup from '~/assets/image/mebile-index-v3/btn-backup.png'
import { getSessionTraceId, renewSessionTraceId } from '~/utils/useEventTracker'
import { getSessionTraceId, renewSessionTraceId } from '~/utils/traceId'
import { copyToClipboard } from '~/utils/clipboard'
import { bottonclick, getButtonList, downloadH5, getCustomerServiceUrl } from '~/api/btn'
+1 -1
View File
@@ -221,7 +221,7 @@ import { bottonclick, getButtonList, downloadH5, getCustomerServiceUrl } from '~
import type { donwloadParam, channel, H5param } from '~/api/interface'
import { onMounted, ref, reactive, version } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { getSessionTraceId, renewSessionTraceId } from '~/utils/useEventTracker'
import { getSessionTraceId, renewSessionTraceId } from '~/utils/traceId'
import { copyToClipboard } from '~/utils/clipboard'
const props = defineProps({
+1 -1
View File
@@ -124,7 +124,7 @@ import logoTopW from '~/assets/image/pc-index/logo-top.webp'
import descW from '~/assets/image/pc-index/desc.webp'
import { getCustomerServiceUrl } from '~/api/btn'
import { getSessionTraceId } from '~/utils/useEventTracker'
import { getSessionTraceId } from '~/utils/traceId'
const route = useRoute()
const URL = ref('')
+1 -1
View File
@@ -101,7 +101,7 @@ import iconUp from '~/assets/image/pc-index2/icon-up.png'
import iconUpW from '~/assets/image/pc-index2/icon-up.webp'
import DesktopQrcode from '~/components/desktop/qrcode.vue'
import { getCustomerServiceUrl } from '~/api/btn'
import { getSessionTraceId } from '~/utils/useEventTracker'
import { getSessionTraceId } from '~/utils/traceId'
const route = useRoute()
const onlineServiceLoading = ref(false)
-250
View File
@@ -1,250 +0,0 @@
<template>
<div class="fit" ref="fitEl">
<div class="hg-share">
<!-- 背景剧照网格拼贴 + 暗化遮罩Figma 导出整图 -->
<div class="bg">
<!-- 作为落地页第三屏时滚到才加载单独访问 /share 时它在视口内lazy 会立即加载 -->
<picture>
<source type="image/webp" :srcset="bgW" />
<img :src="bg" alt="" loading="lazy" fetchpriority="low" />
</picture>
</div>
<!-- 居中内容 -->
<div class="stage">
<div class="content">
<div class="head">
<p class="eyebrow">STAY TUNED</p>
<h1 class="title">第一时间 不错过上新</h1>
</div>
<div class="paras">
<p>加入官方 Telegram 频道与官方社群</p>
<p>早鸟优惠等促销活动即时掌握</p>
</div>
<a class="tg-btn" :href="SOCIAL_LINKS.telegram" target="_blank" rel="noopener">
<img class="tg-icon" :src="iconTelegram" alt="" />
<span>加入 Telegram 频道</span>
</a>
<div class="socials">
<a class="social" :href="SOCIAL_LINKS.x" target="_blank" rel="noopener" aria-label="X">
<img :src="iconX" alt="X" />
</a>
<a class="social" :href="SOCIAL_LINKS.instagram" target="_blank" rel="noopener" aria-label="Instagram">
<img :src="iconInstagram" alt="Instagram" />
</a>
<a class="social" :href="SOCIAL_LINKS.tiktok" target="_blank" rel="noopener" aria-label="TikTok">
<img :src="iconTiktok" alt="TikTok" />
</a>
</div>
<div class="footer">
<picture>
<source type="image/webp" :srcset="logoHW" />
<img class="logo-h" :src="logoH" alt="黄果短剧" />
</picture>
<p>© 2026 黄果短剧 HUANGGUO. All rights reserved.</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { SOCIAL_LINKS } from '~/config/variants'
import bg from '~/assets/image/hg/pc/share-bg.jpg'
import bgW from '~/assets/image/hg/pc/share-bg.webp'
import logoH from '~/assets/image/hg/common/logo-h.png'
import logoHW from '~/assets/image/hg/common/logo-h.webp'
import iconTelegram from '~/assets/image/hg/common/icon-telegram.svg'
import iconX from '~/assets/image/hg/common/icon-x.svg'
import iconTiktok from '~/assets/image/hg/common/icon-tiktok.svg'
import iconInstagram from '~/assets/image/hg/common/icon-instagram.svg'
// 1920×1080 画布按视口 cover 缩放(不再用 body.zoom
const fitEl = ref<HTMLElement | null>(null)
function fit() {
const s = Math.min(window.innerWidth / 1920, window.innerHeight / 1080)
fitEl.value?.style.setProperty('--fit', String(s))
}
onMounted(() => {
fit()
window.addEventListener('resize', fit)
})
onBeforeUnmount(() => window.removeEventListener('resize', fit))
</script>
<style lang="scss" scoped>
img {
-webkit-user-drag: none;
}
.fit {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #000;
}
.hg-share {
user-select: none;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background: #000;
.bg {
position: absolute;
inset: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.stage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(var(--fit, 1));
width: 1920px;
height: 1080px;
z-index: 2;
}
.content {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 60px;
}
.head {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
.eyebrow {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 36px;
line-height: 1;
color: #ffb92b;
}
.title {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 900;
font-size: 60px;
line-height: 1.2;
color: #fff;
text-align: center;
}
}
.paras {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 24px;
line-height: 1;
color: #fff;
text-align: center;
}
}
.tg-btn {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 18px 36px;
border-radius: 999px;
background: #229ed9;
box-shadow: 0 12px 5px rgba(34, 158, 217, 0.1);
text-decoration: none;
cursor: pointer;
.tg-icon {
width: 36px;
height: 36px;
display: block;
}
span {
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 700;
font-size: 24px;
line-height: 1;
color: #fff;
white-space: nowrap;
}
}
.socials {
display: flex;
gap: 24px;
.social {
width: 60px;
height: 60px;
display: inline-flex;
align-items: center;
justify-content: center;
img,
svg {
width: 60px;
height: 60px;
display: block;
}
}
}
.footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
.logo-h {
width: 220px;
height: 77px;
object-fit: contain;
display: block;
}
p {
margin: 0;
font-family: 'Noto Serif SC', 'PingFang SC', 'Songti SC', serif;
font-weight: 400;
font-size: 16px;
line-height: 1.4;
color: #fff;
text-align: center;
}
}
}
</style>
-79
View File
@@ -1,79 +0,0 @@
/**
* 事件上报服务
*
* LandingSDK 由 index.html 异步加载,加载完成前 window.tracker 只是一个排队用的桩函数;
* v2 SDK 不再接受桩队列回放出来的泛型调用 track({ event })(会判「非法上报」丢弃),
* 所以这里等 SDK 就绪后再走具名方法 landingPageView / landingPageClick。
*/
import type { EventCode } from './types'
const SDK_READY_TIMEOUT_MS = 10000
const SDK_POLL_MS = 30
type LandingTracker = ((payload: Record<string, any>) => Promise<unknown>) & {
landingPageView?: (properties?: Record<string, any>) => Promise<unknown>
landingPageClick?: (properties?: Record<string, any>) => Promise<unknown>
}
// 等到 window.tracker 被 App.vue initTracker 替换成真正的 SDK track(带具名方法)为止。
// 注意:SDK 的 track 是 Proxy,访问任意属性(包括 then)都会被当成事件名,所以不能直接把它交给 Promise resolve
// 否则 Promise 会去调它的 then 而永远不落定;这里用对象包一层。
function waitForTracker(): Promise<{ tracker: LandingTracker | null }> {
return new Promise((resolve) => {
const startedAt = Date.now()
const check = () => {
const tracker = (window as any).tracker as LandingTracker | undefined
if (tracker && typeof tracker.landingPageView === 'function') {
resolve({ tracker })
return
}
if (Date.now() - startedAt > SDK_READY_TIMEOUT_MS) {
resolve({ tracker: null })
return
}
setTimeout(check, SDK_POLL_MS)
}
check()
})
}
/**
* 事件上报服务类
*/
export class EventTracker {
/**
* 发送事件到服务器
*/
async sendEvent(eventCode: EventCode, properties?: Record<string, any>) {
const { tracker } = await waitForTracker()
if (!tracker) {
console.log('LandingSDK 未就绪,事件丢弃:' + eventCode)
return
}
try {
if (eventCode === 'landing_page_view' && typeof tracker.landingPageView === 'function') {
await tracker.landingPageView(properties)
} else if (eventCode === 'landing_page_click' && typeof tracker.landingPageClick === 'function') {
await tracker.landingPageClick(properties)
} else {
await tracker({ event: eventCode, ...properties })
}
} catch (err) {
console.log('埋点上报失败:' + eventCode + ' ' + err)
}
}
}
// 单例实例
let trackerInstance: EventTracker | null = null
/**
* 获取事件上报器实例(自动初始化,无需手动初始化)
*/
export function getEventTracker(): EventTracker {
if (!trackerInstance) {
trackerInstance = new EventTracker()
}
return trackerInstance
}
+4 -26
View File
@@ -1,33 +1,11 @@
/**
* 落地页 A/B 版本(对齐 hjll2):
* - ?version=A|B 或 ?v=A|B → 强制指定
* - 路径 /b → 强制 B
* - 否则 50% 随机;同一次页面生命周期内缓存,保证 UI 与接口一致
* 落地页版本:已下线 A 版,固定 B(getButtonList 等接口带 version=B
*/
export type LandingAbVersion = 'A' | 'B'
let cached: LandingAbVersion | null = null
export function getLandingVersion(
query?: Record<string, unknown>,
path?: string,
_query?: Record<string, unknown>,
_path?: string,
): LandingAbVersion {
if (cached) return cached
const pathname =
path ?? (typeof window !== 'undefined' ? window.location.pathname : '')
if (pathname === '/b' || pathname.startsWith('/b/')) {
cached = 'B'
return cached
}
const q = query ?? {}
const raw = String(q.v ?? q.version ?? '').toUpperCase()
if (raw === 'A' || raw === 'B') {
cached = raw
return cached
}
cached = Math.random() < 0.5 ? 'A' : 'B'
return cached
return 'B'
}
+20
View File
@@ -0,0 +1,20 @@
/**
* 会话 trace id:下载链接 / 剪贴板里的 tid、二维码 uid 共用
*/
let sessionTraceIdCache: string | null = null
function generateTraceId(): string {
return Math.random().toString(36).substr(2, 36) + Date.now().toString(36)
}
export function getSessionTraceId(): string {
if (!sessionTraceIdCache) {
sessionTraceIdCache = generateTraceId()
}
return sessionTraceIdCache
}
export function renewSessionTraceId(): string {
sessionTraceIdCache = generateTraceId()
return sessionTraceIdCache
}
-72
View File
@@ -1,72 +0,0 @@
/**
* 事件上报系统类型定义
*/
/**
* 事件优先级
*/
export enum EventPriority {
P0 = 'P0', // 最高优先级
P1 = 'P1', // 普通优先级
}
/**
* 事件代码枚举
*/
export enum EventCode {
// 落地页
LANDING_PAGE_VIEW = 'landing_page_view',
LANDING_PAGE_CLICK = 'landing_page_click',
}
/**
* 事件元数据配置
*/
export interface EventMetadata {
code: EventCode
name: string
priority: EventPriority
description: string
}
/**
* 所有事件的元数据配置
*/
export const EVENT_METADATA: Record<EventCode, EventMetadata> = {
[EventCode.LANDING_PAGE_VIEW]: {
code: EventCode.LANDING_PAGE_VIEW,
name: '落地页展示',
priority: EventPriority.P0,
description: '落地页展示上报一条',
},
[EventCode.LANDING_PAGE_CLICK]: {
code: EventCode.LANDING_PAGE_CLICK,
name: '落地页点击',
priority: EventPriority.P0,
description: '落地页点击上报一条',
},
}
/**
* 事件数据接口
*/
export interface EventData {
/** 事件代码 */
event: EventCode
/** 自定义属性 */
properties?: Record<string, any>
channel?: string | number
event_id?: string
app_id?: string
uid?: string
sid?: string
client_ts?: number
device?: string
device_id?: string
user_agent?: string
device_brand?: string
device_model?: string
}
+117 -139
View File
@@ -1,20 +1,17 @@
/**
* App 下载逻辑(后端接口沿用 bili / jha-505 落地页;交互对齐黄果另一版落地页 d10vry7axuojk2
* App 下载逻辑(请求对齐 bili-landpage-client,接口域名见 api/btn.ts
*
* - 拉取下载地址:getButtonListchannel 字段带整包 URL query(含 ?dc= 渠道),渠道统计由后端负责
* - 点击上报bottonclick,同样带 channel;点击时把 { ...query, tid } 写入剪贴板供 App 首启归因
* - iOS先弹安装引导(guide = 'ios');普通浏览器再跳描述文件;Twitter/IG/TikTok 内置浏览器只引导复制到 Safari,不跳转
* - Android:先弹「安卓安装教程」(guide = 'android'),再按 hk 包 / 渠道包 / 主包 / 商店包 顺序跳转;
* dc 命中 ky / TLGG / erra 前缀时优先走 hkAndroidDownUrl
* - 进页面:bottonlist 拿按钮开关(iosDown / iosStore / iosTF / iosH5 / androidButton),不含下载地址
* - 点下载bottonclick 返回 data.link(地址数组),跳 link[index];同一请求后端记点击
* - iOS商店包 iosStore(packageType 2) / 企业签 iosDown(1) / TF iosTF(3) 走 bottonclick 拿 link
* iosH5 走 h5/config 下载描述文件;社媒内置浏览器装不了描述文件,只弹「复制到 Safari」引导
* - 下载前把 { ...query, tid } 写入剪贴板供 App 首启归因
*
* 组件接入:
* const { init, download, guide, isSocialInApp } = useAppDownload()
* onMounted(init) // 拉取下载地址
* <a data-track="download" @click="download(0)">轻量版 / 主包</a>
* <a data-track="download" @click="download(1)">商店包</a>
* <IosSafariOpenGuide v-if="isSocialInApp" :show="guide === 'ios'" :url="iosInstallUrl" />
* <IosInstallGuide v-else :show="guide === 'ios'" @close="guide = null" />
* <AndroidInstallTip :show="guide === 'android'" @close="guide = null" />
* onMounted(init) // 拉取按钮开关
* <a @click="download(0)">轻量版 / 主包</a>
* <a @click="download(1)">商店包</a>
*/
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
@@ -23,59 +20,43 @@ import type { donwloadParam, H5param } from '~/api/interface'
import { APP_ID } from '~/config/app'
import { copyToClipboard } from '~/utils/clipboard'
import { isSocialInAppBrowser } from '~/utils/isSocialInAppBrowser'
import { getLandingVersion } from '~/utils/landingVersion'
import { getSessionTraceId, renewSessionTraceId } from '~/utils/useEventTracker'
import { renewSessionTraceId } from '~/utils/traceId'
export interface ButtonList {
androidDownUrl?: string
storeAndroidDownUrl?: string
hkAndroidDownUrl?: string
channelApkAndDownUrl?: string
iosDownUrl?: string
storeIosDownUrl?: string
shopIosDownUrl?: string
iosDown?: boolean
iosStore?: boolean
iosTF?: boolean
iosH5?: boolean
iosLink?: string
androidButton?: number
hasChannelApk?: boolean
[key: string]: unknown
}
export type DownloadGuide = 'ios' | 'android' | null
export type Terminal = 'ios' | 'android'
// 弹层展示到真正跳转下载地址之间的间隔,让引导先渲染出来(与参考版一致)
const NAVIGATE_DELAY_MS = 500
// 模块级单例:PC / 手机组件共用同一份下载地址,init 只请求一次
// 模块级单例:PC / 手机组件共用同一份按钮开关,init 只请求一次
const buttonList = ref<ButtonList>({})
const ip = ref('')
const ready = ref(false)
let initPromise: Promise<void> | null = null
const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent)
const isWeiXin = /MicroMessenger/i.test(navigator.userAgent)
// 这些 dc 前缀的渠道走 hk 包
function isHkDc(dc: unknown) {
const value = String(dc ?? '')
return (
/^ky(0?[1-9]|[1-9][0-9]|100)$/i.test(value) ||
/^TLGG(0?[1-9]|[1-4][0-9]|5[0-6])$/i.test(value) ||
/^erra(0?[1-9]|10)$/i.test(value)
)
// 去掉空值后序列化(bili filterChannel
function filterChannel(query: Record<string, unknown>) {
const result: Record<string, unknown> = {}
Object.entries(query).forEach(([key, value]) => {
if (value) result[key] = value
})
return JSON.stringify(result)
}
// 描述文件地址追加渠道 + trace_id(后端据此把渠道写进 Web Clip,安装后可归因);渠道键优先级 dc → ch → pc
function withChannelQuery(url: string, query: Record<string, unknown>, traceId: string) {
const pairs: [string, string][] = []
const channelKey = (['dc', 'ch', 'pc'] as const).find((key) => query[key])
if (channelKey) {
pairs.push([channelKey, String(query[channelKey])])
}
pairs.push(['tid', traceId])
try {
const target = new URL(url)
pairs.forEach(([key, value]) => target.searchParams.set(key, value))
return target.toString()
} catch {
const qs = pairs.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&')
return url + (url.includes('?') ? '&' : '?') + qs
}
function jumpToLink(link: string) {
// 微信内加时间戳,避免命中缓存
window.location.href = isWeiXin ? link + '?time=' + Date.now() : link
}
async function fetchIp() {
@@ -88,30 +69,22 @@ async function fetchIp() {
}
}
export function useAppDownload(version = '') {
export function useAppDownload() {
const route = useRoute()
// 当前要展示的安装引导弹层,由页面组件绑定到 IosInstallGuide / AndroidInstallTip
const guide = ref<DownloadGuide>(null)
// 与首页分流同一份 A/Bhjll2:接口必带 version
const landingVersion = () => version || getLandingVersion(route.query, route.path)
// 整包 query 序列化后交给后端解析 dc / pc 等渠道参数
const channel = () => JSON.stringify(route.query)
async function fetchButtonList() {
try {
const { data } = await getButtonList({
appId: APP_ID,
domain: window.location.protocol + '//' + window.location.host,
channel: channel(),
version: landingVersion(),
tid: getSessionTraceId(),
ip: ip.value,
channel: JSON.stringify(route.query),
})
buttonList.value = (data as ButtonList) || {}
ready.value = true
} catch (err) {
console.log('获取按钮列表失败' + err)
console.log('获取按钮列表失败' + JSON.stringify(err))
}
}
@@ -125,111 +98,116 @@ export function useAppDownload(version = '') {
return initPromise
}
function navigateLater(url: string) {
setTimeout(() => {
window.location.href = url
}, NAVIGATE_DELAY_MS)
// bottonclick 返回的下载地址数组
async function fetchLinks(param: donwloadParam): Promise<string[]> {
const { data } = await bottonclick(param)
const link = (data as { link?: string[] } | undefined)?.link
return Array.isArray(link) ? link.filter(Boolean) : []
}
async function downloadIos(param: donwloadParam, useStore: boolean, traceId: string) {
const list = buttonList.value
async function downloadMobileConfig() {
const paramData: H5param = { appId: APP_ID }
if (route.query.dc) paramData.dc = String(route.query.dc)
if (route.query.pc) paramData.pc = String(route.query.pc)
const content = await downloadH5(paramData)
const file = new File([content], 'descriptionFile.mobileconfig', {
type: 'application/x-apple-aspen-config',
})
const objectUrl = URL.createObjectURL(file)
const link = document.createElement('a')
link.href = objectUrl
link.download = 'descriptionFile.mobileconfig'
link.click()
setTimeout(() => URL.revokeObjectURL(objectUrl), 10000)
}
async function downloadIos(param: donwloadParam, useStore: boolean) {
const flags = buttonList.value
if (useStore) {
if (!list.shopIosDownUrl) {
console.log('后端未返回 iOS 商店包地址 shopIosDownUrl')
return
}
if (!flags.iosStore) return false
param.type = 'iosStore'
param.packageType = 2
window.location.href = list.shopIosDownUrl
await bottonclick(param)
return
const links = await fetchLinks(param)
if (!links.length) return false
jumpToLink(links[0])
return true
}
param.packageType = 1
if (list.iosDownUrl) {
// 先展示安装引导;社媒内置浏览器无法装描述文件,只弹「复制到 Safari」引导,不触发跳转
if (flags.iosDown || flags.iosTF) {
param.type = flags.iosDown ? 'iosDown' : 'iosTF'
param.packageType = flags.iosDown ? 1 : 3
guide.value = 'ios'
if (!isSocialInAppBrowser()) {
// Safari / 普通浏览器:再跳描述文件(会弹「此网站正尝试下载一个配置描述文件」)
navigateLater(withChannelQuery(list.iosDownUrl, route.query, traceId))
}
} else if (list.storeIosDownUrl) {
window.location.href = list.storeIosDownUrl
} else {
// 兜底:前端直接拉描述文件(线上 huangguo.com 未反代 /qaz,此分支目前不可达,后端已配 iosDownUrl
// 只带有值的渠道键,避免 URLSearchParams 把 undefined 序列化成字符串 "undefined"
const paramData: H5param = { appId: APP_ID }
if (route.query.dc) paramData.dc = String(route.query.dc)
if (route.query.pc) paramData.pc = String(route.query.pc)
const res: any = await downloadH5(paramData)
const file = new File([res.data.value as BlobPart], 'descriptionFile.mobileconfig', {
type: 'application/x-apple-aspen-config',
})
const objectUrl = URL.createObjectURL(file)
const link = document.createElement('a')
link.href = objectUrl
link.download = 'descriptionFile.mobileconfig'
link.click()
const links = await fetchLinks(param)
if (!links.length) return false
jumpToLink(links[0])
return true
}
await bottonclick(param)
if (flags.iosH5) {
guide.value = 'ios'
// 社媒内置浏览器无法装描述文件,只展示引导,由页面「复制到 Safari」处理
if (!isSocialInAppBrowser()) await downloadMobileConfig()
return true
}
// 没开轻量版时退到商店包
if (flags.iosStore) return downloadIos(param, true)
return false
}
async function downloadAndroid(param: donwloadParam, index: number) {
const list = buttonList.value
// 先弹「安卓安装教程」(断网安装、报毒说明),再触发 APK 下载
// 先弹「安卓安装教程」(断网安装、报毒说明),再跳 APK
guide.value = 'android'
let target = ''
if (isHkDc(route.query.dc) && list.hkAndroidDownUrl) {
target = list.hkAndroidDownUrl
} else if (index === 0 && list.channelApkAndDownUrl) {
target = list.channelApkAndDownUrl
} else if (index === 0 && list.androidDownUrl) {
target = list.androidDownUrl
} else if (list.storeAndroidDownUrl) {
target = list.storeAndroidDownUrl
}
if (!target) {
console.log('后端未返回 Android 下载地址(appId=' + APP_ID + ' 需在后台配置 APK')
return
}
navigateLater(target)
await bottonclick(param)
const links = await fetchLinks(param)
const target = links[index] || links[0]
if (!target) return false
jumpToLink(target)
return true
}
/**
* 点击下载
* @param index 0 = 轻量版 / 主包1 = 商店包
* 点击下载,返回是否拿到并开始下载(false 时页面可提示稍后重试)
* @param index iOS0 = 轻量版,1 = 商店包Android:下载地址下标
* @param terminal 默认按 UAPC 端弹窗里的 iOS / Android 按钮显式传入
*/
async function download(index = 0) {
async function download(index = 0, terminal: Terminal = isIos ? 'ios' : 'android') {
await init()
const trace_id = renewSessionTraceId()
// 写剪贴板供 App 首次启动读取做安装归因:整包 URL 参数 + tid(键名与 d10vry7axuojk2 版一致,黄果 App 按 tid 读)
// 写剪贴板供 App 首次启动读取做安装归因:整包 URL 参数 + tid(黄果 App 按 tid 读)
copyToClipboard(JSON.stringify({ ...route.query, tid: trace_id }))
const param: donwloadParam = {
appId: APP_ID,
ip: ip.value,
channel: channel(),
channel: filterChannel(route.query),
domain: window.location.host || '',
type: '',
terminal: isIos ? 'ios' : 'android',
version: landingVersion(),
// bili 默认值,安卓请求同样带 iosH5
type: 'iosH5',
terminal,
}
try {
if (isIos) {
// index 0 → iosDownUrl 轻量版;index 1 → shopIosDownUrl 商店包
await downloadIos(param, index === 1, trace_id)
} else {
await downloadAndroid(param, index)
}
return terminal === 'ios'
? await downloadIos(param, index === 1)
: await downloadAndroid(param, index)
} catch (err) {
console.log('下载失败' + err)
console.log('下载失败' + JSON.stringify(err))
return false
}
}
// 社媒引导页展示/复制用:iosDownUrl + dc/ch/pc + tid
const iosInstallUrl = computed(() => {
const url = buttonList.value.iosDownUrl
if (!url) return ''
return withChannelQuery(url, route.query, getSessionTraceId())
})
const hasIosStore = computed(() => !!buttonList.value.iosStore)
return { buttonList, ready, isIos, isSocialInApp: isSocialInAppBrowser(), guide, iosInstallUrl, init, download }
// 社媒引导页「复制到 Safari」用:当前落地页地址(带渠道参数)
const iosInstallUrl = computed(() => window.location.href)
return {
buttonList,
ready,
isIos,
hasIosStore,
isSocialInApp: isSocialInAppBrowser(),
guide,
iosInstallUrl,
init,
download,
}
}
-68
View File
@@ -1,68 +0,0 @@
/**
* Vue Composable for Event Tracking - 简化版
*/
import { getEventTracker } from './eventTracker'
import { EventCode } from './types'
let sessionTraceIdCache: string | null = null
function generateTraceId(): string {
return Math.random().toString(36).substr(2, 36) + Date.now().toString(36)
}
export function getSessionTraceId(): string {
if (!sessionTraceIdCache) {
sessionTraceIdCache = generateTraceId()
}
return sessionTraceIdCache
}
export function renewSessionTraceId(): string {
sessionTraceIdCache = generateTraceId()
return sessionTraceIdCache
}
/**
* 使用事件追踪器
*/
export function useEventTracker() {
const tracker = getEventTracker()
/**
* 上报事件
*/
const track = (eventCode: EventCode, properties?: Record<string, any>) => {
tracker.sendEvent(eventCode, properties)
}
/**
* 上报落地页展示日志
*/
const trackLandingPageView = (properties?: Record<string, any>) => {
track(EventCode.LANDING_PAGE_VIEW, properties)
}
/**
* 上报落地页点击
*/
const trackLandingPageClick = (properties?: Record<string, any>) => {
track(EventCode.LANDING_PAGE_CLICK, properties)
}
const generateSessionId = (): string => {
return Math.random().toString(36).substr(2, 36) + Date.now().toString(36)
}
return {
// 基础方法
track,
// 便捷方法
trackLandingPageView,
trackLandingPageClick,
generateSessionId,
}
}
-14
View File
@@ -25,20 +25,6 @@ export default defineConfig({
ws: false,
changeOrigin: true,
},
// 埋点:/dcserv/api/eventTracking/{report|batchReport}.json
// → https://ap.dc-report.cc/api/eventTracking/{report|batchReport}.json
'/dcserv': {
target: 'https://ap.dc-report.cc',
ws: false,
changeOrigin: true,
rewrite: (p) => p.replace(/^\/dcserv/, ''),
},
//本地开发
'/qaz': {
target: 'https://d2yvsvw55p1b3a.cloudfront.net', // 这里是接口地址
ws: false,
changeOrigin: true,
},
},
},
})