9ae91418e0
Introduce reusable shell components for layout, tabs, and global overlays. Add maintenance page model, wrapper component, and composable driver to standardize maintenance page state, search, and pagination handling.feat(shell): add app shell and maintenance page driver Introduce reusable shell components for layout, tabs, and global overlays. Add maintenance page model, wrapper component, and composable driver to standardize maintenance page state, search, and pagination handling.
238 lines
6.3 KiB
TypeScript
238 lines
6.3 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { computed, ref, watch } from 'vue'
|
|
import { normalizeError } from '@/services/error'
|
|
import { menuApi, type MenuNode } from '@/services/modules/menu'
|
|
|
|
export interface LayoutMenuItem {
|
|
title: string
|
|
path?: string
|
|
icon?: string
|
|
navigable?: boolean
|
|
subItems?: LayoutMenuItem[]
|
|
}
|
|
|
|
export const useMenuStore = defineStore('menu', () => {
|
|
const menu = ref<MenuNode[]>([])
|
|
const favorite = ref<MenuNode[]>([])
|
|
const isRail = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const loading = ref(false)
|
|
|
|
const menuStorageKey = 'sk_playground_menu'
|
|
const favoriteStorageKey = 'sk_playground_favorite'
|
|
const isRailStorageKey = 'sk_playground_is_rail'
|
|
|
|
const readNodes = (key: string): MenuNode[] => {
|
|
if (typeof window === 'undefined') return []
|
|
try {
|
|
const raw = window.localStorage.getItem(key)
|
|
if (!raw) return []
|
|
const parsed = JSON.parse(raw)
|
|
return Array.isArray(parsed) ? (parsed as MenuNode[]) : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
const readBoolean = (key: string, defaultValue = false): boolean => {
|
|
if (typeof window === 'undefined') return defaultValue
|
|
try {
|
|
const raw = window.localStorage.getItem(key)
|
|
return raw === null ? defaultValue : raw === 'true'
|
|
} catch {
|
|
return defaultValue
|
|
}
|
|
}
|
|
|
|
const writeValue = (key: string, value: any) => {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
window.localStorage.setItem(key, String(value))
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
const writeNodes = (key: string, nodes: MenuNode[]) => {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
window.localStorage.setItem(key, JSON.stringify(nodes))
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
const removeValue = (key: string) => {
|
|
if (typeof window === 'undefined') return
|
|
try {
|
|
window.localStorage.removeItem(key)
|
|
} catch {
|
|
return
|
|
}
|
|
}
|
|
|
|
const hydrate = () => {
|
|
menu.value = readNodes(menuStorageKey)
|
|
favorite.value = readNodes(favoriteStorageKey)
|
|
isRail.value = readBoolean(isRailStorageKey)
|
|
}
|
|
|
|
hydrate()
|
|
|
|
const toLayoutMenuItems = (nodes: MenuNode[]): LayoutMenuItem[] => {
|
|
const getString = (node: MenuNode, key: string): string | undefined => {
|
|
const v = node?.[key]
|
|
return typeof v === 'string' ? v : undefined
|
|
}
|
|
|
|
const getChildren = (node: MenuNode): MenuNode[] => {
|
|
return Array.isArray(node?.children) ? (node.children as MenuNode[]) : []
|
|
}
|
|
|
|
return nodes
|
|
.map((mdl) => {
|
|
const mdlTitle = getString(mdl, 'mdl_name') ?? ''
|
|
const untItems = getChildren(mdl)
|
|
.map((unt) => {
|
|
const untTitle = getString(unt, 'unt_name') ?? ''
|
|
const fncItems = getChildren(unt)
|
|
.map((fnc) => {
|
|
const fncTitle = getString(fnc, 'fnc_name') ?? ''
|
|
const fncId = getString(fnc, 'fnc_id')
|
|
return {
|
|
title: fncTitle,
|
|
path: fncId ? `/${fncId}` : undefined,
|
|
} satisfies LayoutMenuItem
|
|
})
|
|
.filter((x) => x.title)
|
|
|
|
return {
|
|
title: untTitle,
|
|
navigable: false,
|
|
subItems: fncItems,
|
|
} satisfies LayoutMenuItem
|
|
})
|
|
.filter((x) => x.title)
|
|
|
|
return {
|
|
title: mdlTitle,
|
|
navigable: false,
|
|
subItems: untItems,
|
|
} satisfies LayoutMenuItem
|
|
})
|
|
.filter((x) => x.title)
|
|
}
|
|
|
|
const toFavoriteLayoutMenuItems = (nodes: MenuNode[]): LayoutMenuItem[] => {
|
|
const getString = (node: MenuNode, key: string): string | undefined => {
|
|
const v = node?.[key]
|
|
return typeof v === 'string' ? v : undefined
|
|
}
|
|
|
|
const getChildren = (node: MenuNode): MenuNode[] => {
|
|
return Array.isArray(node?.children) ? (node.children as MenuNode[]) : []
|
|
}
|
|
|
|
return nodes
|
|
.map((unt) => {
|
|
const untTitle = getString(unt, 'unt_name') ?? ''
|
|
const fncItems = getChildren(unt)
|
|
.map((fnc) => {
|
|
const fncTitle = getString(fnc, 'fnc_name') ?? ''
|
|
const fncId = getString(fnc, 'fnc_id')
|
|
return {
|
|
title: fncTitle,
|
|
path: fncId ? `/${fncId}` : undefined,
|
|
} satisfies LayoutMenuItem
|
|
})
|
|
.filter((x) => x.title)
|
|
|
|
return {
|
|
title: untTitle,
|
|
navigable: false,
|
|
subItems: fncItems,
|
|
} satisfies LayoutMenuItem
|
|
})
|
|
.filter((x) => x.title)
|
|
}
|
|
|
|
const menuItems = computed<LayoutMenuItem[]>(() => toLayoutMenuItems(menu.value))
|
|
const favoriteItems = computed<LayoutMenuItem[]>(() => toFavoriteLayoutMenuItems(favorite.value))
|
|
|
|
watch(
|
|
menu,
|
|
(val) => {
|
|
writeNodes(menuStorageKey, val)
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(
|
|
favorite,
|
|
(val) => {
|
|
writeNodes(favoriteStorageKey, val)
|
|
},
|
|
{ deep: true }
|
|
)
|
|
|
|
watch(isRail, (val) => {
|
|
writeValue(isRailStorageKey, val)
|
|
})
|
|
|
|
const clear = () => {
|
|
menu.value = []
|
|
favorite.value = []
|
|
isRail.value = false
|
|
error.value = null
|
|
removeValue(menuStorageKey)
|
|
removeValue(favoriteStorageKey)
|
|
removeValue(isRailStorageKey)
|
|
}
|
|
|
|
const getMenu = async (id: string) => {
|
|
try {
|
|
loading.value = true
|
|
const res = await menuApi.getMenu({ userID: id })
|
|
menu.value = Array.isArray(res.data.data) ? (res.data.data as MenuNode[]) : []
|
|
} catch (error_) {
|
|
const normalizedError = normalizeError(error_)
|
|
if (normalizedError.name !== 'CanceledRequestError') {
|
|
error.value = normalizedError.message
|
|
}
|
|
throw normalizedError
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const getFavorite = async (id: string) => {
|
|
try {
|
|
loading.value = true
|
|
const res = await menuApi.getFavorite({ userID: id })
|
|
favorite.value = Array.isArray(res.data.data) ? (res.data.data as MenuNode[]) : []
|
|
} catch (error_) {
|
|
const normalizedError = normalizeError(error_)
|
|
if (normalizedError.name !== 'CanceledRequestError') {
|
|
error.value = normalizedError.message
|
|
}
|
|
throw normalizedError
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
return {
|
|
menu,
|
|
favorite,
|
|
isRail,
|
|
menuItems,
|
|
favoriteItems,
|
|
error,
|
|
loading,
|
|
hydrate,
|
|
clear,
|
|
getMenu,
|
|
getFavorite,
|
|
}
|
|
})
|