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.
This commit is contained in:
+236
-1
@@ -1 +1,236 @@
|
||||
export * from './stores/menu'
|
||||
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,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user