feat(h5): 小程序原生顶栏 + 本地联调与一键部署
nh=1 隐藏 H5 顶栏并由 cover 承载 logo;H5 仅 4px 顶距避免重复留白。新增 dev:mp、deploy:h5 与联调文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="app-shell" :class="{ 'mp-embed': mpEmbed }">
|
||||
<div class="app-shell" :class="shellClass">
|
||||
<div class="app-body">
|
||||
<AppHeader />
|
||||
<AppHeader v-if="showHeader" />
|
||||
<router-view />
|
||||
</div>
|
||||
<IcpFooter class="app-footer" />
|
||||
@@ -10,10 +10,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import IcpFooter from './components/IcpFooter.vue'
|
||||
import TabBar from './components/TabBar.vue'
|
||||
import { isMpEmbed } from './lib/mpEmbed'
|
||||
import { isMpEmbed, isMpNativeHeader } from './lib/mpEmbed'
|
||||
|
||||
const mpEmbed = isMpEmbed()
|
||||
const mpNativeHeader = isMpNativeHeader()
|
||||
const showHeader = !mpNativeHeader
|
||||
const shellClass = computed(() => ({
|
||||
'mp-embed': mpEmbed,
|
||||
'mp-native-header': mpNativeHeader,
|
||||
}))
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { applyMpEmbedDom, initMpEmbed, isMpEmbed } from './mpEmbed'
|
||||
import { applyMpEmbedDom, initMpEmbed, isMpEmbed, isMpNativeHeader } from './mpEmbed'
|
||||
|
||||
describe('mpEmbed', () => {
|
||||
beforeEach(() => {
|
||||
@@ -28,6 +28,15 @@ describe('mpEmbed', () => {
|
||||
expect(document.documentElement.style.getPropertyValue('--yxg-safe-top')).toBe('47px')
|
||||
})
|
||||
|
||||
it('uses native mini-program header mode with ?nh=1', () => {
|
||||
window.history.replaceState({}, '', '/psy/?mp=1&nh=1')
|
||||
initMpEmbed()
|
||||
expect(isMpNativeHeader()).toBe(true)
|
||||
expect(document.documentElement.classList.contains('mp-native-header')).toBe(true)
|
||||
expect(document.documentElement.style.getPropertyValue('--yxg-safe-top')).toBe('0px')
|
||||
expect(document.documentElement.style.getPropertyValue('--yxg-page-top')).toBe('4px')
|
||||
})
|
||||
|
||||
it('returns false without mp query', () => {
|
||||
initMpEmbed()
|
||||
expect(isMpEmbed()).toBe(false)
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
const MP_EMBED_KEY = 'yxg_mp_embed'
|
||||
const MP_NATIVE_HEADER_KEY = 'yxg_mp_native_header'
|
||||
const MP_QUERY = 'mp'
|
||||
const NH_QUERY = 'nh'
|
||||
const SBH_QUERY = 'sbh'
|
||||
const SBH_KEY = 'yxg_sbh'
|
||||
const HEADER_H_PX = 50
|
||||
const MP_CONTENT_TOP_PX = 4
|
||||
|
||||
/** Native cover-view handles logo; H5 only needs a tiny gap below web-view top. */
|
||||
function applyNativeHeaderMode(): void {
|
||||
if (typeof document === 'undefined') return
|
||||
const root = document.documentElement
|
||||
root.style.setProperty('--yxg-safe-top', '0px')
|
||||
root.style.setProperty('--yxg-page-top', `${MP_CONTENT_TOP_PX}px`)
|
||||
}
|
||||
|
||||
function applySafeTopInset(px: number): void {
|
||||
if (typeof document === 'undefined' || px <= 0) return
|
||||
@@ -19,11 +30,22 @@ function readStoredSbh(): number {
|
||||
return parseInt(raw, 10)
|
||||
}
|
||||
|
||||
/** True when mini-program renders the brand header above web-view. */
|
||||
export function isMpNativeHeader(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
return sessionStorage.getItem(MP_NATIVE_HEADER_KEY) === '1'
|
||||
}
|
||||
|
||||
/** Apply document-level embed chrome (safe-area wash, hide HTML title). */
|
||||
export function applyMpEmbedDom(): void {
|
||||
if (typeof document === 'undefined' || !isMpEmbed()) return
|
||||
document.title = '\u200b'
|
||||
document.documentElement.classList.add('mp-embed')
|
||||
if (isMpNativeHeader()) {
|
||||
document.documentElement.classList.add('mp-native-header')
|
||||
applyNativeHeaderMode()
|
||||
return
|
||||
}
|
||||
const stored = readStoredSbh()
|
||||
if (stored > 0) applySafeTopInset(stored)
|
||||
}
|
||||
@@ -35,10 +57,16 @@ export function initMpEmbed(): void {
|
||||
if (params.get(MP_QUERY) === '1') {
|
||||
sessionStorage.setItem(MP_EMBED_KEY, '1')
|
||||
}
|
||||
const sbh = params.get(SBH_QUERY)
|
||||
if (sbh && /^\d+$/.test(sbh)) {
|
||||
sessionStorage.setItem(SBH_KEY, sbh)
|
||||
applySafeTopInset(parseInt(sbh, 10))
|
||||
if (params.get(NH_QUERY) === '1') {
|
||||
sessionStorage.setItem(MP_NATIVE_HEADER_KEY, '1')
|
||||
applyNativeHeaderMode()
|
||||
}
|
||||
if (!isMpNativeHeader()) {
|
||||
const sbh = params.get(SBH_QUERY)
|
||||
if (sbh && /^\d+$/.test(sbh)) {
|
||||
sessionStorage.setItem(SBH_KEY, sbh)
|
||||
applySafeTopInset(parseInt(sbh, 10))
|
||||
}
|
||||
}
|
||||
applyMpEmbedDom()
|
||||
}
|
||||
|
||||
@@ -57,8 +57,12 @@ router.beforeEach((to) => {
|
||||
q.mp = '1'
|
||||
changed = true
|
||||
}
|
||||
if (sessionStorage.getItem('yxg_mp_native_header') === '1' && q.nh !== '1') {
|
||||
q.nh = '1'
|
||||
changed = true
|
||||
}
|
||||
const storedSbh = sessionStorage.getItem('yxg_sbh')
|
||||
if (storedSbh && q.sbh !== storedSbh) {
|
||||
if (storedSbh && q.sbh !== storedSbh && q.nh !== '1') {
|
||||
q.sbh = storedSbh
|
||||
changed = true
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ html.mp-embed body{
|
||||
.app-shell.mp-embed .home,
|
||||
.app-shell.mp-embed .ps{
|
||||
margin-top:0;
|
||||
padding-top:8px;
|
||||
padding-top:var(--yxg-page-top);
|
||||
min-height:100%;
|
||||
}
|
||||
.app-footer{
|
||||
|
||||
Reference in New Issue
Block a user