212 lines
5.4 KiB
TypeScript
212 lines
5.4 KiB
TypeScript
import type { AdminLayoutMenuItem } from '@/components/layouts/main-layout/types'
|
|
import { computed, onBeforeUnmount, onMounted, ref, type Ref, watch } from 'vue'
|
|
|
|
type ToggleSidebarPayload = {
|
|
drawer: boolean
|
|
rail: boolean
|
|
}
|
|
|
|
type UseAdminLayoutStateOptions = {
|
|
appBarRef: Ref<unknown>
|
|
breadcrumbBarVisible: Ref<boolean | null>
|
|
emitUpdateBreadcrumbBarVisible: (value: boolean) => void
|
|
emitUpdateFavoritesBarVisible: (value: boolean) => void
|
|
emitUpdateIsRail: (value: boolean) => void
|
|
favoritesBarVisible: Ref<boolean | null>
|
|
isMobile: Ref<boolean>
|
|
isRail: Ref<boolean | null> // 必須為 Ref,確保父層 prop 更新時 getter 能即時反映
|
|
menuItems: AdminLayoutMenuItem[]
|
|
onToggleSidebar: (payload: ToggleSidebarPayload) => void
|
|
}
|
|
|
|
export function useAdminLayoutState(options: UseAdminLayoutStateOptions) {
|
|
const drawer = ref(true)
|
|
const mobileFavoritesPanel = ref(false)
|
|
const mobileMenuPath = ref<AdminLayoutMenuItem[]>([])
|
|
const localBreadcrumbBarVisible = ref(true)
|
|
const localFavoritesBarVisible = ref(true)
|
|
const localIsRail = ref(false)
|
|
const opened = ref<string[]>([])
|
|
const appBarHeight = ref(0)
|
|
|
|
const isRail = computed({
|
|
get: () => options.isRail.value ?? localIsRail.value,
|
|
set: (value: boolean) => {
|
|
if (options.isRail.value === null) {
|
|
localIsRail.value = value
|
|
return
|
|
}
|
|
|
|
options.emitUpdateIsRail(value)
|
|
},
|
|
})
|
|
|
|
const showFavoritesBar = computed({
|
|
get: () => options.favoritesBarVisible.value ?? localFavoritesBarVisible.value,
|
|
set: (value: boolean) => {
|
|
if (options.favoritesBarVisible.value === null) {
|
|
localFavoritesBarVisible.value = value
|
|
return
|
|
}
|
|
|
|
options.emitUpdateFavoritesBarVisible(value)
|
|
},
|
|
})
|
|
|
|
const breadcrumbBarVisible = computed({
|
|
get: () => options.breadcrumbBarVisible.value ?? localBreadcrumbBarVisible.value,
|
|
set: (value: boolean) => {
|
|
if (options.breadcrumbBarVisible.value === null) {
|
|
localBreadcrumbBarVisible.value = value
|
|
return
|
|
}
|
|
|
|
options.emitUpdateBreadcrumbBarVisible(value)
|
|
},
|
|
})
|
|
|
|
const mobileCurrentItems = computed(() =>
|
|
mobileMenuPath.value.reduce(
|
|
(items, currentItem) => currentItem?.subItems ?? [],
|
|
options.menuItems || []
|
|
)
|
|
)
|
|
|
|
const mobileCurrentLevel = computed(() => mobileMenuPath.value.length + 1)
|
|
|
|
const mobileMenuLevels = computed(() =>
|
|
Array.from({ length: mobileCurrentLevel.value }, (_, index) => ({
|
|
level: index + 1,
|
|
title:
|
|
index === 0 ? '主選單' : (mobileMenuPath.value[index - 1]?.title ?? `第${index + 1}層`),
|
|
}))
|
|
)
|
|
|
|
function resetMobilePanels() {
|
|
mobileFavoritesPanel.value = false
|
|
mobileMenuPath.value = []
|
|
}
|
|
|
|
function toggleSidebar() {
|
|
if (options.isMobile.value) {
|
|
drawer.value = !drawer.value
|
|
} else {
|
|
isRail.value = !isRail.value
|
|
}
|
|
|
|
options.onToggleSidebar({
|
|
drawer: drawer.value,
|
|
rail: isRail.value,
|
|
})
|
|
}
|
|
|
|
function goToMobileLevel(level: number) {
|
|
mobileFavoritesPanel.value = false
|
|
mobileMenuPath.value = mobileMenuPath.value.slice(0, Math.max(0, level - 1))
|
|
}
|
|
|
|
function openMobileFavoritesPanel() {
|
|
mobileMenuPath.value = []
|
|
mobileFavoritesPanel.value = true
|
|
}
|
|
|
|
function handleMobileMenuClick(
|
|
item: AdminLayoutMenuItem,
|
|
onSelect: (selectedItem: AdminLayoutMenuItem) => void
|
|
) {
|
|
if (item?.subItems?.length) {
|
|
mobileMenuPath.value = [...mobileMenuPath.value, item]
|
|
return
|
|
}
|
|
|
|
onSelect(item)
|
|
}
|
|
|
|
function handleSelectFavorite(
|
|
item: AdminLayoutMenuItem,
|
|
onSelect: (selectedItem: AdminLayoutMenuItem) => void
|
|
) {
|
|
onSelect(item)
|
|
mobileFavoritesPanel.value = false
|
|
}
|
|
|
|
function toggleFavoritesBar(nextValue?: boolean) {
|
|
showFavoritesBar.value = typeof nextValue === 'boolean' ? nextValue : !showFavoritesBar.value
|
|
}
|
|
|
|
function handleUnshrink() {
|
|
isRail.value = false
|
|
}
|
|
|
|
let appBarObserver: ResizeObserver | null = null
|
|
|
|
function resolveObservedElement() {
|
|
const target = options.appBarRef.value as HTMLElement | { $el?: HTMLElement } | null
|
|
if (!target) return null
|
|
if (target instanceof HTMLElement) return target
|
|
return target.$el ?? null
|
|
}
|
|
|
|
function updateAppBarHeight() {
|
|
const el = resolveObservedElement()
|
|
if (!el) return
|
|
appBarHeight.value = Math.round(el.getBoundingClientRect().height || 0)
|
|
}
|
|
|
|
onMounted(() => {
|
|
updateAppBarHeight()
|
|
if (typeof ResizeObserver === 'undefined') return
|
|
|
|
const el = resolveObservedElement()
|
|
if (!el) return
|
|
|
|
appBarObserver = new ResizeObserver(() => updateAppBarHeight())
|
|
appBarObserver.observe(el)
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
if (!appBarObserver) return
|
|
appBarObserver.disconnect()
|
|
appBarObserver = null
|
|
})
|
|
|
|
watch(options.isMobile, (value) => {
|
|
if (!value) {
|
|
resetMobilePanels()
|
|
}
|
|
})
|
|
|
|
watch(drawer, (value) => {
|
|
if (!value) {
|
|
resetMobilePanels()
|
|
}
|
|
})
|
|
|
|
const mainStyle = computed(() => ({
|
|
paddingTop: appBarHeight.value ? `${appBarHeight.value}px` : undefined,
|
|
height: '100vh',
|
|
minHeight: 0,
|
|
flex: '1 1 0',
|
|
}))
|
|
|
|
return {
|
|
drawer,
|
|
goToMobileLevel,
|
|
handleMobileMenuClick,
|
|
handleSelectFavorite,
|
|
handleUnshrink,
|
|
isRail,
|
|
mainStyle,
|
|
mobileCurrentItems,
|
|
mobileCurrentLevel,
|
|
mobileFavoritesPanel,
|
|
mobileMenuLevels,
|
|
openMobileFavoritesPanel,
|
|
opened,
|
|
breadcrumbBarVisible,
|
|
showFavoritesBar,
|
|
toggleFavoritesBar,
|
|
toggleSidebar,
|
|
}
|
|
}
|