Files
skt-vuetify-templates/src/stores/menu.ts
T
skytek_xinliang b37f4363eb feat(stores): add Pinia domain stores and update docs
Implement concrete Pinia stores for app UI and domain data instead of
placeholder re-exports, including seeded student records and snackbar state.

Refresh README guidance for components, plugins, and services to document the
current project structure, data flow, and usage conventions.feat(stores): add Pinia domain stores and update docs

Implement concrete Pinia stores for app UI and domain data instead of
placeholder re-exports, including seeded student records and snackbar state.

Refresh README guidance for components, plugins, and services to document the
current project structure, data flow, and usage conventions.
2026-05-05 11:54:19 +08:00

237 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
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,
}
})