zant-admin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +36 -0
- package/README.md +248 -0
- package/SCAFFOLD_README.md +215 -0
- package/bin/cli.js +99 -0
- package/bin/generator.js +503 -0
- package/bin/prompts.js +159 -0
- package/bin/utils.js +134 -0
- package/package.json +74 -0
- package/public/logo.png +0 -0
- package/src/App.vue +16 -0
- package/src/api/methods/logError.js +8 -0
- package/src/api/methods/logOperation.js +8 -0
- package/src/api/methods/login.js +6 -0
- package/src/api/methods/quartz.js +36 -0
- package/src/api/methods/region.js +16 -0
- package/src/api/methods/sysAccount.js +30 -0
- package/src/api/methods/sysDict.js +29 -0
- package/src/api/methods/sysDictItem.js +26 -0
- package/src/api/methods/sysMenu.js +42 -0
- package/src/api/methods/sysRole.js +35 -0
- package/src/api/methods/sysUser.js +25 -0
- package/src/api/methods/system.js +16 -0
- package/src/api/request.js +225 -0
- package/src/assets/css/style.css +70 -0
- package/src/assets/css/zcui.css +340 -0
- package/src/assets/imgs/loginbackground.svg +69 -0
- package/src/assets/imgs/logo.png +0 -0
- package/src/assets/imgs/md/1.png +0 -0
- package/src/assets/imgs/md/10.png +0 -0
- package/src/assets/imgs/md/11.png +0 -0
- package/src/assets/imgs/md/2.png +0 -0
- package/src/assets/imgs/md/3.png +0 -0
- package/src/assets/imgs/md/4.png +0 -0
- package/src/assets/imgs/md/5.png +0 -0
- package/src/assets/imgs/md/6.png +0 -0
- package/src/assets/imgs/md/7.png +0 -0
- package/src/assets/imgs/md/8.png +0 -0
- package/src/assets/imgs/md/9.png +0 -0
- package/src/components/FormTable.vue +875 -0
- package/src/components/IconPicker.vue +344 -0
- package/src/components/MainPage.vue +957 -0
- package/src/components/details/logErrorDetails.vue +58 -0
- package/src/components/details/logOperationDetails.vue +76 -0
- package/src/components/edit/QuartzEdit.vue +221 -0
- package/src/components/edit/SysAccountEdit.vue +178 -0
- package/src/components/edit/SysDictEdit.vue +114 -0
- package/src/components/edit/SysDictItemEdit.vue +134 -0
- package/src/components/edit/SysRoleEdit.vue +109 -0
- package/src/components/edit/sysMenuEdit.vue +305 -0
- package/src/config/index.js +74 -0
- package/src/directives/permission.js +45 -0
- package/src/main.js +38 -0
- package/src/router/index.js +270 -0
- package/src/stores/config.js +37 -0
- package/src/stores/dict.js +33 -0
- package/src/stores/menu.js +57 -0
- package/src/stores/user.js +21 -0
- package/src/utils/baseEcharts.js +661 -0
- package/src/utils/dictTemplate.js +26 -0
- package/src/utils/regionUtils.js +169 -0
- package/src/utils/useFormCRUD.js +60 -0
- package/src/views/baiscstatis/center.vue +463 -0
- package/src/views/baiscstatis/iframePage.vue +31 -0
- package/src/views/baiscstatis/notFound.vue +192 -0
- package/src/views/console.vue +771 -0
- package/src/views/demo/importexport.vue +123 -0
- package/src/views/demo/region.vue +240 -0
- package/src/views/demo/statistics.vue +195 -0
- package/src/views/home.vue +7 -0
- package/src/views/login.vue +272 -0
- package/src/views/operations/log/logError.vue +78 -0
- package/src/views/operations/log/logLogin.vue +66 -0
- package/src/views/operations/log/logOperation.vue +103 -0
- package/src/views/operations/log/logQuartz.vue +57 -0
- package/src/views/operations/quartz.vue +181 -0
- package/src/views/operations/serviceMonitoring.vue +134 -0
- package/src/views/system/sysAccount.vue +123 -0
- package/src/views/system/sysDict.vue +156 -0
- package/src/views/system/sysDictItem.vue +118 -0
- package/src/views/system/sysMenu.vue +223 -0
- package/src/views/system/sysRole.vue +184 -0
- package/templates/env.production +2 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import home from '../views/home.vue'
|
|
3
|
+
import { useUserStore } from '@/stores/user'
|
|
4
|
+
import sysMenu from '@/api/methods/sysMenu'
|
|
5
|
+
import { menuStore } from '@/stores/menu'
|
|
6
|
+
import { configStore } from '@/stores/config'
|
|
7
|
+
// 定义不需要 token 的路由白名单
|
|
8
|
+
//const publicRoutes = ['/login', '/Userreport', '/deviceError']
|
|
9
|
+
const publicRoutes = []
|
|
10
|
+
|
|
11
|
+
// 静态路由
|
|
12
|
+
const routes = [
|
|
13
|
+
{
|
|
14
|
+
path: '/login',
|
|
15
|
+
component: () => import('../views/login.vue'),
|
|
16
|
+
meta: { title: '登录', cache: false },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
path: '/',
|
|
20
|
+
component: home,
|
|
21
|
+
children: [
|
|
22
|
+
{
|
|
23
|
+
path: '',
|
|
24
|
+
name: 'home',
|
|
25
|
+
meta: { title: '', cache: true },
|
|
26
|
+
component: () => import('../views/console.vue'), // 默认主页
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: 'console',
|
|
30
|
+
name: 'console',
|
|
31
|
+
meta: { title: '主页', cache: true },
|
|
32
|
+
component: () => import('../views/console.vue'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
path: 'center',
|
|
36
|
+
name: 'center',
|
|
37
|
+
meta: { title: '个人中心', cache: true },
|
|
38
|
+
component: () => import('../views/baiscstatis/center.vue'),
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
path: 'iframePage',
|
|
44
|
+
meta: { cache: false },
|
|
45
|
+
component: () => import('../views/baiscstatis/iframePage.vue'),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: 'notFound',
|
|
49
|
+
component: () => import('../views/baiscstatis/notFound.vue'),
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
]
|
|
54
|
+
const router = createRouter({
|
|
55
|
+
history: createWebHistory(import.meta.env.BASE_URL),
|
|
56
|
+
routes,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// 动态导入所有视图组件
|
|
60
|
+
const modules = import.meta.glob('@/views/**/*.vue')
|
|
61
|
+
|
|
62
|
+
// 标记动态路由加载状态
|
|
63
|
+
let hasAddRoutes = false
|
|
64
|
+
let routeLoadFailed = false
|
|
65
|
+
|
|
66
|
+
// 组件加载器,带有错误处理
|
|
67
|
+
const loadComponent = (path) => {
|
|
68
|
+
const possiblePaths = [
|
|
69
|
+
`/src/views${path}.vue`,
|
|
70
|
+
`./views${path}.vue`,
|
|
71
|
+
`../views${path}.vue`,
|
|
72
|
+
`@/views${path}.vue`,
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
for (const tryPath of possiblePaths) {
|
|
76
|
+
if (modules[tryPath]) {
|
|
77
|
+
return modules[tryPath]
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return () => import('../views/baiscstatis/notFound.vue')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function loadDynamicRoutes() {
|
|
85
|
+
try {
|
|
86
|
+
const menu = menuStore()
|
|
87
|
+
const res = await sysMenu.getRoutesMenu()
|
|
88
|
+
const resbtn = await sysMenu.getMenuBtn()
|
|
89
|
+
const dynamicRoutes = res.data || []
|
|
90
|
+
const dynamicBtns = resbtn.data || []
|
|
91
|
+
menu.setMenus(dynamicRoutes)
|
|
92
|
+
menu.setPermissions(dynamicBtns)
|
|
93
|
+
dynamicRoutes.forEach((route) => {
|
|
94
|
+
if (route.type === 1) {
|
|
95
|
+
// 目录类型
|
|
96
|
+
const parentRoute = {
|
|
97
|
+
path: route.path,
|
|
98
|
+
component: home,
|
|
99
|
+
// meta: { title: route.title },
|
|
100
|
+
// children: [],
|
|
101
|
+
}
|
|
102
|
+
const addRoutes = (parentRoute, children) => {
|
|
103
|
+
if (children && children.length > 0) {
|
|
104
|
+
parentRoute.children = children.map(child => {
|
|
105
|
+
const childRoute = {
|
|
106
|
+
path: child.path,
|
|
107
|
+
meta: { title: child.title },
|
|
108
|
+
name: child.path.replace(/\//g, '-').substring(1),
|
|
109
|
+
meta: { cache: child.cache,fullPath: child.path, },
|
|
110
|
+
}
|
|
111
|
+
if(child.type!=1){
|
|
112
|
+
childRoute.component = loadComponent(child.path)
|
|
113
|
+
}
|
|
114
|
+
// 递归处理多级子路由
|
|
115
|
+
if (child.children) {
|
|
116
|
+
addRoutes(childRoute, child.children)
|
|
117
|
+
}
|
|
118
|
+
return childRoute
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
addRoutes(parentRoute, route.children)
|
|
123
|
+
router.addRoute(parentRoute)
|
|
124
|
+
} else {
|
|
125
|
+
// 页面类型
|
|
126
|
+
const pageRoute = {
|
|
127
|
+
path: route.path,
|
|
128
|
+
name: route.path.replace(/\//g, '-').substring(1),
|
|
129
|
+
component: loadComponent(route.path),
|
|
130
|
+
meta: {
|
|
131
|
+
title: route.title,
|
|
132
|
+
cache: route.cache,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
router.addRoute(pageRoute)
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
hasAddRoutes = true
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('❌ 动态路由加载失败:', error)
|
|
142
|
+
routeLoadFailed = true
|
|
143
|
+
// 如果是401错误(令牌过期),清除用户信息并跳转到登录页
|
|
144
|
+
if (error.code === 401 || (error.errors && error.errors.includes('登录'))) {
|
|
145
|
+
const user = useUserStore()
|
|
146
|
+
user.clearUserInfo()
|
|
147
|
+
router.push('/login')
|
|
148
|
+
}
|
|
149
|
+
throw error
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 全局前置守卫
|
|
154
|
+
router.beforeEach(async (to, from, next) => {
|
|
155
|
+
// document.title = to.meta.title || 'ZCloudVue'
|
|
156
|
+
// document.title = 'ZCloudVue'
|
|
157
|
+
const config = configStore()
|
|
158
|
+
document.title = config.projectName
|
|
159
|
+
const user = useUserStore()
|
|
160
|
+
|
|
161
|
+
// 如果目标路由在白名单中,直接放行
|
|
162
|
+
if (publicRoutes.includes(to.path)) {
|
|
163
|
+
next()
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!user.token && to.path !== '/login') {
|
|
168
|
+
next('/login')
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 动态路由加载
|
|
173
|
+
if (user.token && !hasAddRoutes && !routeLoadFailed) {
|
|
174
|
+
try {
|
|
175
|
+
await loadDynamicRoutes()
|
|
176
|
+
|
|
177
|
+
// 检查是否有持久化的路由状态需要恢复
|
|
178
|
+
const menu = menuStore()
|
|
179
|
+
if (menu.selectmenuKey && menu.selectmenuKey.length > 0 && to.path === '/console') {
|
|
180
|
+
// 如果当前是跳转到首页,但有持久化的选中菜单,尝试恢复路由
|
|
181
|
+
const selectedMenu = menu.tagmenus.find(tag => tag.key === menu.selectmenuKey[0])
|
|
182
|
+
if (selectedMenu && selectedMenu.path && selectedMenu.path !== '/console') {
|
|
183
|
+
// 检查路由是否存在
|
|
184
|
+
const routeExists = router.getRoutes().some((r) => r.path === selectedMenu.path)
|
|
185
|
+
if (routeExists) {
|
|
186
|
+
next({ path: selectedMenu.path, replace: true })
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
next({ ...to, replace: true })
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// 如果是401错误,已经在loadDynamicRoutes中处理了跳转
|
|
195
|
+
if (error.code === 401 || (error.errors && error.errors.includes('登录'))) {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
// 其他错误跳转到404页面
|
|
199
|
+
next('/notFound')
|
|
200
|
+
}
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 如果接口已经失败过,避免死循环
|
|
205
|
+
if (user.token && routeLoadFailed && !hasAddRoutes) {
|
|
206
|
+
if (to.path !== '/notFound') {
|
|
207
|
+
next('/notFound')
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 根路径重定向
|
|
213
|
+
if (to.path === '/') {
|
|
214
|
+
next('/console')
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 路由存在性检查
|
|
219
|
+
const routeExists = router.getRoutes().some((r) => r.path === to.path)
|
|
220
|
+
if (!routeExists && !publicRoutes.includes(to.path)) {
|
|
221
|
+
// 如果路由不存在且不在白名单中,检查是否是动态路由还未加载
|
|
222
|
+
if (user.token && !hasAddRoutes && !routeLoadFailed) {
|
|
223
|
+
// 动态路由还未加载,等待加载完成
|
|
224
|
+
try {
|
|
225
|
+
await loadDynamicRoutes()
|
|
226
|
+
next({ ...to, replace: true })
|
|
227
|
+
return
|
|
228
|
+
} catch (error) {
|
|
229
|
+
// 如果是401错误,已经在loadDynamicRoutes中处理了跳转
|
|
230
|
+
if (error.code === 401 || (error.errors && error.errors.includes('登录'))) {
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
// 其他错误跳转到404页面
|
|
234
|
+
next('/notFound')
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// 如果动态路由已加载但路由仍不存在,跳转到404
|
|
239
|
+
next('/notFound')
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
next()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
router.onError((error) => {
|
|
247
|
+
console.error('🚨 路由错误:', error)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
export async function refreshRoutes() {
|
|
251
|
+
hasAddRoutes = false
|
|
252
|
+
routeLoadFailed = false
|
|
253
|
+
// 清空旧的菜单,避免重复
|
|
254
|
+
const menu = menuStore()
|
|
255
|
+
menu.setMenus([])
|
|
256
|
+
menu.setPermissions([])
|
|
257
|
+
// 移除之前添加的动态路由
|
|
258
|
+
const routes = router.getRoutes()
|
|
259
|
+
routes.forEach(route => {
|
|
260
|
+
// 只移除动态添加的路由,保留静态路由
|
|
261
|
+
if (route.path && !['/login', '/', '/console', '/notFound', '/center'].includes(route.path)) {
|
|
262
|
+
router.removeRoute(route.name)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// 重新加载动态路由
|
|
267
|
+
await loadDynamicRoutes()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default router
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import dayjs from 'dayjs'
|
|
3
|
+
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
|
4
|
+
import enUS from 'ant-design-vue/es/locale/en_US'
|
|
5
|
+
import 'dayjs/locale/zh-cn' // 中文
|
|
6
|
+
import 'dayjs/locale/en' // 英文
|
|
7
|
+
import { appConfig } from '@/config' // 导入全局配置
|
|
8
|
+
|
|
9
|
+
export const configStore = defineStore({
|
|
10
|
+
id: 'CONFIG',
|
|
11
|
+
state: () => ({
|
|
12
|
+
themeClass: appConfig.defaultTheme,
|
|
13
|
+
navigationMode: appConfig.defaultNavigationMode,
|
|
14
|
+
tableSize: appConfig.defaultTableSize,
|
|
15
|
+
tableBordered: appConfig.defaultTableBordered,
|
|
16
|
+
locale: zhCN,
|
|
17
|
+
dayjsLocale: appConfig.defaultLanguage,
|
|
18
|
+
projectName: appConfig.title, // 使用全局配置中的项目名称
|
|
19
|
+
}),
|
|
20
|
+
actions: {
|
|
21
|
+
setLocale(locale) {
|
|
22
|
+
this.locale = locale
|
|
23
|
+
this.dayjsLocale = locale === zhCN ? 'zh-cn' : 'en'
|
|
24
|
+
dayjs.locale(this.dayjsLocale)
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
persist: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
strategies: [
|
|
30
|
+
{
|
|
31
|
+
key: 'CONFIG',
|
|
32
|
+
storage: localStorage,
|
|
33
|
+
paths: ['themeClass','navigationMode', 'tableSize', 'tableBordered', 'locale'],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import sysDict from '@/api/methods/sysDict'
|
|
3
|
+
export const dictStore = defineStore({
|
|
4
|
+
id: 'SYS_DICT',
|
|
5
|
+
state: () => ({
|
|
6
|
+
items: {},
|
|
7
|
+
}),
|
|
8
|
+
actions: {
|
|
9
|
+
initDictList() {
|
|
10
|
+
sysDict
|
|
11
|
+
.getDictList()
|
|
12
|
+
.then(res => {
|
|
13
|
+
res.data.forEach(item => {
|
|
14
|
+
this.items[item.type] = item.sysDictItem
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
.catch()
|
|
18
|
+
},
|
|
19
|
+
getDictItems(type) {
|
|
20
|
+
return this.items[type]
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
persist: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
strategies: [
|
|
26
|
+
{
|
|
27
|
+
key: 'SYS_DICT',
|
|
28
|
+
storage: localStorage,
|
|
29
|
+
paths: ['items'],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
|
|
3
|
+
export const menuStore = defineStore({
|
|
4
|
+
id: 'MENU_RESULT',
|
|
5
|
+
state: () => {
|
|
6
|
+
return {
|
|
7
|
+
openmenuKey: [],
|
|
8
|
+
selectmenuKey: [],
|
|
9
|
+
tagmenus: [
|
|
10
|
+
{
|
|
11
|
+
key: 0,
|
|
12
|
+
parentId: '0',
|
|
13
|
+
title: `控制台`,
|
|
14
|
+
closable: false,
|
|
15
|
+
path: '/console',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
menus: [],
|
|
19
|
+
permissions: []
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
actions: {
|
|
23
|
+
setSelectmenuKey(keys) {
|
|
24
|
+
this.selectmenuKey = keys
|
|
25
|
+
},
|
|
26
|
+
setOpenmenuKey(keys) {
|
|
27
|
+
this.openmenuKey = keys
|
|
28
|
+
},
|
|
29
|
+
setMenus(menus) { this.menus = menus },
|
|
30
|
+
setPermissions(permissions) { this.permissions = permissions },
|
|
31
|
+
// 检查是否有指定权限
|
|
32
|
+
hasPermission(permission) {
|
|
33
|
+
if (!permission) return true
|
|
34
|
+
return this.permissions.includes(permission)
|
|
35
|
+
},
|
|
36
|
+
// 检查是否有多个权限中的任意一个
|
|
37
|
+
hasAnyPermission(permissions) {
|
|
38
|
+
if (!permissions || !Array.isArray(permissions) || permissions.length === 0) return true
|
|
39
|
+
return permissions.some(permission => this.permissions.includes(permission))
|
|
40
|
+
},
|
|
41
|
+
// 检查是否拥有所有权限
|
|
42
|
+
hasAllPermissions(permissions) {
|
|
43
|
+
if (!permissions || !Array.isArray(permissions) || permissions.length === 0) return true
|
|
44
|
+
return permissions.every(permission => this.permissions.includes(permission))
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
persist: {
|
|
48
|
+
enabled: true,
|
|
49
|
+
strategies: [
|
|
50
|
+
{
|
|
51
|
+
key: 'MENU_RESULT',
|
|
52
|
+
storage: localStorage,
|
|
53
|
+
paths: ['menus', 'openmenuKey', 'selectmenuKey', 'tagmenus', 'permissions'],
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
export const useUserStore = defineStore({
|
|
3
|
+
id: 'TOKEN_USER_INFO',
|
|
4
|
+
state: () => {
|
|
5
|
+
return {
|
|
6
|
+
token: '',
|
|
7
|
+
userInfo: {},
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
actions: {
|
|
11
|
+
|
|
12
|
+
},
|
|
13
|
+
persist: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
strategies: [{
|
|
16
|
+
key: 'TOKEN_USER_INFO',
|
|
17
|
+
storage: localStorage,
|
|
18
|
+
paths: ['token', 'userInfo' ],
|
|
19
|
+
}]
|
|
20
|
+
}
|
|
21
|
+
})
|