Commit 120417b792576d87f1b94b27340d351bc47b2c79

Authored by xingyu4j
1 parent 7cbd6cfb

refactor: vxe crud

yudao-ui-admin-vue3/src/api/system/menu/index.ts
1 1 import request from '@/config/axios'
2   -import type { MenuVO } from './types'
3 2  
  3 +export interface MenuVO {
  4 + id: number
  5 + name: string
  6 + permission: string
  7 + type: number
  8 + sort: number
  9 + parentId: number
  10 + path: string
  11 + icon: string
  12 + component: string
  13 + status: number
  14 + visible: boolean
  15 + keepAlive: boolean
  16 + createTime: string
  17 +}
  18 +
  19 +export interface MenuPageReqVO {
  20 + name?: string
  21 + status?: number
  22 +}
4 23 // 查询菜单(精简)列表
5 24 export const listSimpleMenusApi = () => {
6 25 return request.get({ url: '/system/menu/list-all-simple' })
7 26 }
8 27  
9 28 // 查询菜单列表
10   -export const getMenuListApi = (params) => {
  29 +export const getMenuListApi = (params: MenuPageReqVO) => {
11 30 return request.get({ url: '/system/menu/list', params })
12 31 }
13 32  
... ...
yudao-ui-admin-vue3/src/api/system/menu/types.ts deleted
1   -export type MenuVO = {
2   - id: number
3   - name: string
4   - permission: string
5   - type: number
6   - sort: number
7   - parentId: number
8   - path: string
9   - icon: string
10   - component: string
11   - status: number
12   - visible: boolean
13   - keepAlive: boolean
14   - createTime: string
15   -}
yudao-ui-admin-vue3/src/api/system/notice/index.ts
1 1 import request from '@/config/axios'
2   -import type { NoticeVO } from './types'
  2 +export interface NoticeVO {
  3 + id: number
  4 + title: string
  5 + type: number
  6 + content: string
  7 + status: number
  8 + remark: string
  9 + creator: string
  10 + createTime: string
  11 + updater: string
  12 + updateTime: string
  13 +}
  14 +
  15 +export interface NoticePageReqVO extends BasePage {
  16 + title?: string
  17 + status?: number
  18 +}
3 19  
4 20 // 查询公告列表
5   -export const getNoticePageApi = (params) => {
  21 +export const getNoticePageApi = (params: NoticePageReqVO) => {
6 22 return request.get({ url: '/system/notice/page', params })
7 23 }
8 24  
... ...
yudao-ui-admin-vue3/src/api/system/notice/types.ts deleted
1   -export type NoticeVO = {
2   - id: number
3   - title: string
4   - type: number
5   - content: string
6   - status: number
7   - remark: string
8   - creator: string
9   - createTime: string
10   - updater: string
11   - updateTime: string
12   -}
yudao-ui-admin-vue3/src/api/system/post/index.ts
1 1 import request from '@/config/axios'
2   -import type { PostVO, PostPageReqVO, PostExportReqVO } from './types'
  2 +
  3 +export interface PostVO {
  4 + id?: number
  5 + name: string
  6 + code: string
  7 + sort: number
  8 + status: number
  9 + remark: string
  10 + createTime?: string
  11 +}
  12 +
  13 +export interface PostPageReqVO extends BasePage {
  14 + code?: string
  15 + name?: string
  16 + status?: number
  17 +}
  18 +
  19 +export interface PostExportReqVO {
  20 + code?: string
  21 + name?: string
  22 + status?: number
  23 +}
3 24  
4 25 // 查询岗位列表
5 26 export const getPostPageApi = async (params: PostPageReqVO) => {
... ... @@ -31,7 +52,6 @@ export const deletePostApi = async (id: number) => {
31 52 }
32 53  
33 54 // 导出岗位
34   -// TODO @星语:导出这块,咱怎么弄哈
35 55 export const exportPostApi = async (params: PostExportReqVO) => {
36 56 return await request.download({ url: '/system/post/export', params })
37 57 }
... ...
yudao-ui-admin-vue3/src/api/system/post/types.ts deleted
1   -export type PostVO = {
2   - id?: number
3   - name: string
4   - code: string
5   - sort: number
6   - status: number
7   - remark: string
8   - createTime?: string
9   -}
10   -
11   -// TODO @星语:要不要搞个 Page 基类呀?和后端对应
12   -export type PostPageReqVO = {
13   - code: string
14   - name: string
15   - status?: number
16   - pageSize?: number
17   - pageNo?: number
18   -}
19   -
20   -export type PostExportReqVO = {
21   - code: string
22   - name: string
23   - status?: number
24   -}
yudao-ui-admin-vue3/src/components/DictTag/src/DictTag.vue
... ... @@ -38,7 +38,7 @@ onUpdated(() => {
38 38 dictData?.colorType === '' ||
39 39 dictData?.colorType === undefined
40 40 "
41   - :key="dictData?.value"
  41 + :key="dictData?.value.toString()"
42 42 :class="dictData?.cssClass"
43 43 >
44 44 {{ dictData?.label }}
... ...
yudao-ui-admin-vue3/src/hooks/web/useMessage.ts
... ... @@ -71,6 +71,18 @@ export const useMessage = () => {
71 71 }
72 72 )
73 73 },
  74 + // 导出窗体
  75 + exportConfirm(content?: string, tip?: string) {
  76 + return ElMessageBox.confirm(
  77 + content ? content : t('common.exportMessage'),
  78 + tip ? tip : t('common.confirmTitle'),
  79 + {
  80 + confirmButtonText: t('common.ok'),
  81 + cancelButtonText: t('common.cancel'),
  82 + type: 'warning'
  83 + }
  84 + )
  85 + },
74 86 // 提交内容
75 87 prompt(content: string, tip: string) {
76 88 return ElMessageBox.prompt(content, tip, {
... ...
yudao-ui-admin-vue3/src/hooks/web/useVxeGrid.ts
... ... @@ -13,6 +13,7 @@ interface UseVxeGridConfig<T = any> {
13 13 getListApi: (option: any) => Promise<T>
14 14 delListApi?: (option: any) => Promise<T>
15 15 exportListApi?: (option: any) => Promise<T>
  16 + exportName?: string
16 17 }
17 18  
18 19 const appStore = useAppStore()
... ... @@ -88,10 +89,29 @@ export const useVxeGrid = &lt;T = any&gt;(config?: UseVxeGridConfig&lt;T&gt;) =&gt; {
88 89 return new Promise(async (resolve) => {
89 90 resolve(await config?.getListApi(queryParams))
90 91 })
  92 + },
  93 + queryAll: ({ form }) => {
  94 + const queryParams = Object.assign({}, JSON.parse(JSON.stringify(form)))
  95 + return new Promise(async (resolve) => {
  96 + if (config?.exportListApi) {
  97 + resolve(await config?.exportListApi(queryParams))
  98 + } else {
  99 + resolve(await config?.getListApi(queryParams))
  100 + }
  101 + })
91 102 }
92 103 }
  104 + },
  105 + exportConfig: {
  106 + filename: config?.exportName,
  107 + // 默认选中类型
  108 + type: 'csv',
  109 + // 自定义数据量列表
  110 + modes: ['current', 'all'],
  111 + columns: config?.allSchemas.printSchema
93 112 }
94 113 })
  114 +
95 115 const delList = (ids: string | number | string[] | number[]) => {
96 116 return new Promise(async () => {
97 117 message.delConfirm().then(() => {
... ...
yudao-ui-admin-vue3/src/store/modules/dict.ts
... ... @@ -3,7 +3,7 @@ import { store } from &#39;../index&#39;
3 3 import { DictDataVO } from '@/api/system/dict/types'
4 4  
5 5 export interface DictValueType {
6   - value: string
  6 + value: string | number | boolean
7 7 label: string
8 8 clorType: string
9 9 cssClass: string
... ...
yudao-ui-admin-vue3/src/utils/dict.ts
... ... @@ -15,7 +15,7 @@ const dictStore = useDictStoreWithOut()
15 15 export interface DictDataType {
16 16 dictType: string
17 17 label: string
18   - value: string | number
  18 + value: string | number | boolean
19 19 colorType: ElementPlusInfoType | '' | 'default' | 'primary'
20 20 cssClass: string
21 21 }
... ... @@ -34,10 +34,10 @@ export const getDictOptions = (dictType: string) =&gt; {
34 34 export const getIntDictOptions = (dictType: string) => {
35 35 const dictOptions: DictDataType[] = []
36 36 dictStore.getDictMap.forEach((dict: DictDataType) => {
37   - if (dict.dictType + '' === dictType) {
  37 + if (dict.dictType.toString() === dictType) {
38 38 dictOptions.push({
39 39 ...dict,
40   - value: parseInt(dict.value + '')
  40 + value: dict.value
41 41 })
42 42 }
43 43 })
... ...
yudao-ui-admin-vue3/src/views/system/menu/index.vue
... ... @@ -42,7 +42,6 @@
42 42 </template>
43 43 </vxe-toolbar>
44 44 <!-- 列表 -->
45   - <!-- TODO 星语:是不是也搞成 grid 会好点,后续代码就统一走 grid 风格? -->
46 45 <vxe-table
47 46 show-overflow
48 47 keep-source
... ... @@ -84,14 +83,14 @@
84 83 preIcon="ep:edit"
85 84 :title="t('action.edit')"
86 85 v-hasPermi="['system:menu:update']"
87   - @click="handleUpdate(row)"
  86 + @click="handleUpdate(row.id)"
88 87 />
89 88 <!-- 操作:删除 -->
90 89 <XTextButton
91 90 preIcon="ep:delete"
92 91 :title="t('action.del')"
93 92 v-hasPermi="['system:menu:delete']"
94   - @click="handleDelete(row)"
  93 + @click="handleDelete(row.id)"
95 94 />
96 95 </template>
97 96 </vxe-column>
... ... @@ -102,6 +101,7 @@
102 101 <template #default>
103 102 <!-- 对话框(添加 / 修改) -->
104 103 <el-form
  104 + ref="formRef"
105 105 :model="menuForm"
106 106 :rules="rules"
107 107 :inline="true"
... ... @@ -254,7 +254,6 @@
254 254 import { onMounted, reactive, ref } from 'vue'
255 255 import { useI18n } from '@/hooks/web/useI18n'
256 256 import { useMessage } from '@/hooks/web/useMessage'
257   -// TODO @星语:是不是 'element-plus' 和 '@/components/Tooltip' 和 '@/components/Icon' 全局直接引入
258 257 import {
259 258 ElRow,
260 259 ElCol,
... ... @@ -266,14 +265,14 @@ import {
266 265 ElTreeSelect,
267 266 ElOption,
268 267 ElRadioGroup,
269   - ElRadioButton
  268 + ElRadioButton,
  269 + FormInstance
270 270 } from 'element-plus'
271 271 import { Tooltip } from '@/components/Tooltip'
272 272 import { IconSelect } from '@/components/Icon'
273 273 import { VxeTableInstance } from 'vxe-table'
274 274 // 业务相关的 import
275 275 import * as MenuApi from '@/api/system/menu'
276   -import { MenuVO } from '@/api/system/menu/types'
277 276 import { required } from '@/utils/formRules.js'
278 277 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
279 278 import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
... ... @@ -291,7 +290,8 @@ const dialogTitle = ref(&#39;edit&#39;) // 弹出层标题
291 290 const actionType = ref('') // 操作按钮的类型
292 291 const actionLoading = ref(false) // 遮罩层
293 292 // 新增和修改的表单值
294   -const menuForm = ref<MenuVO>({
  293 +const formRef = ref<FormInstance>()
  294 +const menuForm = ref<MenuApi.MenuVO>({
295 295 id: 0,
296 296 name: '',
297 297 permission: '',
... ... @@ -316,19 +316,12 @@ const rules = reactive({
316 316  
317 317 // ========== 下拉框[上级菜单] ==========
318 318 // 下拉框[上级菜单]的配置项目
319   -// TODO @星语:menuProps 貌似也可以抽到全局?
320 319 const menuProps = {
321 320 checkStrictly: true,
322 321 children: 'children',
323 322 label: 'name',
324 323 value: 'id'
325 324 }
326   -// TODO @星语:Tree 可以是全局的
327   -interface Tree {
328   - id: number
329   - name: string
330   - children?: Tree[] | any[]
331   -}
332 325 const menuOptions = ref<any[]>([]) // 树形结构
333 326 const getTree = async () => {
334 327 menuOptions.value = []
... ... @@ -339,9 +332,9 @@ const getTree = async () =&gt; {
339 332 }
340 333  
341 334 // ========== 查询 ==========
342   -const queryParams = reactive({
343   - name: null,
344   - status: null
  335 +const queryParams = reactive<MenuApi.MenuPageReqVO>({
  336 + name: undefined,
  337 + status: undefined
345 338 })
346 339 const getList = async () => {
347 340 tableLoading.value = true
... ... @@ -357,31 +350,47 @@ const handleQuery = async () =&gt; {
357 350  
358 351 // 重置操作
359 352 const resetQuery = async () => {
360   - queryParams.name = null
361   - queryParams.status = null
  353 + queryParams.name = undefined
  354 + queryParams.status = undefined
362 355 await getList()
363 356 }
364 357  
365 358 // ========== 新增/修改 ==========
366 359  
367 360 // 设置标题
368   -const setDialogTile = (type: string) => {
  361 +const setDialogTile = async (type: string) => {
  362 + await getTree()
369 363 dialogTitle.value = t('action.' + type)
370 364 actionType.value = type
371 365 dialogVisible.value = true
372 366 }
373 367  
374   -// 新建操作
  368 +// 新增操作
375 369 const handleCreate = () => {
376 370 setDialogTile('create')
377   - // TODO @星语:重置表单
  371 + formRef.value?.resetFields()
  372 + menuForm.value = {
  373 + id: 0,
  374 + name: '',
  375 + permission: '',
  376 + type: SystemMenuTypeEnum.DIR,
  377 + sort: 1,
  378 + parentId: 0,
  379 + path: '',
  380 + icon: '',
  381 + component: '',
  382 + status: CommonStatusEnum.ENABLE,
  383 + visible: true,
  384 + keepAlive: true,
  385 + createTime: ''
  386 + }
378 387 }
379 388  
380 389 // 修改操作
381   -const handleUpdate = async (row: MenuVO) => {
  390 +const handleUpdate = async (rowId: number) => {
382 391 setDialogTile('update')
383 392 // 设置数据
384   - const res = await MenuApi.getMenuApi(row.id)
  393 + const res = await MenuApi.getMenuApi(rowId)
385 394 menuForm.value = res
386 395 }
387 396  
... ... @@ -411,11 +420,11 @@ const submitForm = async () =&gt; {
411 420 await MenuApi.updateMenuApi(menuForm.value)
412 421 message.success(t('common.updateSuccess'))
413 422 }
414   - // 操作成功,重新加载列表
415   - dialogVisible.value = false
416   - await getList()
417 423 } finally {
  424 + dialogVisible.value = false
418 425 actionLoading.value = false
  426 + // 操作成功,重新加载列表
  427 + await getList()
419 428 }
420 429 }
421 430  
... ... @@ -426,9 +435,9 @@ const isExternal = (path: string) =&gt; {
426 435  
427 436 // ========== 删除 ==========
428 437 // 删除操作
429   -const handleDelete = async (row: MenuVO) => {
430   - message.confirm(t('common.delDataMessage'), t('common.confirmTitle')).then(async () => {
431   - await MenuApi.deleteMenuApi(row.id)
  438 +const handleDelete = async (rowId: number) => {
  439 + message.delConfirm().then(async () => {
  440 + await MenuApi.deleteMenuApi(rowId)
432 441 message.success(t('common.delSuccess'))
433 442 await getList()
434 443 })
... ... @@ -437,7 +446,5 @@ const handleDelete = async (row: MenuVO) =&gt; {
437 446 // ========== 初始化 ==========
438 447 onMounted(async () => {
439 448 await getList()
440   - // TODO @星语:这个告警解决下;是不是改成新增和修改点击的时候,去加载下好一点哈?
441   - getTree()
442 449 })
443 450 </script>
... ...
yudao-ui-admin-vue3/src/views/system/notice/index.vue
... ... @@ -21,7 +21,7 @@
21 21 preIcon="ep:view"
22 22 :title="t('action.detail')"
23 23 v-hasPermi="['system:notice:update']"
24   - @click="handleDetail(row)"
  24 + @click="handleDetail(row.id)"
25 25 />
26 26 <XTextButton
27 27 preIcon="ep:delete"
... ... @@ -36,10 +36,10 @@
36 36 <template #default>
37 37 <!-- 对话框(添加 / 修改) -->
38 38 <Form
  39 + ref="formRef"
39 40 v-if="['create', 'update'].includes(actionType)"
40 41 :schema="allSchemas.formSchema"
41 42 :rules="rules"
42   - ref="formRef"
43 43 />
44 44 <!-- 对话框(详情) -->
45 45 <Descriptions
... ... @@ -63,30 +63,31 @@
63 63 </template>
64 64 <script setup lang="ts">
65 65 import { ref, unref } from 'vue'
66   -import * as NoticeApi from '@/api/system/notice'
67   -import { NoticeVO } from '@/api/system/notice/types'
68   -import { rules, allSchemas } from './notice.data'
69 66 import { useI18n } from '@/hooks/web/useI18n'
70 67 import { useMessage } from '@/hooks/web/useMessage'
71 68 import { useVxeGrid } from '@/hooks/web/useVxeGrid'
72 69 import { VxeGridInstance } from 'vxe-table'
73 70 import { FormExpose } from '@/components/Form'
74 71  
  72 +import * as NoticeApi from '@/api/system/notice'
  73 +import { rules, allSchemas } from './notice.data'
  74 +
75 75 const { t } = useI18n() // 国际化
76 76 const message = useMessage() // 消息弹窗
  77 +
  78 +const xGrid = ref<VxeGridInstance>() // grid Ref
  79 +const { gridOptions } = useVxeGrid<NoticeApi.NoticeVO>({
  80 + allSchemas: allSchemas,
  81 + getListApi: NoticeApi.getNoticePageApi
  82 +})
  83 +
77 84 const dialogVisible = ref(false) // 是否显示弹出层
78 85 const dialogTitle = ref('edit') // 弹出层标题
79 86 const actionType = ref('') // 操作按钮的类型
80 87 const actionLoading = ref(false) // 按钮Loading
81   -const xGrid = ref<VxeGridInstance>() // grid Ref
82 88 const formRef = ref<FormExpose>() // 表单 Ref
83 89 const detailRef = ref() // 详情 Ref
84 90  
85   -const { gridOptions } = useVxeGrid<NoticeVO>({
86   - allSchemas: allSchemas,
87   - getListApi: NoticeApi.getNoticePageApi
88   -})
89   -
90 91 // 设置标题
91 92 const setDialogTile = (type: string) => {
92 93 dialogTitle.value = t('action.' + type)
... ... @@ -110,9 +111,11 @@ const handleUpdate = async (rowId: number) =&gt; {
110 111 }
111 112  
112 113 // 详情操作
113   -const handleDetail = (row: NoticeVO) => {
  114 +const handleDetail = async (rowId: number) => {
114 115 setDialogTile('detail')
115   - detailRef.value = row
  116 + // 设置数据
  117 + const res = await NoticeApi.getNoticeApi(rowId)
  118 + detailRef.value = res
116 119 }
117 120  
118 121 // 删除操作
... ... @@ -137,7 +140,7 @@ const submitForm = async () =&gt; {
137 140 actionLoading.value = true
138 141 // 提交请求
139 142 try {
140   - const data = unref(formRef)?.formModel as NoticeVO
  143 + const data = unref(formRef)?.formModel as NoticeApi.NoticeVO
141 144 if (actionType.value === 'create') {
142 145 await NoticeApi.createNoticeApi(data)
143 146 message.success(t('common.createSuccess'))
... ...
yudao-ui-admin-vue3/src/views/system/notice/notice.data.ts
... ... @@ -25,8 +25,7 @@ const crudSchemas = reactive&lt;VxeCrudSchema&gt;({
25 25 {
26 26 title: '公告类型',
27 27 field: 'type',
28   - dictType: DICT_TYPE.SYSTEM_NOTICE_TYPE,
29   - isSearch: true
  28 + dictType: DICT_TYPE.SYSTEM_NOTICE_TYPE
30 29 },
31 30 {
32 31 title: t('common.status'),
... ...
yudao-ui-admin-vue3/src/views/system/post/index.vue
... ... @@ -11,6 +11,13 @@
11 11 v-hasPermi="['system:post:create']"
12 12 @click="handleCreate()"
13 13 />
  14 + <XButton
  15 + type="primary"
  16 + preIcon="ep:download"
  17 + :title="t('action.export')"
  18 + v-hasPermi="['system:post:export']"
  19 + @click="handleExport()"
  20 + />
14 21 </template>
15 22 <template #actionbtns_default="{ row }">
16 23 <!-- 操作:修改 -->
... ... @@ -25,7 +32,7 @@
25 32 preIcon="ep:view"
26 33 :title="t('action.detail')"
27 34 v-hasPermi="['system:post:update']"
28   - @click="handleDetail(row)"
  35 + @click="handleDetail(row.id)"
29 36 />
30 37 <!-- 操作:删除 -->
31 38 <XTextButton
... ... @@ -42,10 +49,10 @@
42 49 <template #default>
43 50 <!-- 表单:添加/修改 -->
44 51 <Form
  52 + ref="formRef"
45 53 v-if="['create', 'update'].includes(actionType)"
46 54 :schema="allSchemas.formSchema"
47 55 :rules="rules"
48   - ref="formRef"
49 56 />
50 57 <!-- 表单:详情 -->
51 58 <Descriptions
... ... @@ -78,14 +85,14 @@ import { VxeGridInstance } from &#39;vxe-table&#39;
78 85 import { FormExpose } from '@/components/Form'
79 86 // 业务相关的 import
80 87 import * as PostApi from '@/api/system/post'
81   -import { PostVO } from '@/api/system/post/types'
82 88 import { rules, allSchemas } from './post.data'
  89 +import download from '@/utils/download'
83 90  
84 91 const { t } = useI18n() // 国际化
85 92 const message = useMessage() // 消息弹窗
86 93 // 列表相关的变量
87 94 const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
88   -const { gridOptions } = useVxeGrid<PostVO>({
  95 +const { gridOptions } = useVxeGrid<PostApi.PostVO>({
89 96 allSchemas: allSchemas,
90 97 getListApi: PostApi.getPostPageApi
91 98 })
... ... @@ -111,6 +118,16 @@ const handleCreate = () =&gt; {
111 118 unref(formRef)?.getElFormRef()?.resetFields()
112 119 }
113 120  
  121 +// 导出操作
  122 +const handleExport = async () => {
  123 + const queryParams = Object.assign(
  124 + {},
  125 + JSON.parse(JSON.stringify(xGrid.value?.getRefMaps().refForm.value.data))
  126 + )
  127 + const res = await PostApi.exportPostApi(queryParams)
  128 + download.excel(res, '岗位列表.xls')
  129 +}
  130 +
114 131 // 修改操作
115 132 const handleUpdate = async (rowId: number) => {
116 133 setDialogTile('update')
... ... @@ -120,10 +137,10 @@ const handleUpdate = async (rowId: number) =&gt; {
120 137 }
121 138  
122 139 // 详情操作
123   -const handleDetail = (row: PostVO) => {
  140 +const handleDetail = async (rowId: number) => {
124 141 setDialogTile('detail')
125   - // TODO @星语:要不读取下后端?
126   - detailRef.value = row
  142 + const res = await PostApi.getPostApi(rowId)
  143 + detailRef.value = res
127 144 }
128 145  
129 146 // 删除操作
... ... @@ -144,13 +161,12 @@ const handleDelete = async (rowId: number) =&gt; {
144 161 const submitForm = async () => {
145 162 const elForm = unref(formRef)?.getElFormRef()
146 163 if (!elForm) return
147   - // TODO @星语:这个告警,要不要解决下?
148 164 elForm.validate(async (valid) => {
149 165 if (valid) {
150 166 actionLoading.value = true
151 167 // 提交请求
152 168 try {
153   - const data = unref(formRef)?.formModel as PostVO
  169 + const data = unref(formRef)?.formModel as PostApi.PostVO
154 170 if (actionType.value === 'create') {
155 171 await PostApi.createPostApi(data)
156 172 message.success(t('common.createSuccess'))
... ...
yudao-ui-admin-vue3/types/global.d.ts
... ... @@ -36,4 +36,14 @@ declare global {
36 36 code: string
37 37 data: T extends any ? T : T & any
38 38 }
  39 +
  40 + declare interface BasePage {
  41 + pageSize?: number
  42 + pageNo?: number
  43 + }
  44 + declare interface Tree {
  45 + id: number
  46 + name: string
  47 + children?: Tree[] | any[]
  48 + }
39 49 }
... ...