前言
本篇文章来讲解下如何使用tdesign来实现动态路由配置,就是后台进行配置,然后后端将根据后台配置信息,将路由根据权限返回给前台,前台(客户端)通过接口会调用获取路由信息进行渲染。好的,接下来我们就开始吧
服务端接口
这里我就大概讲解下服务端的接口大概啥样,大家也可以通过apifox自己模拟一下,或者懂后端的友友们可以自己写一下这些个接口。
后台配置接口
差不多就是这样的字段
后台页面绘制
card实现基础布局
1 | <template> |
table实现基础布局
定义列
1 | import { PrimaryTableCol, TableRowData } from 'tdesign-vue-next'; |
实现table
1 | <template> |
加载数据
定义一下返回类型
1 | export interface IMenuList { |
写请求
这里用到了我之前封装的axios的知识,如果不懂的可以看看我之前的文章
1 | // 菜单相关 |
调用请求
这里实际上分页没用到,因为我这里的数据量不大,所以就不分页了,但是接口需要,就很怪
1 | import { ref, onMounted, reactive } from 'vue'; |
删除逻辑
这里前端比较简单,只需要传id就行。
写请求
1 | // 删除路由 |
写触发逻辑
这里我省略下代码,不然代码贴太多了,这里给删除图标添加方法handleDeleteRouter方法,传值为当前行id
html部分
1 | <template #op="{ row }"> |
js部分
1 | import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'; |
新增编辑逻辑
弹窗绘制
这里我将弹窗封装成了一个组件,这里直接贴代码,讲解一下一些关键点
- manifest这个是tdesign提供的全部名称
- 图标popup因为visible被我们自己控制了,所以他原有的点击蒙层关闭的效果得我们自己控制,就有了handleIconVisibleChange方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308<template>
<t-dialog
:visible="visible"
:header="header"
@confirm="handleRouterUpdateConfirm"
@cancel="handleRouterUpdateCancel"
@close="handleRouterUpdateCancel"
:closeOnOverlayClick="false"
width="800px"
>
<!-- 菜单 -->
<t-form
:data="formData"
:colon="true"
labelWidth="120px"
ref="formRef"
:rules="ROUTER_FORM_RULES"
@submit="handleSubmit"
>
<t-space direction="vertical" style="width: 100%">
<t-row>
<t-col :span="6">
<t-form-item label="菜单标题" name="title">
<t-input placeholder="请输入内容" v-model="formData.title" />
</t-form-item>
</t-col>
<t-col :span="6">
<t-form-item label="路由路径" name="path">
<t-input placeholder="请输入内容" v-model="formData.path" />
</t-form-item>
</t-col>
</t-row>
<t-row>
<t-col :span="6">
<t-form-item label="组件路径" name="component">
<t-input placeholder="请输入内容" v-model="formData.component" />
</t-form-item>
</t-col>
<t-col :span="6">
<t-form-item label="组件名称" name="componentName">
<t-input placeholder="请输入内容" v-model="formData.componentName" />
</t-form-item>
</t-col>
</t-row>
<t-row>
<t-col :span="6">
<t-form-item label="是否可见" name="iframe">
<t-radio-group v-model="formData.iframe">
<t-radio :value="true">是</t-radio>
<t-radio :value="false">否</t-radio>
</t-radio-group>
</t-form-item>
</t-col>
<t-col :span="6">
<t-form-item label="重定向" name="redirect">
<t-input placeholder="请输入内容" v-model="formData.redirect" />
</t-form-item>
</t-col>
</t-row>
<t-row>
<t-col :span="6">
<t-form-item label="是否为一级目录" name="isTop">
<t-radio-group v-model="formData.isTop">
<t-radio :value="true">是</t-radio>
<t-radio :value="false">否</t-radio>
</t-radio-group>
</t-form-item>
</t-col>
<t-col :span="6" v-if="!formData.isTop">
<t-form-item label="上级菜单" name="pid">
<t-tree-select :data="treeList" clearable :tree-props="treeProps" v-model="formData.pid" />
</t-form-item>
</t-col>
</t-row>
<t-row>
<t-col :span="6">
<t-form-item label="权限标识" name="permission" help="选择则代表仅选择权限可见">
<t-select multiple :options="ROLE_LIST" v-model="formData.permission"> </t-select>
</t-form-item>
</t-col>
<t-col :span="6">
<t-form-item label="顺序" name="menuSort" help="如有数字将排序在前,数字越小路由越靠前">
<t-input placeholder="请输入数字" v-model="formData.menuSort"></t-input>
</t-form-item>
</t-col>
</t-row>
<t-form-item label="菜单图标" name="icon">
<t-space style="width: 100%">
<t-popup :visible="iconPopupVisible" destroyed-on-close @visible-change="handleIconVisibleChange">
<t-input
readonly
placeholder="点击选择图标"
v-model="formData.icon"
@click="iconPopupVisible = !iconPopupVisible"
clearable
>
<template #prefix-icon>
<Icon :name="iconName" />
</template>
</t-input>
<template #content>
<div class="icon-search-line">
<t-input placeholder="请输入图标名称进行搜索" v-model="iconSearchInput">
<template #prefix-icon>
<Icon name="search" />
</template>
</t-input>
<t-button class="reset-icon-button" @click="handleResetIcon">重置图标</t-button>
</div>
<div class="icon-container-list">
<div
v-for="item in iconShowList"
:key="item.stem"
class="each-icon-line"
@click="handleSelectIcon(item.stem)"
>
<t-icon :name="item.stem" class="each-icon"></t-icon>{{ item.stem }}
</div>
</div>
</template>
</t-popup>
</t-space>
</t-form-item>
<t-form-item label="自定义meta" name="meta">
<t-input v-model="formData.meta" />
</t-form-item>
</t-space>
</t-form>
</t-dialog>
</template>
<script lang="ts" setup>
import { reactive, ref, computed, watch, nextTick } from 'vue';
import { ROLE_LIST, ROUTER_FORM_RULES } from '../../constants';
import type { IMenuList } from '@/api/model/checkOrder';
import { manifest, Icon } from 'tdesign-icons-vue-next';
import { MessagePlugin } from 'tdesign-vue-next';
const props = defineProps<{
header: string;
visible: boolean;
list: IMenuList[];
currentRowData: IMenuList;
type: 'add' | 'edit';
}>();
// 观测visible变化
watch(
() => props.visible,
(newVal) => {
nextTick(() => {
if (newVal) {
const { type, currentRowData } = props;
// 将值全部重置
formRef.value.reset();
// 设置默认值
formData.isTop = true;
formData.iframe = true;
iconName.value = 'search';
// 如果是编辑
if (type === 'edit') {
Object.keys(formData).forEach((key) => {
if (key === 'iframe') {
formData[key] = Boolean(currentRowData[key] !== false);
} else if (key === 'isTop') {
if (currentRowData.pid) {
formData[key] = false;
}
} else {
formData[key] = currentRowData[key];
if (key === 'icon' && currentRowData[key]) {
iconName.value = currentRowData[key];
}
}
});
}
}
});
},
);
// 表单数据
const formData = reactive({
title: '',
path: '',
component: '',
componentName: '',
iframe: true,
permission: '',
isTop: true,
meta: '',
redirect: '',
pid: '',
menuSort: '',
icon: '',
});
// 表单的ref
const formRef = ref('');
// 树结构
const treeProps = {
keys: {
label: 'title',
value: 'id',
children: 'children',
},
};
// 树截断两级
const treeList = computed(() => {
const data = props.list;
data.forEach((item) => {
const eachChildren = item.children;
if (eachChildren && eachChildren.length > 0) {
eachChildren.forEach((child) => {
child.children = [];
});
}
});
return data;
});
const emit = defineEmits(['confirm', 'cancel']);
const handleSubmit = ({ validateResult }) => {
if (validateResult === true) {
const param = {
...formData,
icon: formData.icon,
permission: formData.permission.join(','),
id: props.type === 'edit' ? props.currentRowData.id : undefined,
IFrame: formData.iframe,
pid: formData.isTop ? undefined : formData.pid,
type: formData.isTop ? 1 : 2,
};
emit('confirm', param);
}
};
const handleRouterUpdateConfirm = () => {
formRef.value.submit();
};
const handleRouterUpdateCancel = () => {
emit('cancel');
};
// icon搜索框
const iconSearchInput = ref('');
// icon展示列表
const iconShowList = computed(() => {
return manifest.filter((item) => item.stem.includes(iconSearchInput.value));
});
// 当前选中的icon名称
const iconName = ref('search');
// icon弹窗是否可见
const iconPopupVisible = ref(false);
// 选中某个icon之后
const handleSelectIcon = (name: string) => {
formData.icon = name;
iconName.value = name;
iconSearchInput.value = '';
iconPopupVisible.value = false;
};
// 重置icon
const handleResetIcon = () => {
iconSearchInput.value = '';
iconPopupVisible.value = false;
formData.icon = '';
iconName.value = 'search';
};
// 自定义触发点击蒙层关闭
const handleIconVisibleChange = (
visible: boolean,
PopupVisibleChangeContext: { e?: PopupTriggerEvent; trigger?: PopupTriggerSource },
) => {
const { trigger } = PopupVisibleChangeContext;
if (trigger === 'document') {
iconPopupVisible.value = false;
}
};
</script>
<style scoped>
.icon-search-line {
padding: 10px;
display: flex;
.reset-icon-button {
margin-left: 10px;
}
}
.icon-container-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 5px;
width: 750px;
padding: 10px;
max-height: 300px;
overflow-y: auto;
.each-icon-line {
padding: 5px 0;
.each-icon {
margin-right: 5px;
}
&:hover {
background-color: #f5f5f5;
cursor: pointer;
}
}
}
</style>
弹窗效果
触发弹窗以及发送请求
请求部分
1 | // 新增路由 |
完整页面代码
1 | <template> |
前台页面调用
登录初始化路由
在前台的登录之后配置user的信息,之后初始化路由
路由权限
这里我的initRouter使用了默认路由和自己配置的路由结合
配置路由拦截
当没有异步路由的时候获取请求异步路由,上面的截图里面就有异步路由的构建
1 | import NProgress from 'nprogress'; // progress bar |
异步路由配置
transformRouteFormat
首先将后端的接口路由转化成我们前端常见的样子,我这里写了个方法为transformRouteFormat
1 | function transformData(data) { |
这里我原先用的eval,后来看了文档,更推荐Function,所以我就改成了Function
transformObjectToRoute
将前端路由进行渲染,主要是处理哪些引入路径的情况,把路径变成真的引入组件和icon
1 | import cloneDeep from 'lodash/cloneDeep'; |
结语
好了,本篇文章就分享到这里,更多内容敬请期待~