Files
skt-vuetify-templates/src/composables/layout/useAppShell.ts
T
2026-05-19 16:38:08 +08:00

281 lines
7.8 KiB
TypeScript

import {
mdiCloseCircle,
mdiCog,
mdiFileDocumentOutline,
mdiFileTreeOutline,
mdiHome,
mdiPlusCircle,
mdiTableEdit,
} from '@mdi/js'
import { computed, onBeforeUnmount, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { SESSION_FORCE_LOGOUT_EVENT } from '@/services/session'
import { useAuthStore } from '@/stores/auth'
import { useBreadcrumbStore } from '@/stores/breadcrumbs'
import { useFavoritesStore } from '@/stores/favorites'
import { useMenuStore, type LayoutMenuItem } from '@/stores/menu'
import { useMessageStore } from '@/stores/messages'
import { useSnackbarStore } from '@/stores/snackbar'
const fixedMenuItems: LayoutMenuItem[] = [
{
title: '資料維護',
navigable: false,
subItems: [
{ title: '單筆資料維護', icon: mdiFileDocumentOutline, path: '/single-record-maintenance' },
{ title: '主從資料維護A', icon: mdiFileTreeOutline, path: '/master-detail-maintenance' },
{ title: '主從資料維護B', icon: mdiFileTreeOutline, path: '/master-detail-maintenance-b' },
{ title: '主從資料維護C', icon: mdiFileTreeOutline, path: '/master-detail-maintenance-c' },
{ title: '可編輯表格維護', icon: mdiTableEdit, path: '/editable-grid-maintenance' },
],
},
{ title: '登入頁', path: '/login' },
]
const menuItemsExample: LayoutMenuItem[] = [
{ title: '首頁', icon: mdiHome, path: '/' },
{
title: '設定',
icon: mdiCog,
path: '/settings',
navigable: false,
},
...fixedMenuItems,
]
function buildMergedMenuItems(items: LayoutMenuItem[]) {
const flatPaths = new Set<string>()
const collectPaths = (list: LayoutMenuItem[]) => {
for (const item of list || []) {
if (item?.path) flatPaths.add(item.path)
if (item?.subItems?.length) collectPaths(item.subItems)
}
}
collectPaths(items)
const mergeFixedItems = (list: LayoutMenuItem[]) => {
return (list || []).map((item) => {
if (!item?.subItems?.length) return item
const subItems = item.subItems.filter((sub) => !sub?.path || !flatPaths.has(sub.path))
return { ...item, subItems }
})
}
const filteredFixedItems = mergeFixedItems(fixedMenuItems).filter((item) => {
if (!item?.subItems?.length) return !item?.path || !flatPaths.has(item.path)
return item.subItems.length > 0
})
return [...(items || []), ...filteredFixedItems]
}
type UseAppShellOptions = {
onLogout?: () => void
}
export function useAppShell(options: UseAppShellOptions = {}) {
const route = useRoute()
const router = useRouter()
const snackbar = useSnackbarStore()
const authStore = useAuthStore()
const menuStore = useMenuStore()
const breadcrumbStore = useBreadcrumbStore()
const favoritesStore = useFavoritesStore()
const messageStore = useMessageStore()
const mergedMenuItems = computed(() => buildMergedMenuItems(menuStore.menuItems))
const mergedFavoriteItems = computed(() => {
const combined = [...menuStore.favoriteItems, ...favoritesStore.layoutItems]
const seen = new Set<string>()
return combined.filter((item) => {
const key = item.path ?? item.title
if (!key) return false
if (seen.has(key)) return false
seen.add(key)
return true
})
})
const layoutProps = computed(() => {
const layout = route.meta.layout
if (layout === 'default') {
return {
systemTitle: '測試環境',
favoriteItems: mergedFavoriteItems.value,
menuItems: mergedMenuItems.value,
breadcrumbItems: breadcrumbStore.breadcrumbItems,
}
}
return {}
})
function handleSelect(item: LayoutMenuItem) {
if (item.path) {
router.push(item.path)
}
}
function recursiveFindTitle(path: string, items: LayoutMenuItem[]): string | null {
for (const item of items) {
if (item.path === path) return item.title
if (item.subItems?.length) {
const found = recursiveFindTitle(path, item.subItems)
if (found) return found
}
}
return null
}
function findTitle(path: string) {
const menuTitle = recursiveFindTitle(path, menuStore.menuItems)
if (menuTitle) return menuTitle
const favoriteTitle = recursiveFindTitle(path, menuStore.favoriteItems)
if (favoriteTitle) return favoriteTitle
const exampleTitle = recursiveFindTitle(path, menuItemsExample)
if (exampleTitle) return exampleTitle
if (path === '/') return '首頁'
return path
}
function findMenuItem(path: string) {
const recursiveFind = (items: LayoutMenuItem[]): LayoutMenuItem | null => {
for (const item of items) {
if (item.path === path) return item
if (item.subItems?.length) {
const found = recursiveFind(item.subItems)
if (found) return found
}
}
return null
}
return recursiveFind(mergedMenuItems.value)
}
const currentFavoriteInfo = computed(() => {
const path = route.path
const menuItem = findMenuItem(path)
const title =
menuItem?.title ||
(typeof route.meta?.title === 'string' ? route.meta.title : null) ||
findTitle(path)
return {
title,
path,
icon: menuItem?.icon,
}
})
const isCurrentFavorite = computed(() => favoritesStore.isFavorite(route.path))
const isFavoriteActionDisabled = computed(
() => !currentFavoriteInfo.value?.path || route.path === '/'
)
const favoriteActionLabel = computed(() => (isCurrentFavorite.value ? '移除常用' : '加入常用'))
const favoriteActionIcon = computed(() =>
isCurrentFavorite.value ? mdiCloseCircle : mdiPlusCircle
)
function toggleFavoriteItem(item: LayoutMenuItem) {
if (!item?.path || item.path === '/') return
favoritesStore.toggle({
title: item.title || findTitle(item.path),
path: item.path,
icon: item.icon,
})
}
function toggleFavorite() {
toggleFavoriteItem(currentFavoriteInfo.value)
}
function handleRemoveFavorite(item: LayoutMenuItem) {
toggleFavoriteItem(item)
}
function goHome() {
router.push('/')
}
function updateBreadcrumbs() {
const resolvedTitle = findTitle(route.path)
const fallbackTitle =
resolvedTitle && resolvedTitle !== route.path
? resolvedTitle
: typeof route.meta?.title === 'string'
? route.meta.title
: null
breadcrumbStore.setBreadcrumbs({
path: route.path,
menuItems: mergedMenuItems.value,
favoriteItems: mergedFavoriteItems.value,
fallbackTitle,
homeLabel: '首頁',
homeIcon: mdiHome,
})
}
function handleLayoutAction(type: string) {
if (type === 'messages') {
messageStore.open()
}
}
function performLogout(feedback: { message: string; color: string }) {
authStore.logout()
options.onLogout?.()
snackbar.show(feedback)
router.replace({ name: 'login' })
}
function handleLogout() {
performLogout({ message: '登出成功', color: 'success' })
}
function handleForceLogout(event: Event) {
const message = (event as CustomEvent)?.detail?.message || '請重新登入'
performLogout({ message, color: 'warning' })
}
watch(
[
() => route.path,
() => menuStore.menuItems,
() => menuStore.favoriteItems,
() => favoritesStore.items,
],
() => updateBreadcrumbs(),
{ immediate: true, deep: true }
)
onMounted(() => {
window.addEventListener(SESSION_FORCE_LOGOUT_EVENT, handleForceLogout)
})
onBeforeUnmount(() => {
window.removeEventListener(SESSION_FORCE_LOGOUT_EVENT, handleForceLogout)
})
return {
favoriteActionIcon,
favoriteActionLabel,
favoritesStore,
goHome,
handleLayoutAction,
handleLogout,
handleRemoveFavorite,
handleSelect,
isFavoriteActionDisabled,
layoutProps,
menuStore,
mergedMenuItems,
toggleFavorite,
}
}