This commit is contained in:
zlwu committed 2026-08-01 17:29:37 +08:00
1 parent 06956b742b
commit e59b2aa6cb
104 files changed
+653227 -7621

No files matched your search

+61 -97
View File
@@ -1,29 +1,24 @@
/**
* useLayoutHeight - 页面布局高度管理
*
* 自动计算和管理页面内容区域的高度,确保内容区域能够正确填充剩余空间。
* 监听头部元素高度变化,动态调整内容区域高度,避免出现滚动条或布局错乱。
*
* ## 主要功能
*
* 1. 动态高度计算 - 根据头部元素高度自动计算内容区域高度
* 2. 响应式监听 - 自动监听元素尺寸变化并更新高度
* 3. CSS 变量同步 - 自动更新 CSS 变量,方便全局使用
* 4. 灵活配置 - 支持自定义间距、CSS 变量名等
* 5. 自动查找模式 - 提供通过 ID 自动查找元素的便捷方式
* 以实际内容滚动容器 `#app-content` 为高度来源,同步 `--art-full-height`。
* 页面已在该容器内滚动,禁止再用 `calc(100vh - 顶栏)` 给业务页强制 minHeight,
* 否则门户布局下(顶栏为 `#app-portal-header`)会量不到顶栏,叠出多余纵向滚动条与底部空白。
*
* @module useLayoutHeight
* @author Art Design Pro Team
*/
import { ref, computed, watch, onMounted } from 'vue'
import { ref, computed, watch, onMounted, type Ref } from 'vue'
import { useElementSize } from '@vueuse/core'
/**
* 页面容器高度配置
*/
interface LayoutHeightOptions {
/** 额外的间距(默认 15px) */
/**
* @deprecated 已不再用于 100vh 扣减;保留参数以免调用方类型报错
*/
extraSpacing?: number
/** 是否自动更新 CSS 变量(默认 true) */
updateCssVar?: boolean
@@ -31,26 +26,21 @@ interface LayoutHeightOptions {
cssVarName?: string
}
export function useLayoutHeight(options: LayoutHeightOptions = {}) {
const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
function syncFullHeightCssVar(
availableHeight: Ref<number>,
contentHeaderHeight: Ref<number>,
options: LayoutHeightOptions
) {
const { updateCssVar = true, cssVarName = '--art-full-height' } = options
// 元素引用
const headerRef = ref<HTMLElement>()
const contentHeaderRef = ref<HTMLElement>()
// 使用 VueUse 自动监听元素尺寸变化
const { height: headerHeight } = useElementSize(headerRef)
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
// 计算容器最小高度(响应式)
const containerMinHeight = computed(() => {
const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing
return `calc(100vh - ${totalHeight}px)`
const fullHeight = computed(() => {
const available = Math.max(0, Math.round(availableHeight.value - contentHeaderHeight.value))
return available > 0 ? `${available}px` : '100%'
})
if (updateCssVar) {
watch(
containerMinHeight,
fullHeight,
(newHeight) => {
requestAnimationFrame(() => {
document.documentElement.style.setProperty(cssVarName, newHeight)
@@ -60,89 +50,63 @@ export function useLayoutHeight(options: LayoutHeightOptions = {}) {
)
}
return fullHeight
}
function resolveHeaderEl(): HTMLElement | undefined {
if (typeof document === 'undefined') return undefined
return (
document.getElementById('app-portal-header') ??
document.getElementById('app-header') ??
undefined
)
}
export function useLayoutHeight(options: LayoutHeightOptions = {}) {
const appContentRef = ref<HTMLElement>()
const headerRef = ref<HTMLElement>()
const contentHeaderRef = ref<HTMLElement>()
const { height: appContentHeight } = useElementSize(appContentRef)
const { height: headerHeight } = useElementSize(headerRef)
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
/** @deprecated 请勿再用于业务页 minHeight;始终为空字符串 */
const containerMinHeight = computed(() => '' as string)
syncFullHeightCssVar(appContentHeight, contentHeaderHeight, options)
onMounted(() => {
requestAnimationFrame(() => {
if (typeof document === 'undefined') return
appContentRef.value = document.getElementById('app-content') ?? undefined
contentHeaderRef.value = document.getElementById('app-content-header') ?? undefined
if (!headerRef.value) {
headerRef.value = resolveHeaderEl()
}
})
})
return {
/** 容器最小高度(响应式) */
containerMinHeight,
/** 头部元素引用 */
headerRef,
/** 内容头部元素引用 */
contentHeaderRef,
/** 头部高度(响应式) */
headerHeight,
/** 内容头部高度(响应式) */
contentHeaderHeight
contentHeaderHeight,
appContentHeight
}
}
/**
* 通过 ID 自动查找元素的布局高度管理
* 适用于无法直接获取元素引用的场景
* 通过 ID 自动查找内容区,同步全高 CSS 变量。
* 默认不强制业务页 minHeight(由 ArtPageContent 决定)。
*
* @param headerIds 头部元素的 ID 数组
* @param _headerIds 兼容旧签名,已忽略(改为测量 `#app-content`)
* @param options 配置选项
*
* ```
*/
export function useAutoLayoutHeight(
headerIds: string[] = ['app-header', 'app-content-header'],
_headerIds: string[] = ['app-header', 'app-content-header'],
options: LayoutHeightOptions = {}
) {
const { extraSpacing = 15, updateCssVar = true, cssVarName = '--art-full-height' } = options
// 创建元素引用
const headerRef = ref<HTMLElement>()
const contentHeaderRef = ref<HTMLElement>()
// 使用 VueUse 自动监听元素尺寸变化
const { height: headerHeight } = useElementSize(headerRef)
const { height: contentHeaderHeight } = useElementSize(contentHeaderRef)
// 计算容器最小高度(响应式)
const containerMinHeight = computed(() => {
const totalHeight = headerHeight.value + contentHeaderHeight.value + extraSpacing
return `calc(100vh - ${totalHeight}px)`
})
if (updateCssVar) {
watch(
containerMinHeight,
(newHeight) => {
requestAnimationFrame(() => {
document.documentElement.style.setProperty(cssVarName, newHeight)
})
},
{ immediate: true }
)
}
// 在 DOM 挂载后查找元素
onMounted(() => {
if (typeof document !== 'undefined') {
// 使用 nextTick 确保 DOM 完全渲染
requestAnimationFrame(() => {
const header = document.getElementById(headerIds[0])
const contentHeader = document.getElementById(headerIds[1])
if (header) {
headerRef.value = header
}
if (contentHeader) {
contentHeaderRef.value = contentHeader
}
})
}
})
return {
/** 容器最小高度(响应式) */
containerMinHeight,
/** 头部元素引用 */
headerRef,
/** 内容头部元素引用 */
contentHeaderRef,
/** 头部高度(响应式) */
headerHeight,
/** 内容头部高度(响应式) */
contentHeaderHeight
}
return useLayoutHeight(options)
}