在vue中实现条件选项卡式路由的最佳方式

问题描述 投票:0回答:1

我想要实现的是使用选项卡进行路由,其中可用选项卡根据用户类型而变化(经过身份验证,用户类型将存储在 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 访问子级,但是整个解决方案看起来都不像处理这种路由模式的正确方法。

你有什么建议?

vue.js vuejs3 vue-router vue-router4
1个回答
0
投票

老实说,我认为(干)疗法比问题更糟糕:

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)
})
© www.soinside.com 2019 - 2024. All rights reserved.