2026-09-15 16:16:55 +08:00
|
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
import { onResize } from '@dcloudio/uni-app'
|
|
|
|
|
|
|
|
|
|
export type PageLayout = {
|
|
|
|
|
pageStyle: () => string,
|
|
|
|
|
contentStyle: (offset: number, minimum: boolean) => string,
|
|
|
|
|
statusStyle: (offset: number) => string,
|
2026-09-20 14:18:55 +08:00
|
|
|
navigationStyle: () => string,
|
|
|
|
|
navigationTitleStyle: () => string,
|
2026-09-21 15:04:59 +08:00
|
|
|
navigationActionStyle: (headerHeight: number, actionCount: number) => string,
|
2026-09-20 14:18:55 +08:00
|
|
|
navigationBrandStyle: (headerHeight: number) => string,
|
2026-09-15 16:16:55 +08:00
|
|
|
bottomStyle: (padding: number) => string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 原生渲染不支持 vh/calc,统一以窗口尺寸计算,并随旋转更新。
|
|
|
|
|
export function usePageLayout(): PageLayout {
|
|
|
|
|
const windowInfo = ref(uni.getWindowInfo())
|
2026-09-20 14:18:55 +08:00
|
|
|
const capsuleLeft = ref(-1)
|
|
|
|
|
const capsuleTop = ref(0)
|
|
|
|
|
const capsuleHeight = ref(0)
|
|
|
|
|
const readCapsule = () => {
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
try {
|
|
|
|
|
const capsule = uni.getMenuButtonBoundingClientRect()
|
|
|
|
|
const valid = capsule.width > 0 && capsule.height > 0 && capsule.left > 0 && capsule.left < windowInfo.value.windowWidth
|
|
|
|
|
capsuleLeft.value = valid ? capsule.left : -1
|
|
|
|
|
capsuleTop.value = valid ? capsule.top : 0
|
|
|
|
|
capsuleHeight.value = valid ? capsule.height : 0
|
|
|
|
|
} catch (_error) { capsuleLeft.value = -1; capsuleTop.value = 0; capsuleHeight.value = 0 }
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
|
|
|
|
readCapsule()
|
|
|
|
|
const refresh = () => { windowInfo.value = uni.getWindowInfo(); readCapsule() }
|
2026-09-15 16:16:55 +08:00
|
|
|
onMounted(() => refresh())
|
|
|
|
|
// #ifdef APP
|
|
|
|
|
onResize((_options) => { refresh() })
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifndef APP
|
|
|
|
|
const handleResize = (_options: OnWindowResizeCallbackResult) => { refresh() }
|
|
|
|
|
uni.onWindowResize(handleResize)
|
|
|
|
|
onUnmounted(() => { uni.offWindowResize(handleResize) })
|
|
|
|
|
// #endif
|
|
|
|
|
const rpx = (value: number): number => value * windowInfo.value.windowWidth / 750
|
2026-09-20 14:18:55 +08:00
|
|
|
const status = (): number => Math.max(windowInfo.value.statusBarHeight ?? 0, windowInfo.value.safeAreaInsets?.top ?? 0)
|
|
|
|
|
const navigationStyle = (): string => {
|
|
|
|
|
// 只在微信端横向避让胶囊,保持业务导航原有高度。
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
const reserved = capsuleLeft.value > 0 ? windowInfo.value.windowWidth - capsuleLeft.value + 8 : 104
|
|
|
|
|
return 'padding-right:' + reserved + 'px;'
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
return ''
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
|
|
|
|
const navigationTitleStyle = (): string => {
|
|
|
|
|
// 标题左右使用相同留白,以屏幕中心对齐,同时避开微信胶囊。
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
const reserved = capsuleLeft.value > 0 ? windowInfo.value.windowWidth - capsuleLeft.value + 8 : 104
|
|
|
|
|
const inset = Math.max(rpx(112), reserved)
|
|
|
|
|
return 'left:' + inset + 'px;right:' + inset + 'px;'
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
return ''
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
2026-09-21 15:04:59 +08:00
|
|
|
const navigationActionStyle = (headerHeight: number, actionCount: number): string => {
|
|
|
|
|
// 微信操作按钮与胶囊等高、顶部对齐,整组向左展开;App 保持导航行内居中。
|
2026-09-20 14:18:55 +08:00
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
const size = capsuleHeight.value > 0 ? capsuleHeight.value : 32
|
|
|
|
|
const top = capsuleHeight.value > 0 ? Math.max(0, capsuleTop.value - status()) : Math.max(0, (rpx(headerHeight) - size) / 2)
|
|
|
|
|
const right = capsuleLeft.value > 0 ? windowInfo.value.windowWidth - capsuleLeft.value + 8 : 104
|
2026-09-21 15:04:59 +08:00
|
|
|
const width = size * actionCount + rpx(16) * Math.max(0, actionCount - 1)
|
|
|
|
|
return 'position:absolute;top:' + top + 'px;right:' + right + 'px;width:' + width + 'px;height:' + size + 'px;'
|
2026-09-20 14:18:55 +08:00
|
|
|
// #endif
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
return ''
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
|
|
|
|
const navigationBrandStyle = (headerHeight: number): string => {
|
|
|
|
|
// Logo 保留自身比例,与胶囊共享垂直中心,不改变登录页和启动页的 Logo。
|
|
|
|
|
// #ifdef MP-WEIXIN
|
|
|
|
|
const height = rpx(68)
|
|
|
|
|
const center = capsuleHeight.value > 0 ? capsuleTop.value - status() + capsuleHeight.value / 2 : rpx(headerHeight) / 2
|
|
|
|
|
return 'align-self:flex-start;margin-top:' + Math.max(0, center - height / 2) + 'px;height:' + height + 'px;'
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifndef MP-WEIXIN
|
|
|
|
|
return ''
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
2026-09-15 16:16:55 +08:00
|
|
|
const pageStyle = (): string => 'min-height:' + windowInfo.value.windowHeight + 'px;'
|
|
|
|
|
const contentStyle = (offset: number, minimum: boolean): string => {
|
|
|
|
|
// #ifdef WEB
|
|
|
|
|
return (minimum ? 'min-height:' : 'height:') + 'calc(100vh - ' + status() + 'px - ' + offset + 'rpx);'
|
|
|
|
|
// #endif
|
|
|
|
|
// #ifndef WEB
|
|
|
|
|
const height = Math.max(0, windowInfo.value.windowHeight - status() - rpx(offset))
|
|
|
|
|
return (minimum ? 'min-height:' : 'height:') + height + 'px;'
|
|
|
|
|
// #endif
|
|
|
|
|
}
|
|
|
|
|
const statusStyle = (offset: number): string => 'height:' + (status() + rpx(offset)) + 'px;'
|
|
|
|
|
const bottomStyle = (padding: number): string => 'padding-bottom:' + (rpx(padding) + (windowInfo.value.safeAreaInsets?.bottom ?? 0)) + 'px;'
|
2026-09-20 14:18:55 +08:00
|
|
|
return { pageStyle, contentStyle, statusStyle, navigationStyle, navigationTitleStyle, navigationActionStyle, navigationBrandStyle, bottomStyle }
|
2026-09-15 16:16:55 +08:00
|
|
|
}
|