This commit is contained in:
谢宇宁
2026-10-21 11:16:51 +07:00
parent 44004a77b5
commit 4c0c79a2cd
31 changed files with 2045 additions and 7454 deletions
+5 -5
View File
@@ -17,7 +17,7 @@
<meta name="robots" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
<meta name="googlebot" content="index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1" />
<!-- 站长验证(huanguoai.net 的 token,换域名需同步更换) -->
<!-- 站长验证:以下 token 属于 huanguoai.net,主域名 hgaidj.app 需在各站长平台单独验证后替换 -->
<meta name="google-site-verification" content="VEhl0na6ZwYZgkdcdz8BcCR4LpYGFm97zbp62YqJHX4" />
<meta name="yandex-verification" content="56acf08532be3060" />
<meta name="msvalidate.01" content="57A5999E9B5167EE9039265EBCA2AE3A" />
@@ -27,13 +27,13 @@
<meta property="og:site_name" content="黄果短剧" />
<meta property="og:title" content="黄果短剧 – AI成人短剧与原创黄剧每日更新" />
<meta property="og:description" content="黄果短剧专注 AI 成人短剧与原创成人短剧内容,覆盖甜宠、逆袭、虐恋、悬疑、都市、穿越、古风等热门题材,提供高清流畅在线播放,每日更新,打造稳定的成人短剧观看平台。" />
<meta property="og:url" content="https://huanguoai.net/" />
<meta property="og:image" content="https://huanguoai.net/static/og-default.png" />
<meta property="og:url" content="https://hgaidj.app/" />
<meta property="og:image" content="https://hgaidj.app/logo.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="黄果短剧 – AI成人短剧与原创黄剧每日更新" />
<meta name="twitter:description" content="黄果短剧专注 AI 成人短剧与原创成人短剧内容,覆盖甜宠、逆袭、虐恋、悬疑、都市、穿越、古风等热门题材,提供高清流畅在线播放,每日更新,打造稳定的成人短剧观看平台。" />
<meta name="twitter:image" content="https://huanguoai.net/static/og-default.png" />
<meta name="twitter:image" content="https://hgaidj.app/logo.png" />
<meta name="twitter:site" content="黄果短剧" />
<meta name="theme-color" content="#000000" />
@@ -41,7 +41,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="#000000" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="canonical" href="https://huanguoai.net/" />
<link rel="canonical" href="https://hgaidj.app/" />
<!-- 统计:与 huanguoai.net 主站同一套 ID(见 web_huangguo2_h5/app/template/base/analytics.html) -->
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Q04W0RSFPH"></script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="download-01">
<path id="Icon" d="M3.5 17.8349C3.85412 18.1807 4.33441 18.375 4.83521 18.375H16.1648C16.6656 18.375 17.1459 18.1807 17.5 17.8349M10.5011 2.625V13.0747M6.18502 9.08189L10.5011 13.0747L14.8171 9.08189" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" stroke="#EC237F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 486 B

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="download-01">
<path id="Icon" d="M3 15.287C3.30353 15.5835 3.71521 15.75 4.14446 15.75H13.8555C14.2848 15.75 14.6965 15.5835 15 15.287M9.0009 2.25V11.2068M5.30144 7.78447L9.0009 11.2068L12.7004 7.78447" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 471 B

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="download-01">
<path id="Icon" d="M2.66667 13.5885C2.93647 13.852 3.3024 14 3.68397 14H12.316C12.6976 14 13.0635 13.852 13.3333 13.5885M8.0008 2V9.96164M4.7124 6.91953L8.0008 9.96164L11.2892 6.91953" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" stroke="#EC237F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 470 B

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="edit-03">
<path id="Icon" d="M11.5 16.293H16.5M3.50003 16.293L7.13835 15.5599C7.33149 15.521 7.50884 15.4259 7.64812 15.2865L15.7929 7.13731C16.1834 6.7466 16.1831 6.11328 15.7923 5.72289L14.0669 3.9995C13.6762 3.60927 13.0432 3.60954 12.6529 4.00009L4.50733 12.1501C4.36832 12.2892 4.2734 12.4662 4.23444 12.659L3.50003 16.293Z" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" stroke="#EC237F"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" overflow="visible" style="display: block;" width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="play-01">
<path id="Icon" d="M13.6495 11.2083L13.1721 10.4749L13.1599 10.4829L13.148 10.4912L13.6495 11.2083ZM13.6495 10.0041L13.1307 10.7087L13.1697 10.7374L13.2117 10.7617L13.6495 10.0041ZM8.82352 6.45047L9.34235 5.74588L9.31766 5.7277L9.29175 5.71129L8.82352 6.45047ZM7.81178 6.98125L8.68678 6.97871L8.68676 6.97105L8.6866 6.96339L7.81178 6.98125ZM7.83202 13.9617L6.95702 13.9643L6.95709 13.989L6.95857 14.0137L7.83202 13.9617ZM8.88085 14.5435L9.30454 15.309L9.34472 15.2868L9.38234 15.2605L8.88085 14.5435ZM5.25 2.1V2.975H15.75V2.1V1.225H5.25V2.1ZM18.9 5.25H18.025V15.75H18.9H19.775V5.25H18.9ZM15.75 18.9V18.025H5.25V18.9V19.775H15.75V18.9ZM2.1 15.75H2.975V5.25H2.1H1.225V15.75H2.1ZM5.25 18.9V18.025C3.99355 18.025 2.975 17.0064 2.975 15.75H2.1H1.225C1.225 17.9729 3.02705 19.775 5.25 19.775V18.9ZM18.9 15.75H18.025C18.025 17.0064 17.0064 18.025 15.75 18.025V18.9V19.775C17.9729 19.775 19.775 17.9729 19.775 15.75H18.9ZM15.75 2.1V2.975C17.0064 2.975 18.025 3.99355 18.025 5.25H18.9H19.775C19.775 3.02705 17.9729 1.225 15.75 1.225V2.1ZM5.25 2.1V1.225C3.02705 1.225 1.225 3.02705 1.225 5.25H2.1H2.975C2.975 3.99355 3.99355 2.975 5.25 2.975V2.1ZM13.6495 11.2083L14.1269 11.9416C14.507 11.6941 14.9874 11.2528 14.9865 10.5876C14.9856 9.90274 14.4805 9.47374 14.0873 9.24651L13.6495 10.0041L13.2117 10.7617C13.3033 10.8146 13.3201 10.8421 13.3074 10.8259C13.2885 10.8018 13.2367 10.7189 13.2365 10.59C13.2364 10.4643 13.2854 10.3903 13.2931 10.3799C13.2968 10.3749 13.2699 10.4113 13.1721 10.4749L13.6495 11.2083ZM13.6495 10.0041L14.1683 9.29951L9.34235 5.74588L8.82352 6.45047L8.30469 7.15506L13.1307 10.7087L13.6495 10.0041ZM8.82352 6.45047L9.29175 5.71129C8.87997 5.45045 8.3017 5.30099 7.75481 5.55634C7.1596 5.83426 6.92541 6.43296 6.93697 6.9991L7.81178 6.98125L8.6866 6.96339C8.68556 6.91215 8.69622 6.92872 8.66931 6.98021C8.63899 7.03823 8.57976 7.10253 8.49519 7.14201C8.33248 7.21799 8.26682 7.13361 8.35529 7.18965L8.82352 6.45047ZM7.81178 6.98125L6.93679 6.98379L6.95702 13.9643L7.83202 13.9617L8.70702 13.9592L8.68678 6.97871L7.81178 6.98125ZM7.83202 13.9617L6.95857 14.0137C6.9864 14.4813 7.14893 15.1045 7.73555 15.4178C8.31314 15.7263 8.91448 15.5249 9.30454 15.309L8.88085 14.5435L8.45716 13.7779C8.37044 13.8259 8.33634 13.8285 8.35004 13.8266C8.37073 13.8238 8.45619 13.8188 8.55998 13.8742C8.66522 13.9304 8.71077 14.0068 8.72181 14.0303C8.72909 14.0458 8.71179 14.0159 8.70547 13.9097L7.83202 13.9617ZM8.88085 14.5435L9.38234 15.2605L14.151 11.9253L13.6495 11.2083L13.148 10.4912L8.37936 13.8264L8.88085 14.5435Z" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-667
View File
@@ -1,667 +0,0 @@
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 { useAppDownload } from '~/utils/useAppDownload'
import { copyToClipboard } from '~/utils/clipboard'
import { getLandingVersion } from '~/utils/landingVersion'
export interface Drama {
t: string
tags: string[]
eps: number | null
cast: string | null
kind: '短剧' | '影集'
cover: string
syn: string | null
slug?: string
}
interface RawDrama {
id?: number
title?: string
t?: string
tags?: string[]
episodes?: number | null
eps?: number | null
cast?: string | null
kind?: '短剧' | '影集'
cover?: string
synopsis?: string | null
syn?: string | null
}
interface RawFeatured {
order?: number
title?: string
t?: string
slug: string
files?: Record<string, string>
}
const DRAMAS: Drama[] = (rawDramas as RawDrama[]).map((d) => ({
t: d.t || d.title || '',
tags: Array.isArray(d.tags) ? d.tags : [],
eps: d.eps ?? d.episodes ?? null,
cast: d.cast ?? null,
kind: d.kind === '影集' ? '影集' : '短剧',
cover: d.cover || 'covers2/d01.jpg',
syn: d.syn ?? d.synopsis ?? '',
}))
const FEATURED_CONFIG = rawFeatured as RawFeatured[]
const FEATURED: Drama[] = FEATURED_CONFIG.map((f) => {
const title = f.t || f.title || ''
const found = DRAMAS.find((d) => d.t === title)
return {
t: title || (found ? found.t : ''),
tags: found?.tags && found.tags.length ? found.tags : ['原创'],
eps: found?.eps ?? 10,
cast: found?.cast ?? '原创',
kind: found?.kind || '短剧',
cover: f.files?.cover || found?.cover || `hero/${f.slug}-cover.jpg`,
syn: found?.syn || '',
slug: f.slug,
}
})
const GENRES = ['都市', '悬疑', '古装', '奇幻', '穿越', '重生', '武侠', '逆袭']
const FAQS = [
{
q: '黄果短剧 App 需要付费吗?',
a: 'App 免费下载。海量短剧与影集每日更新,开通会员可解锁全站作品、免广告与高清画质,随时可取消。',
},
{
q: 'iOS 如何安装黄果短剧 App?',
a: '用 Safari 打开本页,点击「下载 iOS 版」,允许下载描述文件后,前往 设置 › 已下载描述文件 › 安装,输入锁屏密码即可;桌面出现图标即安装完成。',
},
{
q: '内容多久更新一次?',
a: '每日更新。短剧与影集双轨并行,新作上线会同步显示在「最新上架」与 App 首页。',
},
]
const assetUrl = (path: string) => `/b-land/assets/${path}`
export type LandingBMode = 'pc' | 'mobile'
/**
* Landing B 共享逻辑。mode 决定下载分流:pc 开扫码弹窗,mobile 直接触发下载。
*/
export function useLandingB(mode: LandingBMode) {
const isMobileMode = mode === 'mobile'
const route = useRoute()
const currentView = ref<'landing' | 'works'>('landing')
const currentHeroIndex = ref(0)
const isHeroTransitioning = ref(false)
const isHeroHovered = ref(false)
const heroProgKey = ref(0)
const bgFront = ref<'a' | 'b'>('a')
const bgAUrl = ref('')
const bgBUrl = ref('')
const topImgUrl = ref('')
let heroTimer: ReturnType<typeof setTimeout> | null = null
let heroStartedAt = 0
let heroRemaining = 6000
const HERO_INTERVAL = 6000
const currentHero = computed(() => {
return (
FEATURED[currentHeroIndex.value] ||
FEATURED[0] || {
t: '废柴杂役',
tags: ['重生', '武侠'],
eps: 15,
cast: '粗硬风辞',
kind: '短剧' as const,
cover: 'covers2/d01.jpg',
syn: '',
slug: '05-feichai',
}
)
})
function heroBlurUrl(slug: string) {
return assetUrl(`hero/${slug}-land-blur.jpg`)
}
function heroPortUrl(slug: string) {
return assetUrl(`hero/${slug}-port-top.jpg`)
}
function applyHeroVisuals(index: number, first = false) {
const item = FEATURED[index]
if (!item?.slug) return
const blur = heroBlurUrl(item.slug)
topImgUrl.value = heroPortUrl(item.slug)
if (first) {
bgAUrl.value = blur
bgBUrl.value = blur
bgFront.value = 'a'
return
}
if (bgFront.value === 'a') {
bgBUrl.value = blur
bgFront.value = 'b'
} else {
bgAUrl.value = blur
bgFront.value = 'a'
}
}
function setHero(index: number, first = false) {
if (!first && index === currentHeroIndex.value) return
currentHeroIndex.value = index
applyHeroVisuals(index, first)
if (!first) {
isHeroTransitioning.value = false
nextTick(() => {
isHeroTransitioning.value = true
})
}
heroProgKey.value += 1
heroRemaining = HERO_INTERVAL
heroStartedAt = Date.now()
if (isHeroHovered.value) {
pauseHeroTimer()
} else {
startHeroTimer(HERO_INTERVAL)
}
}
function nextHero() {
setHero((currentHeroIndex.value + 1) % FEATURED.length)
}
function prevHero() {
setHero((currentHeroIndex.value - 1 + FEATURED.length) % FEATURED.length)
}
function startHeroTimer(ms = HERO_INTERVAL) {
if (heroTimer) clearTimeout(heroTimer)
heroRemaining = ms
heroStartedAt = Date.now()
heroTimer = setTimeout(() => {
if (!isHeroHovered.value) nextHero()
}, ms)
}
function pauseHeroTimer() {
if (heroTimer) {
clearTimeout(heroTimer)
heroTimer = null
}
}
function onHeroMouseEnter() {
if (isMobileMode) return
if (isHeroHovered.value) return
isHeroHovered.value = true
if (heroStartedAt) {
heroRemaining = Math.max(50, heroRemaining - (Date.now() - heroStartedAt))
}
pauseHeroTimer()
}
function onHeroMouseLeave() {
if (isMobileMode) return
if (!isHeroHovered.value) return
isHeroHovered.value = false
startHeroTimer(heroRemaining)
}
let touchStartX = 0
let touchStartY = 0
function handleHeroTouchStart(e: TouchEvent) {
if (e.touches.length > 0) {
touchStartX = e.touches[0].clientX
touchStartY = e.touches[0].clientY
}
}
function handleHeroTouchEnd(e: TouchEvent) {
if (e.changedTouches.length > 0) {
const diffX = e.changedTouches[0].clientX - touchStartX
const diffY = e.changedTouches[0].clientY - touchStartY
if (Math.abs(diffX) > 40 && Math.abs(diffX) > Math.abs(diffY)) {
if (diffX < 0) nextHero()
else prevHero()
}
}
}
const wallDramas = computed(() => [...DRAMAS.slice(0, 12), ...DRAMAS.slice(0, 12)])
const selectedKind = ref<'全部' | '短剧' | '影集'>('全部')
const selectedGenres = ref<string[]>([])
const showPcDownloadModal = ref(false)
const showInstallGuideModal = ref(false)
const installGuideTab = ref<'ios' | 'android'>('ios')
/** iOS 安装引导内:商店版 / 轻量版(仅 iOS 页内切换) */
const iosInstallGuideTab = ref<'store' | 'lite'>('store')
const targetDramaForDownload = ref<Drama | null>(null)
const AGE_STORAGE_KEY = 'hg_age_ok'
const showAgeGate = ref(false)
const isScrolled = ref(false)
const showFloatingQr = ref(false)
const isFloatingQrDismissed = ref(false)
const showMobileStickyBar = ref(false)
const activeAnchor = ref('#works')
const toastMessage = ref('')
const isToastVisible = ref(false)
let toastTimer: ReturnType<typeof setTimeout> | null = null
/** PC 扫码:当前页 URL(对齐 hjll2 / A 版),并带上当前 A/B 避免扫码后重新随机 */
const pageQrUrl = ref('')
function buildPageQrUrl() {
const url = new URL(window.location.href)
if (!url.searchParams.has('version') && !url.searchParams.has('v')) {
url.searchParams.set('version', getLandingVersion(route.query, route.path))
}
return url.toString()
}
const {
buttonList,
isIos,
download,
hasIosStore: hasIosShopPackage,
iosInstallUrl,
isSocialInApp,
guide,
init: initDownload,
} = useAppDownload()
function closeGuide() {
guide.value = null
}
function showToast(msg: string) {
toastMessage.value = msg
isToastVisible.value = true
if (toastTimer) clearTimeout(toastTimer)
toastTimer = setTimeout(() => {
isToastVisible.value = false
}, 2000)
}
function openInstallGuide(
platform: 'ios' | 'android' = 'ios',
iosVariant?: 'store' | 'lite',
) {
installGuideTab.value = platform
if (platform === 'ios') {
iosInstallGuideTab.value =
iosVariant ?? (hasIosShopPackage.value ? 'store' : 'lite')
}
showInstallGuideModal.value = true
}
function confirmAge() {
try {
sessionStorage.setItem(AGE_STORAGE_KEY, '1')
} catch {
/* ignore */
}
showAgeGate.value = false
}
function leaveSite() {
window.location.replace('about:blank')
}
function syncPageLock() {
const locked =
showAgeGate.value || showPcDownloadModal.value || showInstallGuideModal.value
document.body.classList.toggle('landing-b-lock', locked)
document.body.style.overflow = locked ? 'hidden' : ''
document.documentElement.style.overflow = locked ? 'hidden' : 'auto'
}
function handleFilterClick() {
if (isMobileMode) {
handleDownload()
} else {
showPcDownloadModal.value = true
}
}
function setKind(k: '全部' | '短剧' | '影集') {
selectedKind.value = k
}
function toggleGenre(g: string) {
const idx = selectedGenres.value.indexOf(g)
if (idx > -1) selectedGenres.value.splice(idx, 1)
else selectedGenres.value.push(g)
}
function filterDramaList(list: Drama[]) {
return list.filter((d) => {
const tags = Array.isArray(d.tags) ? d.tags : []
const matchKind = selectedKind.value === '全部' || d.kind === selectedKind.value
const matchGenre =
selectedGenres.value.length === 0 ||
selectedGenres.value.some((tag) => tags.includes(tag))
return matchKind && matchGenre
})
}
const top5List = computed(() => DRAMAS.slice(0, 5))
const filteredTop5 = computed(() => filterDramaList(top5List.value))
const latest6List = computed(() => DRAMAS.slice(5, 11))
const filteredLatest6 = computed(() => filterDramaList(latest6List.value))
const allWorksList = computed(() => DRAMAS)
const filteredAllWorks = computed(() => filterDramaList(allWorksList.value))
const faqOpenIndex = ref<number | null>(0)
function toggleFaq(index: number) {
faqOpenIndex.value = faqOpenIndex.value === index ? null : index
}
function handleDownload(
platform?: 'ios' | 'ios-lite' | 'ios-store' | 'android',
drama?: Drama | null,
) {
targetDramaForDownload.value = drama || null
if (isMobileMode) {
// 商店版:直接跳转 App Store,不进安装引导
if (platform === 'ios-store') {
handleIosStoreAction()
return
}
// H5:对齐 A 版 —— 先弹出安装引导,同时触发真实下载(download 内会跳描述文件 / APK)
// A 版是 CTA 直接 download(0),再用 IosInstallGuide 做说明层;B 版用自研引导页承接说明
if (platform === 'android') {
openInstallGuide('android')
download(0)
return
}
if (platform === 'ios-lite') {
openInstallGuide('ios', 'lite')
// 社媒内置浏览器无法装描述文件,只展示引导文案,由底栏「复制/下载」处理
if (!isSocialInApp) download(0)
return
}
if (platform === 'ios') {
openInstallGuide('ios')
if (iosInstallGuideTab.value === 'lite' && !isSocialInApp) download(0)
return
}
// 无平台参数:按 UA 打开引导,并立刻开始对应下载
if (isIos) {
openInstallGuide('ios')
if (iosInstallGuideTab.value === 'lite' && !isSocialInApp) download(0)
} else {
openInstallGuide('android')
download(0)
}
} else {
showPcDownloadModal.value = true
}
}
function handleInstallGuideDownload() {
download(0)
}
function handleInstallGuidePrimary() {
// isSocialInApp 来自 useAppDownload,是 boolean 不是 ref
if (installGuideTab.value === 'ios') {
if (iosInstallGuideTab.value === 'store' && hasIosShopPackage.value) {
handleIosStoreAction()
return
}
if (isSocialInApp) {
handleCopyIosLiteLink()
return
}
download(0, 'ios').then((ok) => {
if (!ok) showToast('下载链接获取中,请稍后重试')
})
return
}
download(0, 'android').then((ok) => {
if (!ok) showToast('下载链接获取中,请稍后重试')
})
}
function handleCopyIosLiteLink() {
copyToClipboard(iosInstallUrl.value)
showToast('已复制链接')
}
function handleIosStoreAction() {
if (!hasIosShopPackage.value) {
showToast('商店版链接获取中,请扫码下载')
return
}
// 链接由 bottonclick 返回,拿到后同页跳转(异步后 window.open 会被拦截)
download(1, 'ios').then((ok) => {
if (!ok) showToast('商店版链接获取中,请扫码下载')
})
}
function handlePcAndroidDownload() {
download(0, 'android').then((ok) => {
if (!ok) showToast('正在获取 Android 安装包,请扫码下载')
})
}
function handleOpenAppDeepLink() {
showToast('示意:Deep link 未配置')
}
function onModalKeydown(e: KeyboardEvent) {
if (e.key !== 'Escape') return
// 年龄确认不可 Esc 关闭
if (showAgeGate.value) return
if (showPcDownloadModal.value) showPcDownloadModal.value = false
else if (showInstallGuideModal.value) showInstallGuideModal.value = false
}
watch([showAgeGate, showPcDownloadModal, showInstallGuideModal], syncPageLock)
let revealObserver: IntersectionObserver | null = null
function initScrollReveal() {
if (typeof window === 'undefined') return
if (revealObserver) revealObserver.disconnect()
revealObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('rv')
revealObserver?.unobserve(entry.target)
}
})
},
{ rootMargin: '0px 0px -8% 0px', threshold: 0.05 },
)
const elements = document.querySelectorAll('[data-reveal]:not(.rv)')
elements.forEach((el) => {
if (el.getBoundingClientRect().top < window.innerHeight * 1.15) {
el.classList.add('rv')
} else {
revealObserver?.observe(el)
}
})
setTimeout(() => {
document.querySelectorAll('[data-reveal]:not(.rv)').forEach((el) => {
el.classList.add('rv')
})
}, 6000)
}
function switchView(view: 'landing' | 'works', anchor?: string) {
currentView.value = view
window.scrollTo({ top: 0, behavior: 'instant' as ScrollBehavior })
setTimeout(() => initScrollReveal(), 50)
if (anchor) {
setTimeout(() => {
const el = document.querySelector(anchor)
if (el) el.scrollIntoView({ behavior: 'smooth' })
}, 100)
}
}
function onScroll() {
const top = window.scrollY
isScrolled.value = top > 40
const triggerHeight = window.innerHeight * 0.6
const modalOpen = showAgeGate.value || showInstallGuideModal.value || showPcDownloadModal.value
if (top > triggerHeight && !modalOpen) {
if (!isFloatingQrDismissed.value) showFloatingQr.value = true
showMobileStickyBar.value = true
} else {
showFloatingQr.value = false
showMobileStickyBar.value = false
}
if (currentView.value === 'landing') {
const anchors = ['#works', '#about', '#faq']
for (const a of anchors) {
const el = document.querySelector(a)
if (el && el.getBoundingClientRect().top < window.innerHeight * 0.45) {
activeAnchor.value = a
}
}
}
}
let prevHtmlOverflow = ''
onMounted(() => {
prevHtmlOverflow = document.documentElement.style.overflow
document.documentElement.style.overflow = 'auto'
// 年龄确认暂时关闭
// const skipGate =
// route.query.nogate != null ||
// (typeof location !== 'undefined' && /(?:^|[?&])nogate(?:[=&]|$)/.test(location.search))
// let ageOk = false
// try {
// ageOk = sessionStorage.getItem(AGE_STORAGE_KEY) === '1'
// } catch {
// ageOk = false
// }
// showAgeGate.value = !skipGate && !ageOk
showAgeGate.value = false
syncPageLock()
setHero(0, true)
pageQrUrl.value = buildPageQrUrl()
initDownload()
window.addEventListener('scroll', onScroll, { passive: true })
window.addEventListener('keydown', onModalKeydown)
onScroll()
setTimeout(() => initScrollReveal(), 100)
if (route.query.view === 'works') {
currentView.value = 'works'
}
})
onBeforeUnmount(() => {
document.documentElement.style.overflow = prevHtmlOverflow || 'hidden'
document.body.classList.remove('landing-b-lock')
document.body.style.overflow = ''
pauseHeroTimer()
window.removeEventListener('scroll', onScroll)
window.removeEventListener('keydown', onModalKeydown)
if (toastTimer) clearTimeout(toastTimer)
if (revealObserver) revealObserver.disconnect()
})
return {
DRAMAS,
FEATURED,
GENRES,
FAQS,
assetUrl,
currentView,
currentHeroIndex,
isHeroTransitioning,
isHeroHovered,
heroProgKey,
bgFront,
bgAUrl,
bgBUrl,
topImgUrl,
currentHero,
setHero,
nextHero,
prevHero,
onHeroMouseEnter,
onHeroMouseLeave,
handleHeroTouchStart,
handleHeroTouchEnd,
wallDramas,
selectedKind,
selectedGenres,
showPcDownloadModal,
showInstallGuideModal,
installGuideTab,
iosInstallGuideTab,
showAgeGate,
confirmAge,
leaveSite,
openInstallGuide,
handleInstallGuideDownload,
handleInstallGuidePrimary,
targetDramaForDownload,
isScrolled,
showFloatingQr,
isFloatingQrDismissed,
showMobileStickyBar,
activeAnchor,
toastMessage,
isToastVisible,
pageQrUrl,
buttonList,
isIos,
download,
iosInstallUrl,
isSocialInApp,
guide,
hasIosShopPackage,
closeGuide,
showToast,
handleFilterClick,
setKind,
toggleGenre,
filteredTop5,
filteredLatest6,
filteredAllWorks,
faqOpenIndex,
toggleFaq,
handleDownload,
handleCopyIosLiteLink,
handleIosStoreAction,
handlePcAndroidDownload,
handleOpenAppDeepLink,
switchView,
}
}
@@ -1,6 +1,6 @@
<script setup lang="ts">
import LandingBPc from './LandingBPc.vue'
import LandingBMobile from './LandingBMobile.vue'
<script setup>
import Mobile from './Mobile.vue'
import Pc from './Pc.vue'
// 仅按 UA 分流,不按宽度切换
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
@@ -9,6 +9,6 @@ const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/
</script>
<template>
<LandingBMobile v-if="isMobile" />
<LandingBPc v-else />
<Mobile v-if="isMobile" />
<Pc v-else />
</template>
File diff suppressed because it is too large Load Diff
+851
View File
@@ -0,0 +1,851 @@
<template>
<div class="pcm-scroll" ref="scroller" @scroll.passive="onScroll">
<div class="pcm-navBar" ref="navBar" :style="{ height: navBarHeight }">
<div class="pcm-navInner" :style="scaleStyle">
<div class="pcm-logo">
<img class="pcm-logoImg" :src="logoImg" alt="" />
<img class="logoWord" :src="wordmarkImg" alt="黄果短剧" />
</div>
<nav class="pcm-menu">
<button
v-for="item in menus"
:key="item.key"
type="button"
class="pcm-menuItem"
:class="{ 'is-active': activeKey === item.key }"
@click="scrollToSection(item.key)"
>
<span class="pcm-menuLabel">
<span class="pcm-menuGhost">{{ item.label }}</span>
<span class="pcm-menuText">{{ item.label }}</span>
</span>
<i class="pcm-menuLine"></i>
</button>
</nav>
<button type="button" class="pcm-client" @click="openQr('手机扫码下载')">
<span class="pcm-clientIcon">
<img :src="downloadIcon" alt="" />
</span>
<span>客户端</span>
</button>
</div>
</div>
<div class="pcm-stage" :style="{ height: stageHeight + 'px' }">
<div class="pcm-page" ref="page" :style="scaleStyle">
<section class="pcm-hero" :ref="el => setSection('home', el)">
<img class="pcm-fill" :src="heroImg" alt="" />
<button type="button" class="pcm-btn pcm-btnDownload" @click="openQr('手机扫码下载')">
<span class="pcm-btnIcon">
<img class="pcm-iconLg" :src="downloadLgIcon" alt="" />
</span>
<span>下载黄果短剧</span>
</button>
<button type="button" class="pcm-btn pcm-btnExplore" @click="scrollToSection('genres')">
<span class="pcm-btnIcon pcm-btnIconBrand">
<img class="pcm-iconPlay" :src="playIcon" alt="" />
</span>
<span>探索精彩内容</span>
</button>
</section>
<section class="pcm-highlights" :ref="el => setSection('highlights', el)">
<img class="pcm-fill" :src="highlightsImg" alt="" loading="lazy" decoding="async" />
</section>
<section class="pcm-genres" :ref="el => setSection('genres', el)">
<img class="pcm-fill" :src="genresImg" alt="" loading="lazy" decoding="async" />
</section>
<section class="pcm-immersive">
<img class="pcm-fill" :src="immersiveImg" alt="" loading="lazy" decoding="async" />
</section>
<section class="pcm-wish" :ref="el => setSection('wish', el)">
<img class="pcm-fill" :src="wishImg" alt="" loading="lazy" decoding="async" />
<button type="button" class="pcm-btn pcm-btnWish" @click="openQr('扫码打开 App 许愿')">
<span class="pcm-btnIcon pcm-btnIconSm">
<img class="pcm-iconEdit" :src="editIcon" alt="" />
</span>
<span>去许愿</span>
</button>
</section>
<section class="pcm-creator" :ref="el => setSection('original', el)">
<img class="pcm-fill" :src="creatorImg" alt="" loading="lazy" decoding="async" />
</section>
<section class="pcm-about" :ref="el => setSection('about', el)">
<div class="pcm-aboutInner">
<div class="pcm-aboutLogo">
<img :src="logoImg" alt="" />
<img class="logoWord" :src="wordmarkImg" alt="黄果短剧" />
</div>
<div class="pcm-aboutCopy">
<p>打开黄果短剧,</p>
<p>遇见更多精彩故事</p>
</div>
</div>
</section>
<footer class="pcm-footer">
<div class="pcm-brand">
<div class="pcm-footerLogo">
<img :src="logoImg" alt="" />
<img class="logoWord" :src="wordmarkImg" alt="黄果短剧" />
</div>
<p class="pcm-brandSlogan">随时随地,打开好戏世界</p>
<div class="pcm-addresses">
<p>
<span>黄果短剧官网地址:</span>
<a :href="SITE_URL" target="_blank" rel="noopener noreferrer">{{ SITE_URL }}</a>
</p>
<p>
<span>黄果短剧导航站:</span>
<a :href="NAV_URL" target="_blank" rel="noopener noreferrer">{{ NAV_URL }}</a>
</p>
</div>
</div>
<div class="pcm-footCol pcm-footSupport">
<p class="pcm-footTitle">服务支持</p>
<p>常见问题</p>
</div>
<div class="pcm-footCol pcm-footAbout">
<p class="pcm-footTitle">关于我们</p>
<p>公司简介</p>
</div>
<div class="pcm-footCol pcm-footLegal">
<p class="pcm-footTitle">法律信息</p>
<p>用户协议</p>
<p>隐私政策</p>
</div>
<p class="pcm-copyright">Copyright © 2026 黄果短剧. All Rights Reserved.</p>
</footer>
</div>
</div>
<transition name="pcm-fade">
<div v-if="qrVisible" class="pcm-qrMask" @click="closeQr">
<div class="pcm-qrCard" @click.stop>
<div class="pcm-qrTitle">{{ qrTitle }}</div>
<div class="pcm-qrBox">
<QrcodeVue :value="pageUrl" :size="168" level="H" />
</div>
<div class="pcm-qrSub">支持 iOS 与 Android</div>
<button type="button" class="pcm-qrConfirm" @click="closeQr">我知道了</button>
</div>
</div>
</transition>
</div>
</template>
<script setup>
import { computed, onMounted, onBeforeUnmount, nextTick, ref } from 'vue'
import QrcodeVue from 'qrcode.vue'
import logoImg from '~/assets/image/land/logo-icon.png'
import wordmarkImg from '~/assets/image/land/wordmark.png'
import heroImg from '~/assets/image/land/hero.webp'
import highlightsImg from '~/assets/image/land/highlights.webp'
import genresImg from '~/assets/image/land/genres.webp'
import immersiveImg from '~/assets/image/land/immersive.webp'
import wishImg from '~/assets/image/land/wish.webp'
import creatorImg from '~/assets/image/land/creator.webp'
import downloadIcon from '~/assets/image/land/download.svg'
import downloadLgIcon from '~/assets/image/land/download-lg.svg'
import playIcon from '~/assets/image/land/play.svg'
import editIcon from '~/assets/image/land/edit.svg'
const PAGE_WIDTH = 1440
const NAV_HEIGHT = 88
const menus = [
{ key: 'home', label: '首页' },
{ key: 'highlights', label: '内容亮点' },
{ key: 'genres', label: '剧场类型' },
{ key: 'wish', label: '心愿榜' },
{ key: 'original', label: '原创内容' },
{ key: 'about', label: '关于我们' },
]
const scroller = ref(null)
const navBar = ref(null)
const page = ref(null)
const sections = {}
const setSection = (key, el) => {
if (el) sections[key] = el
}
const activeKey = ref('home')
const scale = ref(1)
const stageHeight = ref(0)
const qrVisible = ref(false)
const qrTitle = ref('手机扫码下载')
const pageUrl = window.location.href
// 主域名与导航站
const SITE_URL = 'https://hgaidj.app'
const NAV_URL = 'https://huanguoai.net/'
let locking = false
let scrollTimer = null
let scrollRaf = 0
const scaleStyle = computed(() => ({
width: PAGE_WIDTH + 'px',
transform: 'scale(' + scale.value + ')',
transformOrigin: 'left top',
}))
const navBarHeight = computed(() => scale.value * NAV_HEIGHT + 'px')
function updateScale() {
if (!scroller.value || !page.value) return
scale.value = scroller.value.clientWidth / PAGE_WIDTH
stageHeight.value = page.value.offsetHeight * scale.value
}
function onScroll() {
if (locking || scrollRaf) return
scrollRaf = requestAnimationFrame(() => {
scrollRaf = 0
syncActive()
})
}
function syncActive() {
const sc = scroller.value
const nav = navBar.value
if (!sc || !nav) return
if (sc.scrollTop + sc.clientHeight >= sc.scrollHeight - 4) {
activeKey.value = 'about'
return
}
const line = sc.getBoundingClientRect().top + nav.offsetHeight + 1
let current = menus[0].key
menus.forEach((item) => {
const el = sections[item.key]
if (el && el.getBoundingClientRect().top <= line) current = item.key
})
activeKey.value = current
}
function scrollToSection(key) {
if (activeKey.value === key) return
const sc = scroller.value
const section = sections[key]
const nav = navBar.value
if (!sc || !section || !nav) return
activeKey.value = key
const top = Math.max(0, section.offsetTop * scale.value)
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches
locking = true
sc.scrollTo({ top, behavior: reduce ? 'auto' : 'smooth' })
waitScrollEnd()
}
function waitScrollEnd() {
const sc = scroller.value
clearTimeout(scrollTimer)
const done = () => {
sc.removeEventListener('scroll', onEnd)
locking = false
syncActive()
}
const onEnd = () => {
clearTimeout(scrollTimer)
scrollTimer = setTimeout(done, 140)
}
sc.addEventListener('scroll', onEnd)
scrollTimer = setTimeout(done, 800)
}
function openQr(title) {
qrTitle.value = title
qrVisible.value = true
}
function closeQr() {
qrVisible.value = false
}
function onKeydown(e) {
if (e.key === 'Escape') closeQr()
}
onMounted(() => {
updateScale()
window.addEventListener('resize', updateScale)
document.addEventListener('keydown', onKeydown)
nextTick(updateScale)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateScale)
document.removeEventListener('keydown', onKeydown)
clearTimeout(scrollTimer)
if (scrollRaf) cancelAnimationFrame(scrollRaf)
})
</script>
<style lang="scss" scoped>
.pcm-scroll {
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
background: #0b0b0d;
position: relative;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
color: #f2f2f4;
button {
margin: 0;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
appearance: none;
}
button:focus {
outline: none;
}
button:focus-visible {
outline: 2px solid rgba(236, 35, 127, 0.85);
outline-offset: 3px;
}
}
.pcm-navBar {
position: sticky;
top: 0;
z-index: 20;
overflow: hidden;
background: rgba(11, 11, 13, 0.98);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.pcm-navInner {
position: relative;
height: 88px;
}
.pcm-logo {
position: absolute;
left: 40px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
gap: 6px;
.pcm-logoImg {
width: 52px;
height: 52px;
border-radius: 0;
display: block;
}
.logoWord {
height: 28px;
width: auto;
display: block;
}
span {
font-size: 23px;
font-weight: 600;
line-height: 28px;
letter-spacing: -0.23px;
white-space: nowrap;
}
}
.pcm-menu {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
gap: 48px;
}
.pcm-menuItem {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
border: 0;
background: transparent;
transition: opacity 120ms ease;
&:active {
opacity: 0.72;
}
}
.pcm-menuLabel {
display: grid;
font-size: 20px;
line-height: 26px;
}
.pcm-menuGhost,
.pcm-menuText {
grid-area: 1 / 1;
white-space: nowrap;
}
.pcm-menuGhost {
font-weight: 600;
visibility: hidden;
}
.pcm-menuText {
font-weight: 400;
color: #f2f2f4;
transition: color 200ms ease;
}
.pcm-menuItem.is-active .pcm-menuText {
font-weight: 600;
color: #ec237f;
}
.pcm-menuLine {
display: block;
width: 100%;
height: 2px;
border-radius: 5px;
background: #ec237f;
transform: scaleX(0.45);
opacity: 0;
transition: transform 220ms ease, opacity 220ms ease;
}
.pcm-menuItem.is-active .pcm-menuLine {
transform: scaleX(1);
opacity: 1;
}
.pcm-client {
position: absolute;
left: 1250px;
top: 50%;
border: 0;
transform: translateY(-50%);
width: 150px;
height: 50px;
border-radius: 33px;
display: flex;
align-items: center;
justify-content: center;
gap: 13px;
background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%);
transition: transform 150ms ease, filter 150ms ease;
span {
font-size: 20px;
font-weight: 600;
line-height: 35px;
color: #fff;
white-space: nowrap;
}
&:active {
transform: translateY(-50%) scale(0.97);
}
}
.pcm-clientIcon {
width: 24px;
height: 24px;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
img {
width: 16px;
height: 16px;
display: block;
}
}
.pcm-stage {
position: relative;
}
.pcm-page {
position: absolute;
left: 0;
top: 0;
}
.pcm-hero,
.pcm-highlights,
.pcm-genres,
.pcm-immersive,
.pcm-wish,
.pcm-creator {
position: relative;
width: 1440px;
overflow: hidden;
}
.pcm-hero {
height: 608px;
}
.pcm-highlights {
height: 186px;
}
.pcm-genres,
.pcm-immersive {
height: 864px;
}
.pcm-wish {
height: 956px;
}
.pcm-creator {
height: 768px;
}
.pcm-fill {
width: 1440px;
height: 100%;
display: block;
object-fit: cover;
}
.pcm-btn {
position: absolute;
z-index: 2;
border: 0;
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
transition: transform 150ms ease, filter 150ms ease;
&:active {
transform: scale(0.97);
}
span {
font-weight: 600;
color: #fff;
}
}
.pcm-btnIcon {
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.pcm-btnIconBrand {
background: #ec237f;
}
.pcm-iconLg {
width: 21px;
height: 21px;
display: block;
}
.pcm-iconPlay {
width: 21px;
height: 21px;
display: block;
}
.pcm-iconEdit {
width: 20px;
height: 20px;
display: block;
}
.pcm-btnDownload,
.pcm-btnExplore {
top: 484px;
width: 237px;
height: 64px;
border-radius: 32px;
gap: 13px;
span {
font-size: 22px;
line-height: 30px;
}
.pcm-btnIcon {
width: 36px;
height: 36px;
}
}
.pcm-btnDownload {
left: 743px;
background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%);
}
.pcm-btnExplore {
left: 1021px;
background: #1b1b1f;
border: 1.5px solid #ec237f;
span {
color: #f2f2f4;
}
}
.pcm-btnWish {
left: 567px;
top: 782px;
width: 306px;
height: 68px;
border-radius: 34px;
gap: 12px;
background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%);
span {
font-size: 22px;
line-height: 30px;
}
.pcm-btnIcon {
width: 35px;
height: 35px;
}
}
.pcm-about {
position: relative;
width: 1440px;
height: 346px;
background: linear-gradient(90deg, #0b0b0d 0%, #1d0812 50%, #0b0b0d 100%);
}
.pcm-aboutInner {
position: absolute;
left: 393px;
top: 117px;
display: flex;
align-items: center;
gap: 64px;
}
.pcm-aboutLogo {
display: flex;
align-items: center;
gap: 11px;
img {
width: 96px;
height: 96px;
border-radius: 0;
display: block;
}
.logoWord {
height: 48px;
width: auto;
display: block;
}
span {
font-size: 42px;
font-weight: 600;
line-height: 51px;
letter-spacing: -0.42px;
white-space: nowrap;
}
}
.pcm-aboutCopy {
font-size: 34px;
font-weight: 600;
line-height: 56px;
letter-spacing: 6px;
white-space: nowrap;
p {
margin: 0;
}
}
.pcm-footer {
position: relative;
width: 1440px;
height: 265px;
background: #141417;
border-top: 1px solid rgba(255, 255, 255, 0.06);
box-sizing: border-box;
}
.pcm-brand {
position: absolute;
left: 40px;
top: 37px;
display: flex;
flex-direction: column;
gap: 14px;
}
.pcm-footerLogo {
display: flex;
align-items: center;
gap: 4px;
img {
width: 36px;
height: 36px;
border-radius: 0;
display: block;
}
.logoWord {
height: 19px;
width: auto;
display: block;
}
span {
font-size: 16px;
font-weight: 600;
line-height: 19px;
letter-spacing: -0.16px;
}
}
.pcm-brandSlogan,
.pcm-addresses {
margin: 0;
color: #6b6b74;
font-size: 12px;
line-height: 17px;
font-weight: 400;
}
.pcm-addresses {
display: flex;
flex-direction: column;
gap: 8px;
p {
margin: 0;
white-space: nowrap;
}
a {
color: #ec237f;
text-decoration: none;
transition: opacity 150ms ease;
&:hover {
opacity: 0.8;
}
}
}
.pcm-footCol {
position: absolute;
top: 37px;
display: flex;
flex-direction: column;
gap: 13px;
font-size: 14px;
line-height: 20px;
color: #6b6b74;
white-space: nowrap;
p {
margin: 0;
}
}
.pcm-footTitle {
color: #f2f2f4;
font-weight: 600;
}
.pcm-footSupport {
left: 770px;
}
.pcm-footAbout {
left: 1010px;
}
.pcm-footLegal {
left: 1250px;
}
.pcm-copyright {
position: absolute;
left: 1128px;
top: 227px;
margin: 0;
color: #6b6b74;
font-size: 12px;
line-height: 17px;
white-space: nowrap;
}
.pcm-qrMask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 30;
display: flex;
align-items: center;
justify-content: center;
background: rgba(11, 11, 13, 0.72);
}
.pcm-qrCard {
width: 320px;
padding: 28px 24px 24px;
box-sizing: border-box;
border-radius: 16px;
background: #1b1b1f;
border: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
align-items: center;
}
.pcm-qrTitle {
font-size: 20px;
font-weight: 600;
line-height: 28px;
color: #f2f2f4;
}
.pcm-qrBox {
width: 180px;
height: 180px;
margin-top: 16px;
border-radius: 12px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
color: #6b6b74;
font-size: 14px;
}
.pcm-qrImg {
width: 168px;
height: 168px;
display: block;
}
.pcm-qrSub {
margin-top: 12px;
color: #6b6b74;
font-size: 13px;
line-height: 18px;
}
.pcm-qrConfirm {
width: 100%;
height: 44px;
border: 0;
margin-top: 20px;
border-radius: 22px;
background: linear-gradient(135deg, #ff4a7b 0%, #ec237f 52%, #93226f 100%);
color: #fff;
font-size: 16px;
font-weight: 600;
transition: transform 150ms ease;
&:active {
transform: scale(0.97);
}
}
.pcm-fade-enter-active,
.pcm-fade-leave-active {
transition: opacity 200ms ease;
}
.pcm-fade-enter,
.pcm-fade-leave-to {
opacity: 0;
}
</style>
+2 -2
View File
@@ -1,9 +1,9 @@
<script setup>
import LandingB from '~/components/landing-b/LandingB.vue'
import Landing from '~/components/landing-hg2/Landing.vue'
</script>
<template>
<LandingB />
<Landing />
</template>
<style scoped></style>
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup>
import LandingB from '~/components/landing-b/LandingB.vue'
import Landing from '~/components/landing-hg2/Landing.vue'
</script>
<template>
<LandingB />
<Landing />
</template>
+22
View File
@@ -3,6 +3,23 @@ import AutoImport from 'unplugin-auto-import/vite';
import vue from '@vitejs/plugin-vue'
import path from 'path';
// 新落地页移动端样式按 375 设计稿写 px,这里统一转 rem(根字号见 main.js:width/7.5,375 宽时 1rem=50px)
const landingPx2Rem = () => ({
postcssPlugin: 'landing-hg2-px2rem',
Declaration(decl) {
const file = (decl.source && decl.source.input && decl.source.input.file) || ''
if (!file.includes('landing-hg2/Mobile.vue')) return
if (!/\d*\.?\d+px/.test(decl.value)) return
decl.value = decl.value.replace(/(-?\d*\.?\d+)px/g, (m, n) => {
const v = parseFloat(n)
if (!v || Math.abs(v) < 0.5) return m
return +(v / 50).toFixed(5) + 'rem'
})
},
})
landingPx2Rem.postcss = true
export default defineConfig({
plugins: [vue(),AutoImport({
imports: [
@@ -10,6 +27,11 @@ export default defineConfig({
'vue-router',
],
})],
css: {
postcss: {
plugins: [landingPx2Rem()],
},
},
resolve: {
alias: {
'~': path.resolve(__dirname, 'src'),