使用路由器和动态嵌套路由验证选项卡
Vuetify Tabs with Router and dynamic nested routes
我是 Vue 的新手。我的应用程序有一个非常标准的布局,包括顶部导航、侧边导航、页脚和内容区域。内容区域分为两部分,左边是树状结构,右边是选项卡式界面。
我正在使用带有嵌套动态路由的 vue 路由器。
TreeAndTab.vue
import Vue from 'vue'
import VueRouter from 'vue-router'
/* import DefaultLayout from '../layout/Default.vue' */
/* import TreeAndTabLayout from '../layout/TreeAndTab.vue' */
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'home',
meta: { layout: 'default' },
component: () => import('../pages/Home.vue')
},
{
path: '/about',
name: 'about',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/About.vue')
},
{
path: '/dashboard',
name: 'dashboard',
meta: { layout: 'default' },
component: () => import('../pages/dashboard/dashboard.vue')
},
{
// Top level requirement goes to epic
path: '/r/:epic_id?',
//name: 'requirement',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/requirement/Requirement.vue'),
children: [
{
path: '',
name: 'epic',
component: () => import('../pages/About.vue'),
props: true,
children: [
{
path: '/r/:epic_id/s/:story_id',
name: 'story',
component: () => import('../pages/Home.vue'),
props: true
}
]
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
我有两个布局 - Default.vue 和 TreeAndTab.vue。当应用程序加载时,使用 Default.vue 并且页面进一步加载 TreeAndTab.vue 布局。
TreeAndTab.vue
<template>
<tree-and-tab-layout
:treeProps="treeProps"
:tabProps="tabProps"
:treeOptions="treeOptions"
>
</tree-and-tab-layout>
</template>
<script>
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";
export default {
components: {
TreeAndTabLayout,
},
data: () => ({
treeProps: {},
tabProps: {
tabs: [
{
id: 1,
title: "epic",
route: { name: "epic" },
},
{
id: 2,
title: "story",
route: { name: "story" },
},
{
id: 3,
title: "mapping",
/* route: `/requirement/mapping/${this.$route.params.map_id}` */
},
],
},
treeOptions: {
propertyNames: {
text: "title",
},
},
}),
methods: {
getTabProps() {
return {};
},
},
created() {
this.treeProps = RequirementService.getAllRequirementsForApp();
//this.tabProps = this.getTabProps();
this.treeProps.activeNode = [
this.$route.params.epic_id || this.$route.params.story_id,
];
},
};
</script>
Requirement.vue
<template>
<tree-and-tab-layout
:treeProps="treeProps"
:tabProps="tabProps"
:treeOptions="treeOptions"
>
</tree-and-tab-layout>
</template>
<script>
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";
export default {
components: {
TreeAndTabLayout,
},
data: () => ({
treeProps: {},
tabProps: {
tabs: [
{
id: 1,
title: "epic",
route: { name: "epic" },
},
{
id: 2,
title: "story",
route: { name: "story" },
},
{
id: 3,
title: "mapping",
/* route: `/requirement/mapping/${this.$route.params.map_id}` */
},
],
},
treeOptions: {
propertyNames: {
text: "title",
},
},
}),
methods: {
getTabProps() {
return {};
},
},
created() {
this.treeProps = RequirementService.getAllRequirementsForApp();
//this.tabProps = this.getTabProps();
this.treeProps.activeNode = [
this.$route.params.epic_id || this.$route.params.story_id,
];
},
};
</script>
我要的流程如下:
- 页面加载时,树中的第一项被选中。
- 当用户单击树中的父节点时,应选择右侧的第一个选项卡并加载适当的内容。它是路由器中的父路由。
- 当用户点击子加载时,应该根据路由器加载第二个选项卡。
我看到树的行为正确,地址栏上显示了正确的路线。第一个选项卡的组件也可以正确加载。但是,当我单击叶节点时,即使正确创建了路由,选项卡也不会更新。选项卡既没有更改,也没有获得适当的组件 loaded.I 尝试了各种选项,包括在选项卡中使用路由名称 :to 等,但似乎没有任何效果。
非常感谢任何帮助。如果需要,我可以 post GitHub 上的代码。
最后我能够修复 it.Looks 选项卡上的路由设置不正确。这是我更改的内容:
- 将 tabProps 移至 compute() 以动态更新路线。
- 从 child 组件触发一个事件来更新被更新路由的 parent 捕获的路由。
- 我没有使用 this.$route 来动态更新选项卡路由,因为如果在树上选择了 child 节点但用户切换到第一个,我想保留第二个选项卡的状态包含 parent 数据的选项卡。 (我知道这很混乱)。所以它就像一个文件资源管理器,其中第一个选项卡显示文件夹的详细信息,第二个选项卡显示该文件夹中所选 child 的详细信息。
选项卡状态现在保持不变。
这是相关代码(不是最有效的,但可以工作)。希望它能帮助面临类似问题的人。
route.js
/* eslint-disable no-unused-vars */
import Vue from 'vue'
import VueRouter from 'vue-router'
/* import DefaultLayout from '../layout/Default.vue' */
/* import TreeAndTabLayout from '../layout/TreeAndTab.vue' */
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'home',
meta: { layout: 'default' },
component: () => import('../pages/Home.vue')
},
{
path: '/about',
name: 'about',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/About.vue')
},
{
path: '/dashboard',
name: 'dashboard',
meta: { layout: 'default' },
component: () => import('../pages/dashboard/dashboard.vue')
},
{
// Top level requirement goes to epic
path: '/plan',
//name: 'requirement',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/requirement/Requirement.vue'),
children: [
{
path: 'e/:epic_id',
name: 'epic',
component: () => import('../pages/About.vue'),
props: true
},
{
path: 'e/:epic_id/s/:story_id',
name: 'story',
component: () => import('../pages/Home.vue'),
props: true
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
pages/Requirement.vue
<template>
<tree-and-tab-layout
:treeProps="treeProps"
:tabProps="tabProps"
:treeOptions="treeOptions"
v-on:activateTreeNode="handleTreeNodeActivate"
>
</tree-and-tab-layout>
</template>
<script>
//
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";
export default {
name: "RequirementPage",
components: {
TreeAndTabLayout,
},
data: () => ({
base_path: "/plan",
epic_id: "",
story_id: "",
epic_base_path: "/e/",
story_base_path: "/s/",
treeProps: {},
treeOptions: {
propertyNames: {
text: "title",
},
},
}),
computed: {
tabProps() {
return {
tabs: [
{
id: 1,
title: "Epic",
route:
this.base_path + this.epic_base_path + this.epic_id,
},
{
id: 2,
title: "Story",
route:
this.base_path +
this.epic_base_path +
this.epic_id +
this.story_base_path +
this.story_id,
},
],
};
},
},
methods: {
handleTreeNodeActivate(child_id, parent_id) {
this.story_id = child_id;
this.epic_id = parent_id;
},
},
created() {
this.treeProps = RequirementService.getAllRequirementsForApp();
// Does not work somehow. Handling it from the template
//this.$on("activateTreeNode", this.handleTreeNodeActivate);
},
};
</script>
TreeAndTab.vue
<template>
<splitpanes>
<pane size="30">
<tree
:data="treeProps.items"
:options="treeOptions"
ref="tree"
@node:selected="onActive"
@node:expanded="onExpand"
/>
</pane>
<pane size="70">
<v-tabs v-model="activeTab" light>
<!-- <v-tabs-slider></v-tabs-slider> -->
<v-tab
v-for="tab in tabProps.tabs"
:key="tab.id"
:to="tab.route"
exact
>{{ tab.title }}</v-tab
>
</v-tabs>
<v-card flat tile>
<keep-alive>
<router-view />
</keep-alive>
</v-card>
</pane>
</splitpanes>
</template>
<script>
import { Splitpanes, Pane } from "splitpanes";
import LiquorTree from "liquor-tree";
import "splitpanes/dist/splitpanes.css";
export default {
name: "TreeAndTab",
components: {
Splitpanes,
Pane,
tree: LiquorTree,
},
props: {
treeProps: {
type: Object,
required: true,
},
tabProps: {
type: Object,
required: true,
},
treeOptions: {
type: Object,
},
},
data: () => ({
newEpic: {
id: "e_new",
title: "New Epic",
isFolder: true,
},
newStory: {
id: "s_new",
title: "New Story",
},
newCounter: 0,
activeTab: null,
}),
mounted() {
console.log("mounted Tree and Tab");
},
computed: {},
methods: {
onActive(node) {
if (node.parent != null) {
this.$emit("activateTreeNode", node.id, node.parent.id);
this.$router.push({
name: "story",
params: { epic_id: node.parent.id, story_id: node.id },
});
} else {
this.$emit("activateTreeNode", null, node.id);
this.$router.push({
name: "epic",
params: { epic_id: node.id },
});
}
},
onExpand(node) {
console.log("expand=", node);
},
getCurrentActiveNode() {
return this.$refs.tree.selected()[0];
},
},
};
</script>
<style scoped>
</style>
我是 Vue 的新手。我的应用程序有一个非常标准的布局,包括顶部导航、侧边导航、页脚和内容区域。内容区域分为两部分,左边是树状结构,右边是选项卡式界面。 我正在使用带有嵌套动态路由的 vue 路由器。
TreeAndTab.vue
import Vue from 'vue'
import VueRouter from 'vue-router'
/* import DefaultLayout from '../layout/Default.vue' */
/* import TreeAndTabLayout from '../layout/TreeAndTab.vue' */
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'home',
meta: { layout: 'default' },
component: () => import('../pages/Home.vue')
},
{
path: '/about',
name: 'about',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/About.vue')
},
{
path: '/dashboard',
name: 'dashboard',
meta: { layout: 'default' },
component: () => import('../pages/dashboard/dashboard.vue')
},
{
// Top level requirement goes to epic
path: '/r/:epic_id?',
//name: 'requirement',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/requirement/Requirement.vue'),
children: [
{
path: '',
name: 'epic',
component: () => import('../pages/About.vue'),
props: true,
children: [
{
path: '/r/:epic_id/s/:story_id',
name: 'story',
component: () => import('../pages/Home.vue'),
props: true
}
]
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
我有两个布局 - Default.vue 和 TreeAndTab.vue。当应用程序加载时,使用 Default.vue 并且页面进一步加载 TreeAndTab.vue 布局。
TreeAndTab.vue
<template>
<tree-and-tab-layout
:treeProps="treeProps"
:tabProps="tabProps"
:treeOptions="treeOptions"
>
</tree-and-tab-layout>
</template>
<script>
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";
export default {
components: {
TreeAndTabLayout,
},
data: () => ({
treeProps: {},
tabProps: {
tabs: [
{
id: 1,
title: "epic",
route: { name: "epic" },
},
{
id: 2,
title: "story",
route: { name: "story" },
},
{
id: 3,
title: "mapping",
/* route: `/requirement/mapping/${this.$route.params.map_id}` */
},
],
},
treeOptions: {
propertyNames: {
text: "title",
},
},
}),
methods: {
getTabProps() {
return {};
},
},
created() {
this.treeProps = RequirementService.getAllRequirementsForApp();
//this.tabProps = this.getTabProps();
this.treeProps.activeNode = [
this.$route.params.epic_id || this.$route.params.story_id,
];
},
};
</script>
Requirement.vue
<template>
<tree-and-tab-layout
:treeProps="treeProps"
:tabProps="tabProps"
:treeOptions="treeOptions"
>
</tree-and-tab-layout>
</template>
<script>
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";
export default {
components: {
TreeAndTabLayout,
},
data: () => ({
treeProps: {},
tabProps: {
tabs: [
{
id: 1,
title: "epic",
route: { name: "epic" },
},
{
id: 2,
title: "story",
route: { name: "story" },
},
{
id: 3,
title: "mapping",
/* route: `/requirement/mapping/${this.$route.params.map_id}` */
},
],
},
treeOptions: {
propertyNames: {
text: "title",
},
},
}),
methods: {
getTabProps() {
return {};
},
},
created() {
this.treeProps = RequirementService.getAllRequirementsForApp();
//this.tabProps = this.getTabProps();
this.treeProps.activeNode = [
this.$route.params.epic_id || this.$route.params.story_id,
];
},
};
</script>
我要的流程如下:
- 页面加载时,树中的第一项被选中。
- 当用户单击树中的父节点时,应选择右侧的第一个选项卡并加载适当的内容。它是路由器中的父路由。
- 当用户点击子加载时,应该根据路由器加载第二个选项卡。
我看到树的行为正确,地址栏上显示了正确的路线。第一个选项卡的组件也可以正确加载。但是,当我单击叶节点时,即使正确创建了路由,选项卡也不会更新。选项卡既没有更改,也没有获得适当的组件 loaded.I 尝试了各种选项,包括在选项卡中使用路由名称 :to 等,但似乎没有任何效果。
非常感谢任何帮助。如果需要,我可以 post GitHub 上的代码。
最后我能够修复 it.Looks 选项卡上的路由设置不正确。这是我更改的内容:
- 将 tabProps 移至 compute() 以动态更新路线。
- 从 child 组件触发一个事件来更新被更新路由的 parent 捕获的路由。
- 我没有使用 this.$route 来动态更新选项卡路由,因为如果在树上选择了 child 节点但用户切换到第一个,我想保留第二个选项卡的状态包含 parent 数据的选项卡。 (我知道这很混乱)。所以它就像一个文件资源管理器,其中第一个选项卡显示文件夹的详细信息,第二个选项卡显示该文件夹中所选 child 的详细信息。
选项卡状态现在保持不变。
这是相关代码(不是最有效的,但可以工作)。希望它能帮助面临类似问题的人。
route.js
/* eslint-disable no-unused-vars */
import Vue from 'vue'
import VueRouter from 'vue-router'
/* import DefaultLayout from '../layout/Default.vue' */
/* import TreeAndTabLayout from '../layout/TreeAndTab.vue' */
Vue.use(VueRouter)
const routes = [
{
path: '/home',
name: 'home',
meta: { layout: 'default' },
component: () => import('../pages/Home.vue')
},
{
path: '/about',
name: 'about',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/About.vue')
},
{
path: '/dashboard',
name: 'dashboard',
meta: { layout: 'default' },
component: () => import('../pages/dashboard/dashboard.vue')
},
{
// Top level requirement goes to epic
path: '/plan',
//name: 'requirement',
meta: { layout: 'default' },
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('../pages/requirement/Requirement.vue'),
children: [
{
path: 'e/:epic_id',
name: 'epic',
component: () => import('../pages/About.vue'),
props: true
},
{
path: 'e/:epic_id/s/:story_id',
name: 'story',
component: () => import('../pages/Home.vue'),
props: true
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
pages/Requirement.vue
<template>
<tree-and-tab-layout
:treeProps="treeProps"
:tabProps="tabProps"
:treeOptions="treeOptions"
v-on:activateTreeNode="handleTreeNodeActivate"
>
</tree-and-tab-layout>
</template>
<script>
//
import TreeAndTabLayout from "../../layout/TreeAndTab.vue";
import RequirementService from "./RequirementService.js";
export default {
name: "RequirementPage",
components: {
TreeAndTabLayout,
},
data: () => ({
base_path: "/plan",
epic_id: "",
story_id: "",
epic_base_path: "/e/",
story_base_path: "/s/",
treeProps: {},
treeOptions: {
propertyNames: {
text: "title",
},
},
}),
computed: {
tabProps() {
return {
tabs: [
{
id: 1,
title: "Epic",
route:
this.base_path + this.epic_base_path + this.epic_id,
},
{
id: 2,
title: "Story",
route:
this.base_path +
this.epic_base_path +
this.epic_id +
this.story_base_path +
this.story_id,
},
],
};
},
},
methods: {
handleTreeNodeActivate(child_id, parent_id) {
this.story_id = child_id;
this.epic_id = parent_id;
},
},
created() {
this.treeProps = RequirementService.getAllRequirementsForApp();
// Does not work somehow. Handling it from the template
//this.$on("activateTreeNode", this.handleTreeNodeActivate);
},
};
</script>
TreeAndTab.vue
<template>
<splitpanes>
<pane size="30">
<tree
:data="treeProps.items"
:options="treeOptions"
ref="tree"
@node:selected="onActive"
@node:expanded="onExpand"
/>
</pane>
<pane size="70">
<v-tabs v-model="activeTab" light>
<!-- <v-tabs-slider></v-tabs-slider> -->
<v-tab
v-for="tab in tabProps.tabs"
:key="tab.id"
:to="tab.route"
exact
>{{ tab.title }}</v-tab
>
</v-tabs>
<v-card flat tile>
<keep-alive>
<router-view />
</keep-alive>
</v-card>
</pane>
</splitpanes>
</template>
<script>
import { Splitpanes, Pane } from "splitpanes";
import LiquorTree from "liquor-tree";
import "splitpanes/dist/splitpanes.css";
export default {
name: "TreeAndTab",
components: {
Splitpanes,
Pane,
tree: LiquorTree,
},
props: {
treeProps: {
type: Object,
required: true,
},
tabProps: {
type: Object,
required: true,
},
treeOptions: {
type: Object,
},
},
data: () => ({
newEpic: {
id: "e_new",
title: "New Epic",
isFolder: true,
},
newStory: {
id: "s_new",
title: "New Story",
},
newCounter: 0,
activeTab: null,
}),
mounted() {
console.log("mounted Tree and Tab");
},
computed: {},
methods: {
onActive(node) {
if (node.parent != null) {
this.$emit("activateTreeNode", node.id, node.parent.id);
this.$router.push({
name: "story",
params: { epic_id: node.parent.id, story_id: node.id },
});
} else {
this.$emit("activateTreeNode", null, node.id);
this.$router.push({
name: "epic",
params: { epic_id: node.id },
});
}
},
onExpand(node) {
console.log("expand=", node);
},
getCurrentActiveNode() {
return this.$refs.tree.selected()[0];
},
},
};
</script>
<style scoped>
</style>