refactor(app): extract page logic into composable drivers
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user