我想要实现的是使用选项卡进行路由,其中可用选项卡根据用户类型而变化(经过身份验证,用户类型将存储在 pinia 存储中)。
这就是我最终实现的:
const mappingAudienceGuard = async (to: RouteLocation): Promise<true | '/mypath'> => {
const userStore = useUserStore()
// if user not in memory get user
if (userStore.user == null) {
await userStore.getUser()
}
if (to.meta.enabledTypes?.includes(userStore.user.type) ?? true) {
return true
}
return '/mypath'
}
const routes: RouteRecordRaw[] = [
{
path: '/mypath',
redirect: () => (isCustom(useUserStore().user) ? '/mypath/orders-custom' : '/mypath/orders'),
name: 'My Path',
beforeEnter: authenticationGuard,
component: DynamicInlineMenu,
props: {
links: [
{
to: '/mypath/orders',
text: 'Orders',
audience: [UserType.STANDARD]
},
{
to: '/mypath/orders-custom',
text: 'Orders Custom',
audience: [UserType.CUSTOM]
}
// [...]
],
title: 'My Title'
},
children: [
{
path: 'orders',
components: { tabContent: OrderListPage },
beforeEnter: mappingAudienceGuard,
meta: { enabledTypes: [UserType.STANDARD] }
},
{
path: 'orders-custom',
components: { tabContent: OrderListCustomPage },
beforeEnter: mappingAudienceGuard,
meta: { enabledTypes: [UserType.CUSTOM] }
}
// [...]
],
meta: {
requiresAuth: true,
title: 'My Title'
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
<template>
<div>
<TitleBar :title="title" />
<div>
<ul>
<li v-for="(link, index) in filteredLinks" :key="index">
<RouterLink
:to="link.to"
>
{{ link.text }}
</RouterLink>
</li>
</ul>
<router-view name="tabContent" />
</div>
</div>
</template>
<script setup lang="ts">
const userStore = useUserStore()
const filteredLinks = computed(() =>
props.links.filter(({ audience }) => audience.includes(userStore.user.type))
)
interface DynamicInlineMenuProps {
links: Array<{
to: string
text: string
audience: UserType[]
}>
title: string
}
const props = defineProps<DynamicInlineMenuProps>()
</script>
基本上,我们在路线的
props
变量中复制选项卡信息,而我们已经在 children
数组中拥有此类信息。
我必须为每个选项卡声明受众两次(在 props 内和子项内),并且必须过滤选项卡两次:我必须过滤模板内的 props 以不渲染选项卡,并且必须在每个子项内添加一个appingAudienceGuard如果用户手动输入不可用的 url,则阻止访问。
另外还有重定向问题:“/mypath”默认显示第一个允许的路径,我也必须在 redirect
getter 中复制(一式三份?)此类信息。我想要单一的真理来源。
避免信息重复的一种可能的解决方案是删除
props
并从 children
推断链接:
const route = useRoute()
const router = useRouter()
const links = router.options.routes.find(({ path }) => path === route.matched[0].path)?.children
但这很麻烦,我仍然需要向
to
属性添加像 meta
这样的冗余字段。另外,我仍然需要在 redirect
getter 中复制信息。也许我也可以从重定向 getter 访问子级,但是整个解决方案看起来都不像处理这种路由模式的正确方法。
你有什么建议?
老实说,我认为(干)疗法比问题更糟糕:
const getAudienceGuard = <T extends string>(
parentPath: T,
enabledTypes: UserType[] | undefined
): (() => Promise<true | T>) => {
return async (): Promise<true | T> => {
const userStore = useUserStore()
// if user not in memory get user
if (userStore.user == null) {
await userStore.getUser()
}
if (enabledTypes?.includes(getUserType(userStore.user)) ?? true) {
return true
}
return parentPath
}
}
export const filterChildrenByShowInTabAndAccessibility = async (
children?: RouteRecordRaw[]
): Promise<RouteRecordRaw[]> => {
return (
(await children?.reduce<Promise<RouteRecordRaw[]>>(async (acc, cur) => {
const guardPredicate = cur.meta?.guard == null || (await cur.meta?.guard?.()) === true
if (cur.meta?.showInTab === true && guardPredicate) {
return (await acc).concat(cur)
}
return await acc
}, Promise.resolve([]))) ?? []
)
}
const redirectToFirstAvailableChild = async (
to: RouteLocationNormalized
): Promise<NavigationGuardReturn> => {
if (to.matched.length === 1) {
return (await filterChildrenByShowInTabAndAccessibility(to.matched[0].children))[0] ?? true
}
return true
}
[...]
{
path: 'mypath',
name: 'My Path',
// FIXME: https://github.com/vuejs/vue-router/issues/2729#issuecomment-2204258741
beforeEnter: [authenticationGuard, redirectToFirstAvailableChild],
component: DynamicInlineMenu,
props: { title: 'My Path' } satisfies DynamicInlineMenuProps,
children: [
{
path: 'orders',
name: 'Mapped orders',
components: { tabContent: OrderListPage },
meta: {
showInTab: true,
guard: getAudienceGuard('/v2/responsible-sourcing/mapping', [UserType.STANDARD])
}
},
[...]
const route = useRoute()
const router = useRouter()
const filteredLinks = computedAsync(async () => {
const children = router.options.routes.find(
({ path }) => path === route.matched[0].path
)?.children
return await filterChildrenByShowInTabAndAccessibility(children)
})