zant-admin 2.0.2 → 2.0.4

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.
Files changed (69) hide show
  1. package/.editorconfig +6 -0
  2. package/.env.development +3 -0
  3. package/.env.production +1 -0
  4. package/.env.test +1 -0
  5. package/.gitignore +36 -0
  6. package/.prettierrc.json +9 -0
  7. package/README.en.md +461 -272
  8. package/README.md +4 -3
  9. package/bin/cli.js +1 -1
  10. package/eslint.config.js +30 -0
  11. package/index.html +13 -0
  12. package/jsconfig.json +8 -0
  13. package/package.json +11 -3
  14. package/src/App.vue +16 -16
  15. package/src/api/methods/logError.js +8 -8
  16. package/src/api/methods/logOperation.js +8 -8
  17. package/src/api/methods/login.js +6 -6
  18. package/src/api/methods/quartz.js +36 -36
  19. package/src/api/methods/region.js +16 -16
  20. package/src/api/methods/sysAccount.js +29 -29
  21. package/src/api/methods/sysDict.js +29 -29
  22. package/src/api/methods/sysDictItem.js +26 -26
  23. package/src/api/methods/sysMenu.js +42 -42
  24. package/src/api/methods/sysRole.js +35 -35
  25. package/src/api/methods/sysUser.js +25 -25
  26. package/src/api/methods/system.js +15 -15
  27. package/src/api/request.js +225 -225
  28. package/src/assets/css/zcui.css +1023 -1023
  29. package/src/components/IconPicker.vue +351 -351
  30. package/src/components/MainPage.vue +838 -838
  31. package/src/components/details/logErrorDetails.vue +58 -58
  32. package/src/components/details/logOperationDetails.vue +76 -76
  33. package/src/components/edit/QuartzEdit.vue +221 -221
  34. package/src/components/edit/SysAccountEdit.vue +185 -185
  35. package/src/components/edit/SysDictEdit.vue +116 -116
  36. package/src/components/edit/SysDictItemEdit.vue +136 -136
  37. package/src/components/edit/SysRoleEdit.vue +111 -111
  38. package/src/config/index.js +74 -74
  39. package/src/directives/permission.js +49 -49
  40. package/src/main.js +37 -37
  41. package/src/stores/config.js +43 -43
  42. package/src/stores/dict.js +33 -33
  43. package/src/stores/menu.js +81 -81
  44. package/src/stores/user.js +21 -21
  45. package/src/utils/baseEcharts.js +661 -661
  46. package/src/utils/dictTemplate.js +26 -26
  47. package/src/utils/regionUtils.js +173 -173
  48. package/src/utils/useFormCRUD.js +59 -59
  49. package/src/views/baiscstatis/center.vue +474 -474
  50. package/src/views/baiscstatis/iframePage.vue +29 -29
  51. package/src/views/baiscstatis/notFound.vue +192 -192
  52. package/src/views/console.vue +821 -821
  53. package/src/views/demo/button.vue +269 -269
  54. package/src/views/demo/importexport.vue +119 -119
  55. package/src/views/demo/region.vue +322 -322
  56. package/src/views/demo/statistics.vue +214 -214
  57. package/src/views/home.vue +6 -6
  58. package/src/views/operations/log/logError.vue +78 -78
  59. package/src/views/operations/log/logLogin.vue +66 -66
  60. package/src/views/operations/log/logOperation.vue +103 -103
  61. package/src/views/operations/log/logQuartz.vue +56 -56
  62. package/src/views/operations/quartz.vue +179 -179
  63. package/src/views/operations/serviceMonitoring.vue +134 -134
  64. package/src/views/system/sysAccount.vue +128 -128
  65. package/src/views/system/sysDict.vue +159 -159
  66. package/src/views/system/sysDictItem.vue +118 -118
  67. package/src/views/system/sysMenu.vue +225 -225
  68. package/src/views/system/sysRole.vue +207 -207
  69. package/vite.config.js +33 -0
@@ -1,838 +1,838 @@
1
- <template>
2
- <div class="common-layout">
3
- <a-layout v-if="formConfig.navigationMode == 'side'">
4
- <!-- 侧边栏部分 -->
5
- <a-layout-sider
6
- v-model:collapsed="collapsed"
7
- :trigger="null"
8
- collapsible
9
- :style="{
10
- overflow: 'auto',
11
- height: '100vh',
12
- position: 'fixed',
13
- left: 0,
14
- top: 0,
15
- bottom: 0,
16
- background: formConfig.themeClass == 'light' ? '#fff' : '',
17
- }"
18
- >
19
- <div class="logo">
20
- <span v-if="!collapsed" :class="formConfig.themeClass == 'light' ? 'text-color-primary' : 'text-color-white '">
21
- <div class="flex-row"><img src="@/assets/imgs/logo.png" alt="" width="30px" class="margin-right-10" />{{ config.projectName }}</div>
22
- </span>
23
- <span v-else :class="formConfig.themeClass == 'light' ? 'text-color-primary' : 'text-color-white '"><img src="@/assets/imgs/logo.png" alt="" width="30px" /></span>
24
- </div>
25
- <a-menu
26
- v-model:openKeys="openmenuKey"
27
- v-model:selectedKeys="selectmenuKey"
28
- mode="inline"
29
- :theme="formConfig.themeClass"
30
- :inline-collapsed="collapsed"
31
- :items="menuItems"
32
- @click="menuclick"
33
- ></a-menu>
34
- </a-layout-sider>
35
- <!-- 主体部分 -->
36
- <a-layout :style="{ marginLeft: collapsed ? '80px' : '200px' }">
37
- <a-layout-header
38
- class="zc-layout-header"
39
- :style="{
40
- position: 'fixed',
41
- zIndex: 1,
42
- width: collapsed ? 'calc(100% - 80px)' : 'calc(100% - 200px)',
43
- }"
44
- >
45
- <div class="flex-row">
46
- <div class="flex-item text-align-left">
47
- <menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
48
- <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
49
- <a class="text-color-black" @click="reload">
50
- <UndoOutlined :style="{ fontSize: '18px' }" />
51
- </a>
52
- <!-- 面包屑导航 -->
53
- <a-breadcrumb class="breadcrumb" v-if="breadcrumbItems.length > 0">
54
- <a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.key">
55
- {{ item.title }}
56
- </a-breadcrumb-item>
57
- </a-breadcrumb>
58
- </div>
59
- <div class="flex-item text-align-right padding-right-20">
60
- <a-space :size="18">
61
- <a-dropdown>
62
- <a class="text-color-black" @click.prevent>
63
- <div class="flex-row">
64
- <div class="flex-item">
65
- <a-avatar
66
- :size="35"
67
- :style="{
68
- backgroundColor: '#1677ff',
69
- verticalAlign: 'middle',
70
- margin: '0 0 5px 0',
71
- }"
72
- >
73
- {{ username?.charAt(0) }}
74
- </a-avatar>
75
- </div>
76
-
77
- <div class="flex-item margin-left-10 text-align-left">
78
- <div class="username">{{ username }}</div>
79
- <div class="userphone">{{ mobile }}</div>
80
- </div>
81
- </div>
82
- </a>
83
- <template #overlay>
84
- <a-menu @click="avatardropdownonClick">
85
- <a-menu-item key="1"> 个人中心 </a-menu-item>
86
- <a-menu-item key="2"> 退出 </a-menu-item>
87
- </a-menu>
88
- </template>
89
- </a-dropdown>
90
- <a class="text-color-black" @click.prevent @click="toggleFullScreen">
91
- <a-tooltip :title="isFullScreen ? '退出全屏' : '进入全屏'">
92
- <FullscreenOutlined v-if="isFullScreen" />
93
- <FullscreenExitOutlined v-else />
94
- </a-tooltip>
95
- </a>
96
- <a-dropdown>
97
- <a class="text-color-black" @click.prevent>
98
- <GlobalOutlined />
99
- </a>
100
- <template #overlay>
101
- <a-menu @click="dropdownonLanguageClick">
102
- <a-menu-item key="zhCN">
103
- <span :class="config.dayjsLocale == 'zh-cn' ? 'text-color-primary' : ''">简体中文</span>
104
- </a-menu-item>
105
- <a-menu-item key="enUS">
106
- <span :class="config.dayjsLocale == 'en' ? 'text-color-primary' : ''">English</span>
107
- </a-menu-item>
108
- </a-menu>
109
- </template>
110
- </a-dropdown>
111
- <a class="text-color-black" @click.prevent @click="showDrawer">
112
- <SettingOutlined />
113
- </a>
114
- </a-space>
115
- </div>
116
- </div>
117
- </a-layout-header>
118
- <!-- 选项卡组件 -->
119
- <div :class="['zc-layout-content-tabs', { collapsed: collapsed }]">
120
- <div class="flex-row">
121
- <div class="zc-layout-content-tabs-left">
122
- <div class="my-tabs-wrapper" @wheel.prevent="onWheelScroll" ref="scrollContainer">
123
- <a-tabs v-model:activeKey="selectmenuKey[0]" hide-add type="editable-card" size="small" @edit="tagdelete" @tabClick="tagchange">
124
- <a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable" class="tag"> </a-tab-pane>
125
- </a-tabs>
126
- </div>
127
- </div>
128
-
129
- <div class="zc-layout-content-tabs-right">
130
- <a-dropdown>
131
- <a class="text-color-black" @click.prevent>
132
- <MoreOutlined :style="{ color: 'black', fontSize: '18px' }" />
133
- </a>
134
- <template #overlay>
135
- <a-menu @click="dropdownonClick">
136
- <a-menu-item key="1"> <ArrowLeftOutlined /> 关闭左侧 </a-menu-item>
137
- <a-menu-item key="2"> <ArrowRightOutlined /> 关闭右侧 </a-menu-item>
138
- <a-menu-item key="3"> <CloseOutlined /> 关闭其它 </a-menu-item>
139
- <a-menu-item key="4"> <CloseCircleOutlined /> 全部关闭 </a-menu-item>
140
- </a-menu>
141
- </template>
142
- </a-dropdown>
143
- </div>
144
- </div>
145
- </div>
146
- <!-- 路由视图部分 -->
147
- <a-layout-content :style="{ margin: '120px 8px 8px 8px' }">
148
- <router-view v-slot="{ Component, route }">
149
- <keep-alive v-if="route.meta.cache">
150
- <component :is="Component" />
151
- </keep-alive>
152
- <component v-else :is="Component" />
153
- </router-view>
154
- </a-layout-content>
155
- </a-layout>
156
- </a-layout>
157
- <!-- 顶部菜单布局 -->
158
- <a-layout v-else>
159
- <a-layout-header
160
- class="zc-layout-header-top"
161
- :style="{
162
- position: 'fixed',
163
- zIndex: 1,
164
- width: '100%',
165
- background: formConfig.themeClass == 'light' ? '#fff' : '#001529',
166
- }"
167
- >
168
- <div class="flex-row">
169
- <div class="flex-item-9">
170
- <div class="flex-row">
171
- <div class="flex-item-1" style="line-height: 32px; height: 32px; margin: 16px">
172
- <a-flex>
173
- <img src="@/assets/imgs/logo.png" alt="" width="30px" class="margin-right-10" />
174
- <span :class="formConfig.themeClass == 'light' ? 'text-color-primary' : 'text-color-white '" style="font-size: 16px">{{ config.projectName }}</span>
175
- </a-flex>
176
- </div>
177
- <div class="flex-item-9">
178
- <a-menu v-model:selectedKeys="selectmenuKey" mode="horizontal" :theme="formConfig.themeClass" :items="menuItems" @click="menuclick"></a-menu>
179
- </div>
180
- </div>
181
- </div>
182
- <div class="flex-item-1 text-align-right padding-right-20">
183
- <a-space :size="18">
184
- <a-dropdown>
185
- <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent>
186
- <div class="flex-row">
187
- <div class="flex-item">
188
- <a-avatar
189
- :size="35"
190
- :style="{
191
- backgroundColor: '#1677ff',
192
- verticalAlign: 'middle',
193
- margin: '0 0 5px 0',
194
- }"
195
- >
196
- {{ username?.charAt(0) }}
197
- </a-avatar>
198
- </div>
199
-
200
- <div class="flex-item margin-left-10 text-align-left">
201
- <div class="username" :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'">{{ username }}</div>
202
- <div class="userphone" :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'">{{ mobile }}</div>
203
- </div>
204
- </div>
205
- </a>
206
- <template #overlay>
207
- <a-menu @click="avatardropdownonClick">
208
- <a-menu-item key="1"> 个人中心 </a-menu-item>
209
- <a-menu-item key="2"> 退出 </a-menu-item>
210
- </a-menu>
211
- </template>
212
- </a-dropdown>
213
- <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent @click="toggleFullScreen">
214
- <a-tooltip :title="isFullScreen ? '退出全屏' : '进入全屏'">
215
- <FullscreenOutlined v-if="isFullScreen" />
216
- <FullscreenExitOutlined v-else />
217
- </a-tooltip>
218
- </a>
219
- <a-dropdown>
220
- <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent>
221
- <GlobalOutlined />
222
- </a>
223
- <template #overlay>
224
- <a-menu @click="dropdownonLanguageClick">
225
- <a-menu-item key="zhCN">
226
- <span :class="config.locale.locale == 'zh-cn' ? 'text-color-primary' : ''">简体中文</span>
227
- </a-menu-item>
228
- <a-menu-item key="enUS">
229
- <span :class="config.locale.locale == 'en' ? 'text-color-primary' : ''">English</span>
230
- </a-menu-item>
231
- </a-menu>
232
- </template>
233
- </a-dropdown>
234
- <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent @click="showDrawer">
235
- <SettingOutlined />
236
- </a>
237
- </a-space>
238
- </div>
239
- </div>
240
- </a-layout-header>
241
- <!-- 选项卡组件 -->
242
- <div :class="['zc-layout-content-tabs', 'top-layout', { collapsed: collapsed }]">
243
- <div class="flex-row">
244
- <div class="zc-layout-content-tabs-left">
245
- <div class="my-tabs-wrapper" @wheel.prevent="onWheelScroll" ref="scrollContainer">
246
- <a-tabs v-model:activeKey="selectmenuKey[0]" hide-add type="editable-card" size="small" @edit="tagdelete" @tabClick="tagchange">
247
- <a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable" class="tag"> </a-tab-pane>
248
- </a-tabs>
249
- </div>
250
- </div>
251
-
252
- <div class="zc-layout-content-tabs-right">
253
- <a-dropdown>
254
- <a class="text-color-black" @click.prevent>
255
- <MoreOutlined :style="{ color: 'black', fontSize: '18px' }" />
256
- </a>
257
- <template #overlay>
258
- <a-menu @click="dropdownonClick">
259
- <a-menu-item key="1"> <ArrowLeftOutlined /> 关闭左侧 </a-menu-item>
260
- <a-menu-item key="2"> <ArrowRightOutlined /> 关闭右侧 </a-menu-item>
261
- <a-menu-item key="3"> <CloseOutlined /> 关闭其它 </a-menu-item>
262
- <a-menu-item key="4"> <CloseCircleOutlined /> 全部关闭 </a-menu-item>
263
- </a-menu>
264
- </template>
265
- </a-dropdown>
266
- </div>
267
- </div>
268
- </div>
269
- <!-- 面包屑导航 -->
270
- <a-breadcrumb class="breadcrumb-top" v-if="breadcrumbItems.length > 0">
271
- <a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.key">
272
- {{ item.title }}
273
- </a-breadcrumb-item>
274
- </a-breadcrumb>
275
- <!-- 路由视图部分 -->
276
- <a-layout-content :style="{ margin: '120px 8px 8px 8px' }">
277
- <router-view v-slot="{ Component, route }">
278
- <keep-alive v-if="route.meta.cache">
279
- <component :is="Component" />
280
- </keep-alive>
281
- <component v-else :is="Component" />
282
- </router-view>
283
- </a-layout-content>
284
- </a-layout>
285
- <!-- 抽屉组件 -->
286
- <a-drawer title="配置" :closable="false" :open="open" @close="onClose">
287
- <a-form name="config_form" :model="formConfig">
288
- <div class="blockquote">主题</div>
289
- <div class="padding-10">
290
- <a-radio-group v-model:value="formConfig.themeClass" @change="themeClasschangeTheme">
291
- <a-radio value="dark">黑</a-radio>
292
- <a-radio value="light">白</a-radio>
293
- </a-radio-group>
294
- </div>
295
- <div class="blockquote">导航模式</div>
296
- <div class="padding-10">
297
- <a-radio-group v-model:value="formConfig.navigationMode" @change="navigationModechangeTheme">
298
- <a-radio value="side">侧边菜单布局</a-radio>
299
- <a-radio value="top">顶部菜单布局</a-radio>
300
- </a-radio-group>
301
- </div>
302
- <div class="blockquote">表格设置</div>
303
- <div class="padding-10">
304
- <a-row>
305
- <a-col :span="12">表格边框</a-col>
306
- <a-col :span="12">
307
- <a-switch v-model:checked="formConfig.tableBordered" checked-children="边框开" un-checked-children="边框关" @change="tableBorderedchangeTheme" />
308
- </a-col>
309
- </a-row>
310
- </div>
311
- </a-form>
312
- </a-drawer>
313
- </div>
314
- </template>
315
- <script setup>
316
- import { ref, reactive, h, provide, getCurrentInstance, watch, onMounted } from 'vue'
317
- import { menuStore } from '@/stores/menu'
318
- import { configStore } from '@/stores/config'
319
- import { useUserStore } from '@/stores/user'
320
- import router from '@/router'
321
- import zhCN from 'ant-design-vue/es/locale/zh_CN'
322
- import enUS from 'ant-design-vue/es/locale/en_US'
323
- const user = useUserStore()
324
- const username = user.userInfo.name
325
- const mobile = user.userInfo.mobile
326
- // 菜单状态
327
- const menu = menuStore()
328
- const config = configStore()
329
- const collapsed = ref(false)
330
- const openmenuKey = ref(menu.openmenuKey)
331
- const selectmenuKey = ref(menu.selectmenuKey)
332
- const panes = ref(menu.tagmenus)
333
- // 定义一个布尔状态来跟踪是否处于全屏
334
- const isFullScreen = ref(false)
335
- const formConfig = reactive({
336
- themeClass: config.themeClass,
337
- tableBordered: config.tableBordered,
338
- navigationMode: config.navigationMode,
339
- })
340
- const menuItems = ref([])
341
- const { proxy } = getCurrentInstance() // 获取当前实例
342
- // 菜单项生成
343
- const menuinit = () => {
344
- const icons = proxy.$icons // 访问全局图标
345
- const createMenuItem = menu => {
346
- return menu.map(item => {
347
- // 动态解析图标
348
- const iconComponent = item.icon ? icons[item.icon] : null
349
- const menuItem = {
350
- key: item.id,
351
- parentId: item.parentId,
352
- icon: iconComponent ? () => h(iconComponent) : undefined, // 动态渲染图标
353
- label: item.title,
354
- title: item.title,
355
- path: item.path,
356
- type: item.type,
357
- url: item.url,
358
- }
359
- if (item.children && item.children.length > 0) {
360
- menuItem.children = createMenuItem(item.children)
361
- }
362
- return menuItem
363
- })
364
- }
365
- menuItems.value = createMenuItem(menu.menus)
366
- }
367
- menuinit()
368
- // 菜单点击事件
369
- const menuclick = ({ item, key }) => {
370
- const type = item.originItemValue.type
371
- const url = item.url
372
-
373
- if (type === 4 && url) {
374
- // 外链:新窗口打开,不生成 tab
375
- window.open(url, '_blank')
376
- return
377
- }
378
- let current = panes.value.find(x => x.key === key)
379
- if (!current) {
380
- current = {
381
- key,
382
- parentId: item.parentId,
383
- title: item.title,
384
- path: item.path,
385
- closable: true,
386
- type,
387
- url,
388
- }
389
- panes.value.push(current)
390
- }
391
- updateMenuPanes(current)
392
- }
393
- // 点击 Tag 切换选项卡
394
- const tagchange = key => {
395
- const current = panes.value.find(x => x.key === key)
396
- if (current) {
397
- updateMenuPanes(current)
398
- }
399
- }
400
-
401
- // 删除 Tag
402
- const tagdelete = (key, action) => {
403
- if (action === 'add') {
404
- //add();
405
- } else {
406
- if (key == selectmenuKey.value[0]) {
407
- const index = panes.value.findIndex(item => item.key === key)
408
- let nextPane = null
409
- //如果有下一条
410
- if (index + 1 < panes.value.length) {
411
- // 有下一个
412
- nextPane = panes.value[index + 1]
413
- } else if (index - 1 >= 0) {
414
- // 有上一个
415
- nextPane = panes.value[index - 1]
416
- }
417
- if (nextPane) {
418
- updateMenuPanes(nextPane)
419
- }
420
- }
421
- panes.value = panes.value.filter(x => x.key !== key)
422
- menu.tagmenus = panes.value
423
- }
424
- }
425
- // 下拉菜单点击事件
426
- const dropdownonClick = ({ key }) => {
427
- const index = panes.value.findIndex(item => item.key === selectmenuKey.value[0])
428
- switch (key) {
429
- case '1':
430
- panes.value = panes.value.filter((item, i) => i >= index || item.key === 0)
431
- menu.tagmenus = panes.value
432
- break
433
- case '2':
434
- panes.value = panes.value.filter((item, i) => i <= index)
435
- menu.tagmenus = panes.value
436
- break
437
- case '3':
438
- panes.value = panes.value.filter(x => x.key === selectmenuKey.value[0] || x.key === 0)
439
- menu.tagmenus = panes.value
440
- break
441
- case '4':
442
- panes.value = panes.value.filter(x => x.key === 0)
443
- menu.tagmenus = panes.value
444
- // 切换到控制台
445
- const consolePane = {
446
- key: 'console',
447
- parentId: 0,
448
- title: '控制台',
449
- path: '/console',
450
- closable: false,
451
- type: 1, // 普通路由
452
- url: '',
453
- }
454
- updateMenuPanes(consolePane)
455
- break
456
- default:
457
- break
458
- }
459
- }
460
- const findAllByHref = (items, targetHref) => {
461
- let result = []
462
- items.forEach(item => {
463
- if (item.path === targetHref) {
464
- result.push(item)
465
- }
466
- if (item.children && item.children.length > 0) {
467
- result = result.concat(findAllByHref(item.children, targetHref))
468
- }
469
- })
470
- return result
471
- }
472
- //个人中心和退出
473
- const avatardropdownonClick = ({ key }) => {
474
- switch (key) {
475
- case '1':
476
- const result = findAllByHref(menuItems.value, '/center')
477
- let pane
478
- if (result.length === 0) {
479
- // 如果菜单项中没有个人中心,创建一个默认的个人中心菜单项
480
- pane = {
481
- key: 'center',
482
- parentId: '0',
483
- title: '个人中心',
484
- path: '/center',
485
- closable: true,
486
- type: 2, // 普通菜单
487
- url: '',
488
- }
489
- } else {
490
- pane = {
491
- key: result[0].key,
492
- parentId: result[0].parentId,
493
- title: result[0].title,
494
- path: result[0].path,
495
- closable: true,
496
- type: result[0].type,
497
- url: result[0].url,
498
- }
499
- }
500
- const existingTab = panes.value.find(x => x.key === pane.key)
501
- if (!existingTab) {
502
- panes.value.push(pane)
503
- }
504
- updateMenuPanes(pane)
505
- break
506
- case '2':
507
- user.$reset()
508
- menu.$reset()
509
- router.push('/login')
510
- break
511
- default:
512
- break
513
- }
514
- }
515
-
516
- function findParentKeys(menuArray, targetKey, parentKeys = []) {
517
- for (const item of menuArray) {
518
- // 如果找到目标项,返回当前收集的父级key
519
- if (item.key === targetKey) {
520
- return parentKeys
521
- }
522
-
523
- // 如果有子菜单,递归查找
524
- if (item.children && item.children.length > 0) {
525
- const result = findParentKeys(item.children, targetKey, [...parentKeys, item.key])
526
- if (result) {
527
- return result
528
- }
529
- }
530
- }
531
- return null
532
- }
533
- // 更新选中的选项卡和路由
534
- const updateMenuPanes = pane => {
535
- selectmenuKey.value = [pane.key]
536
- if (pane.key != 0) {
537
- const parentKeys = findParentKeys(menuItems.value, pane.key)
538
- // 设置展开的菜单key,包括所有上级key
539
- openmenuKey.value = parentKeys ? [...parentKeys] : []
540
- } else {
541
- openmenuKey.value = [pane.parentId]
542
- }
543
- menu.setSelectmenuKey(selectmenuKey.value)
544
- menu.setOpenmenuKey(openmenuKey.value)
545
-
546
- if (pane.type === 3 && pane.url) {
547
- // 内链:用 iframe 打开
548
- router.push({
549
- path: '/iframePage',
550
- query: { url: encodeURIComponent(pane.url) },
551
- })
552
- } else {
553
- // 普通菜单:走路由
554
- router.push(pane.path)
555
- }
556
- }
557
- //国际化
558
- const dropdownonLanguageClick = ({ key }) => {
559
- if (key === 'zhCN') {
560
- config.setLocale(zhCN)
561
- } else {
562
- config.setLocale(enUS)
563
- }
564
- }
565
- // 抽屉状态
566
- const open = ref(false)
567
- const showDrawer = () => {
568
- open.value = true
569
- }
570
- const onClose = () => {
571
- open.value = false
572
- }
573
- // 主题切换
574
- const themeClasschangeTheme = e => {
575
- config.themeClass = e.target.value
576
- }
577
- //导航模式
578
- const navigationModechangeTheme = e => {
579
- config.navigationMode = e.target.value
580
- }
581
-
582
- // 定义全屏切换方法
583
- const toggleFullScreen = () => {
584
- const element = document.documentElement
585
- if (!isFullScreen.value) {
586
- // 进入全屏模式
587
- if (element.requestFullscreen) {
588
- element.requestFullscreen()
589
- } else if (element.mozRequestFullScreen) {
590
- // Firefox
591
- element.mozRequestFullScreen()
592
- } else if (element.webkitRequestFullscreen) {
593
- // Chrome, Safari and Opera
594
- element.webkitRequestFullscreen()
595
- } else if (element.msRequestFullscreen) {
596
- // IE/Edge
597
- element.msRequestFullscreen()
598
- }
599
- } else {
600
- // 退出全屏模式
601
- if (document.exitFullscreen) {
602
- document.exitFullscreen()
603
- } else if (document.mozCancelFullScreen) {
604
- // Firefox
605
- document.mozCancelFullScreen()
606
- } else if (document.webkitExitFullscreen) {
607
- // Chrome, Safari and Opera
608
- document.webkitExitFullscreen()
609
- } else if (document.msExitFullscreen) {
610
- // IE/Edge
611
- document.msExitFullscreen()
612
- }
613
- }
614
- isFullScreen.value = !isFullScreen.value // 切换全屏状态
615
- }
616
- //表格边框切换
617
- const tableBorderedchangeTheme = checked => {
618
- config.tableBordered = checked
619
- }
620
- const reload = () => {
621
- window.location.reload()
622
- }
623
- const scrollContainer = ref(null)
624
- const onWheelScroll = e => {
625
- if (scrollContainer.value) {
626
- scrollContainer.value.scrollLeft += e.deltaY // 将垂直滚动转为水平滚动
627
- }
628
- }
629
- // 生成面包屑导航项
630
- const breadcrumbItems = ref([])
631
-
632
- /**
633
- * 根据当前选中的菜单项生成面包屑导航
634
- */
635
- const generateBreadcrumbItems = () => {
636
- breadcrumbItems.value = []
637
-
638
- if (selectmenuKey.value.length === 0) return
639
-
640
- const currentKey = selectmenuKey.value[0]
641
- const findItemPath = (items, targetKey, path = []) => {
642
- for (const item of items) {
643
- if (item.key === targetKey) {
644
- return [...path, item]
645
- }
646
- if (item.children && item.children.length > 0) {
647
- const result = findItemPath(item.children, targetKey, [...path, item])
648
- if (result) return result
649
- }
650
- }
651
- return null
652
- }
653
-
654
- const itemPath = findItemPath(menuItems.value, currentKey)
655
- if (itemPath) {
656
- breadcrumbItems.value = itemPath.map(item => ({
657
- key: item.key,
658
- title: item.title,
659
- path: item.path,
660
- type: item.type,
661
- url: item.url,
662
- }))
663
- }
664
- }
665
-
666
- // 监听选中菜单项变化,更新面包屑
667
- watch(
668
- selectmenuKey,
669
- () => {
670
- generateBreadcrumbItems()
671
- },
672
- { immediate: true },
673
- )
674
-
675
- // 组件挂载时恢复路由状态
676
- onMounted(() => {
677
- // 检查是否有持久化的选中菜单项
678
- if (selectmenuKey.value && selectmenuKey.value.length > 0 && selectmenuKey.value[0] !== 0) {
679
- const currentKey = selectmenuKey.value[0]
680
-
681
- // 在tagmenus中查找对应的pane
682
- const savedPane = panes.value.find(pane => pane.key === currentKey)
683
-
684
- if (savedPane) {
685
- // 如果找到了对应的pane,恢复路由状态
686
- updateMenuPanes(savedPane)
687
- } else {
688
- // 如果没有找到对应的pane,检查当前路由是否与持久化状态匹配
689
- const currentRoute = router.currentRoute.value
690
- if (currentRoute.path === '/' || currentRoute.path === '/home') {
691
- // 如果当前在首页,但持久化状态不是首页,尝试恢复
692
- const menuItem = findMenuItemByKey(menuItems.value, currentKey)
693
- if (menuItem) {
694
- // 创建新的pane并跳转
695
- const newPane = {
696
- key: menuItem.key,
697
- parentId: menuItem.parentId,
698
- title: menuItem.title,
699
- path: menuItem.path,
700
- closable: true,
701
- type: menuItem.type,
702
- url: menuItem.url,
703
- }
704
- panes.value.push(newPane)
705
- updateMenuPanes(newPane)
706
- }
707
- }
708
- }
709
- }
710
- })
711
-
712
- // 根据key在菜单项中查找对应项
713
- function findMenuItemByKey(items, targetKey) {
714
- for (const item of items) {
715
- if (item.key === targetKey) {
716
- return item
717
- }
718
- if (item.children && item.children.length > 0) {
719
- const result = findMenuItemByKey(item.children, targetKey)
720
- if (result) return result
721
- }
722
- }
723
- return null
724
- }
725
-
726
- // 提供方法
727
- provide('menuinit', menuinit)
728
- provide('menuclick', menuclick)
729
- </script>
730
- <style scoped>
731
- .trigger {
732
- font-size: 18px;
733
- line-height: 64px;
734
- padding: 0 24px;
735
- cursor: pointer;
736
- transition: color 0.3s;
737
- }
738
- .trigger:hover {
739
- color: #1890ff;
740
- }
741
- .logo {
742
- text-align: center;
743
- line-height: 32px;
744
- height: 32px;
745
- margin: 16px;
746
- font-size: 16px;
747
- }
748
- .site-layout .site-layout-background {
749
- background: #fff;
750
- }
751
- .zc-layout-header {
752
- background: #fff !important;
753
- padding: 0 !important;
754
- border-bottom: 1px solid rgba(5, 5, 5, 0.06);
755
- }
756
- .zc-layout-header-top {
757
- padding: 0 !important;
758
- }
759
- .zc-layout-content-tabs {
760
- align-items: center;
761
- position: fixed;
762
- top: 64px;
763
- left: 200px;
764
- right: 0;
765
- z-index: 100;
766
- background: #fff;
767
- border-bottom: 1px solid rgba(5, 5, 5, 0.06);
768
- transition: left 0.3s ease;
769
- }
770
-
771
- .zc-layout-content-tabs.collapsed {
772
- left: 80px;
773
- }
774
-
775
- .zc-layout-content-tabs.top-layout {
776
- left: 0;
777
- }
778
- .zc-layout-content-tabs-left {
779
- flex: 0.99;
780
- width: 90%;
781
- }
782
- .zc-layout-content-tabs-right {
783
- flex: 0.01;
784
- text-align: right;
785
- padding-right: 15px;
786
- }
787
-
788
- /* 使用深度选择器修改所有标签项的样式 */
789
- .my-tabs-wrapper :deep(.ant-tabs-tab) {
790
- border-radius: 4px !important; /* 圆角 */
791
- color: #999999 !important;
792
- background-color: #f5f5f5 !important;
793
- border: 0px !important;
794
- margin-right: 4px;
795
- margin: 8px;
796
- padding: 5px 10px !important;
797
- }
798
- /* 修改激活标签的样式 */
799
- .my-tabs-wrapper :deep(.ant-tabs-tab-active .ant-tabs-tab-btn .ant-tabs-tab-remove) {
800
- color: #1890ff !important;
801
- }
802
- .my-tabs-wrapper :deep(.ant-tabs-tab-active .ant-tabs-tab-remove) {
803
- color: #1890ff !important;
804
- }
805
- /* 标签悬停效果 */
806
- .my-tabs-wrapper :deep(.ant-tabs-tab:hover, .ant-tabs-tab-remove:hover) {
807
- color: #1890ff !important;
808
- }
809
- .my-tabs-wrapper :deep(.ant-tabs-content-holder) {
810
- display: none !important;
811
- }
812
- .my-tabs-wrapper :deep(.ant-tabs-nav) {
813
- margin: 0px !important;
814
- border-bottom: none !important; /* 移除底部分割线 */
815
- }
816
- .my-tabs-wrapper :deep(.ant-tabs-tab-remove) {
817
- margin-left: 0px !important;
818
- }
819
-
820
- .breadcrumb {
821
- display: inline-block;
822
- margin-left: 16px;
823
- }
824
- .breadcrumb-top {
825
- margin-top: 8px;
826
- padding-left: 16px;
827
- }
828
-
829
- .username {
830
- line-height: 1.2;
831
- margin-bottom: 2px;
832
- }
833
-
834
- .userphone {
835
- line-height: 1.2;
836
- margin-bottom: 4px;
837
- }
838
- </style>
1
+ <template>
2
+ <div class="common-layout">
3
+ <a-layout v-if="formConfig.navigationMode == 'side'">
4
+ <!-- 侧边栏部分 -->
5
+ <a-layout-sider
6
+ v-model:collapsed="collapsed"
7
+ :trigger="null"
8
+ collapsible
9
+ :style="{
10
+ overflow: 'auto',
11
+ height: '100vh',
12
+ position: 'fixed',
13
+ left: 0,
14
+ top: 0,
15
+ bottom: 0,
16
+ background: formConfig.themeClass == 'light' ? '#fff' : '',
17
+ }"
18
+ >
19
+ <div class="logo">
20
+ <span v-if="!collapsed" :class="formConfig.themeClass == 'light' ? 'text-color-primary' : 'text-color-white '">
21
+ <div class="flex-row"><img src="@/assets/imgs/logo.png" alt="" width="30px" class="margin-right-10" />{{ config.projectName }}</div>
22
+ </span>
23
+ <span v-else :class="formConfig.themeClass == 'light' ? 'text-color-primary' : 'text-color-white '"><img src="@/assets/imgs/logo.png" alt="" width="30px" /></span>
24
+ </div>
25
+ <a-menu
26
+ v-model:openKeys="openmenuKey"
27
+ v-model:selectedKeys="selectmenuKey"
28
+ mode="inline"
29
+ :theme="formConfig.themeClass"
30
+ :inline-collapsed="collapsed"
31
+ :items="menuItems"
32
+ @click="menuclick"
33
+ ></a-menu>
34
+ </a-layout-sider>
35
+ <!-- 主体部分 -->
36
+ <a-layout :style="{ marginLeft: collapsed ? '80px' : '200px' }">
37
+ <a-layout-header
38
+ class="zc-layout-header"
39
+ :style="{
40
+ position: 'fixed',
41
+ zIndex: 1,
42
+ width: collapsed ? 'calc(100% - 80px)' : 'calc(100% - 200px)',
43
+ }"
44
+ >
45
+ <div class="flex-row">
46
+ <div class="flex-item text-align-left">
47
+ <menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
48
+ <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
49
+ <a class="text-color-black" @click="reload">
50
+ <UndoOutlined :style="{ fontSize: '18px' }" />
51
+ </a>
52
+ <!-- 面包屑导航 -->
53
+ <a-breadcrumb class="breadcrumb" v-if="breadcrumbItems.length > 0">
54
+ <a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.key">
55
+ {{ item.title }}
56
+ </a-breadcrumb-item>
57
+ </a-breadcrumb>
58
+ </div>
59
+ <div class="flex-item text-align-right padding-right-20">
60
+ <a-space :size="18">
61
+ <a-dropdown>
62
+ <a class="text-color-black" @click.prevent>
63
+ <div class="flex-row">
64
+ <div class="flex-item">
65
+ <a-avatar
66
+ :size="35"
67
+ :style="{
68
+ backgroundColor: '#1677ff',
69
+ verticalAlign: 'middle',
70
+ margin: '0 0 5px 0',
71
+ }"
72
+ >
73
+ {{ username?.charAt(0) }}
74
+ </a-avatar>
75
+ </div>
76
+
77
+ <div class="flex-item margin-left-10 text-align-left">
78
+ <div class="username">{{ username }}</div>
79
+ <div class="userphone">{{ mobile }}</div>
80
+ </div>
81
+ </div>
82
+ </a>
83
+ <template #overlay>
84
+ <a-menu @click="avatardropdownonClick">
85
+ <a-menu-item key="1"> 个人中心 </a-menu-item>
86
+ <a-menu-item key="2"> 退出 </a-menu-item>
87
+ </a-menu>
88
+ </template>
89
+ </a-dropdown>
90
+ <a class="text-color-black" @click.prevent @click="toggleFullScreen">
91
+ <a-tooltip :title="isFullScreen ? '退出全屏' : '进入全屏'">
92
+ <FullscreenOutlined v-if="isFullScreen" />
93
+ <FullscreenExitOutlined v-else />
94
+ </a-tooltip>
95
+ </a>
96
+ <a-dropdown>
97
+ <a class="text-color-black" @click.prevent>
98
+ <GlobalOutlined />
99
+ </a>
100
+ <template #overlay>
101
+ <a-menu @click="dropdownonLanguageClick">
102
+ <a-menu-item key="zhCN">
103
+ <span :class="config.dayjsLocale == 'zh-cn' ? 'text-color-primary' : ''">简体中文</span>
104
+ </a-menu-item>
105
+ <a-menu-item key="enUS">
106
+ <span :class="config.dayjsLocale == 'en' ? 'text-color-primary' : ''">English</span>
107
+ </a-menu-item>
108
+ </a-menu>
109
+ </template>
110
+ </a-dropdown>
111
+ <a class="text-color-black" @click.prevent @click="showDrawer">
112
+ <SettingOutlined />
113
+ </a>
114
+ </a-space>
115
+ </div>
116
+ </div>
117
+ </a-layout-header>
118
+ <!-- 选项卡组件 -->
119
+ <div :class="['zc-layout-content-tabs', { collapsed: collapsed }]">
120
+ <div class="flex-row">
121
+ <div class="zc-layout-content-tabs-left">
122
+ <div class="my-tabs-wrapper" @wheel.prevent="onWheelScroll" ref="scrollContainer">
123
+ <a-tabs v-model:activeKey="selectmenuKey[0]" hide-add type="editable-card" size="small" @edit="tagdelete" @tabClick="tagchange">
124
+ <a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable" class="tag"> </a-tab-pane>
125
+ </a-tabs>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="zc-layout-content-tabs-right">
130
+ <a-dropdown>
131
+ <a class="text-color-black" @click.prevent>
132
+ <MoreOutlined :style="{ color: 'black', fontSize: '18px' }" />
133
+ </a>
134
+ <template #overlay>
135
+ <a-menu @click="dropdownonClick">
136
+ <a-menu-item key="1"> <ArrowLeftOutlined /> 关闭左侧 </a-menu-item>
137
+ <a-menu-item key="2"> <ArrowRightOutlined /> 关闭右侧 </a-menu-item>
138
+ <a-menu-item key="3"> <CloseOutlined /> 关闭其它 </a-menu-item>
139
+ <a-menu-item key="4"> <CloseCircleOutlined /> 全部关闭 </a-menu-item>
140
+ </a-menu>
141
+ </template>
142
+ </a-dropdown>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ <!-- 路由视图部分 -->
147
+ <a-layout-content :style="{ margin: '120px 8px 8px 8px' }">
148
+ <router-view v-slot="{ Component, route }">
149
+ <keep-alive v-if="route.meta.cache">
150
+ <component :is="Component" />
151
+ </keep-alive>
152
+ <component v-else :is="Component" />
153
+ </router-view>
154
+ </a-layout-content>
155
+ </a-layout>
156
+ </a-layout>
157
+ <!-- 顶部菜单布局 -->
158
+ <a-layout v-else>
159
+ <a-layout-header
160
+ class="zc-layout-header-top"
161
+ :style="{
162
+ position: 'fixed',
163
+ zIndex: 1,
164
+ width: '100%',
165
+ background: formConfig.themeClass == 'light' ? '#fff' : '#001529',
166
+ }"
167
+ >
168
+ <div class="flex-row">
169
+ <div class="flex-item-9">
170
+ <div class="flex-row">
171
+ <div class="flex-item-1" style="line-height: 32px; height: 32px; margin: 16px">
172
+ <a-flex>
173
+ <img src="@/assets/imgs/logo.png" alt="" width="30px" class="margin-right-10" />
174
+ <span :class="formConfig.themeClass == 'light' ? 'text-color-primary' : 'text-color-white '" style="font-size: 16px">{{ config.projectName }}</span>
175
+ </a-flex>
176
+ </div>
177
+ <div class="flex-item-9">
178
+ <a-menu v-model:selectedKeys="selectmenuKey" mode="horizontal" :theme="formConfig.themeClass" :items="menuItems" @click="menuclick"></a-menu>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ <div class="flex-item-1 text-align-right padding-right-20">
183
+ <a-space :size="18">
184
+ <a-dropdown>
185
+ <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent>
186
+ <div class="flex-row">
187
+ <div class="flex-item">
188
+ <a-avatar
189
+ :size="35"
190
+ :style="{
191
+ backgroundColor: '#1677ff',
192
+ verticalAlign: 'middle',
193
+ margin: '0 0 5px 0',
194
+ }"
195
+ >
196
+ {{ username?.charAt(0) }}
197
+ </a-avatar>
198
+ </div>
199
+
200
+ <div class="flex-item margin-left-10 text-align-left">
201
+ <div class="username" :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'">{{ username }}</div>
202
+ <div class="userphone" :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'">{{ mobile }}</div>
203
+ </div>
204
+ </div>
205
+ </a>
206
+ <template #overlay>
207
+ <a-menu @click="avatardropdownonClick">
208
+ <a-menu-item key="1"> 个人中心 </a-menu-item>
209
+ <a-menu-item key="2"> 退出 </a-menu-item>
210
+ </a-menu>
211
+ </template>
212
+ </a-dropdown>
213
+ <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent @click="toggleFullScreen">
214
+ <a-tooltip :title="isFullScreen ? '退出全屏' : '进入全屏'">
215
+ <FullscreenOutlined v-if="isFullScreen" />
216
+ <FullscreenExitOutlined v-else />
217
+ </a-tooltip>
218
+ </a>
219
+ <a-dropdown>
220
+ <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent>
221
+ <GlobalOutlined />
222
+ </a>
223
+ <template #overlay>
224
+ <a-menu @click="dropdownonLanguageClick">
225
+ <a-menu-item key="zhCN">
226
+ <span :class="config.locale.locale == 'zh-cn' ? 'text-color-primary' : ''">简体中文</span>
227
+ </a-menu-item>
228
+ <a-menu-item key="enUS">
229
+ <span :class="config.locale.locale == 'en' ? 'text-color-primary' : ''">English</span>
230
+ </a-menu-item>
231
+ </a-menu>
232
+ </template>
233
+ </a-dropdown>
234
+ <a :class="formConfig.themeClass == 'light' ? 'text-color-black' : 'text-color-white'" @click.prevent @click="showDrawer">
235
+ <SettingOutlined />
236
+ </a>
237
+ </a-space>
238
+ </div>
239
+ </div>
240
+ </a-layout-header>
241
+ <!-- 选项卡组件 -->
242
+ <div :class="['zc-layout-content-tabs', 'top-layout', { collapsed: collapsed }]">
243
+ <div class="flex-row">
244
+ <div class="zc-layout-content-tabs-left">
245
+ <div class="my-tabs-wrapper" @wheel.prevent="onWheelScroll" ref="scrollContainer">
246
+ <a-tabs v-model:activeKey="selectmenuKey[0]" hide-add type="editable-card" size="small" @edit="tagdelete" @tabClick="tagchange">
247
+ <a-tab-pane v-for="pane in panes" :key="pane.key" :tab="pane.title" :closable="pane.closable" class="tag"> </a-tab-pane>
248
+ </a-tabs>
249
+ </div>
250
+ </div>
251
+
252
+ <div class="zc-layout-content-tabs-right">
253
+ <a-dropdown>
254
+ <a class="text-color-black" @click.prevent>
255
+ <MoreOutlined :style="{ color: 'black', fontSize: '18px' }" />
256
+ </a>
257
+ <template #overlay>
258
+ <a-menu @click="dropdownonClick">
259
+ <a-menu-item key="1"> <ArrowLeftOutlined /> 关闭左侧 </a-menu-item>
260
+ <a-menu-item key="2"> <ArrowRightOutlined /> 关闭右侧 </a-menu-item>
261
+ <a-menu-item key="3"> <CloseOutlined /> 关闭其它 </a-menu-item>
262
+ <a-menu-item key="4"> <CloseCircleOutlined /> 全部关闭 </a-menu-item>
263
+ </a-menu>
264
+ </template>
265
+ </a-dropdown>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ <!-- 面包屑导航 -->
270
+ <a-breadcrumb class="breadcrumb-top" v-if="breadcrumbItems.length > 0">
271
+ <a-breadcrumb-item v-for="item in breadcrumbItems" :key="item.key">
272
+ {{ item.title }}
273
+ </a-breadcrumb-item>
274
+ </a-breadcrumb>
275
+ <!-- 路由视图部分 -->
276
+ <a-layout-content :style="{ margin: '120px 8px 8px 8px' }">
277
+ <router-view v-slot="{ Component, route }">
278
+ <keep-alive v-if="route.meta.cache">
279
+ <component :is="Component" />
280
+ </keep-alive>
281
+ <component v-else :is="Component" />
282
+ </router-view>
283
+ </a-layout-content>
284
+ </a-layout>
285
+ <!-- 抽屉组件 -->
286
+ <a-drawer title="配置" :closable="false" :open="open" @close="onClose">
287
+ <a-form name="config_form" :model="formConfig">
288
+ <div class="blockquote">主题</div>
289
+ <div class="padding-10">
290
+ <a-radio-group v-model:value="formConfig.themeClass" @change="themeClasschangeTheme">
291
+ <a-radio value="dark">黑</a-radio>
292
+ <a-radio value="light">白</a-radio>
293
+ </a-radio-group>
294
+ </div>
295
+ <div class="blockquote">导航模式</div>
296
+ <div class="padding-10">
297
+ <a-radio-group v-model:value="formConfig.navigationMode" @change="navigationModechangeTheme">
298
+ <a-radio value="side">侧边菜单布局</a-radio>
299
+ <a-radio value="top">顶部菜单布局</a-radio>
300
+ </a-radio-group>
301
+ </div>
302
+ <div class="blockquote">表格设置</div>
303
+ <div class="padding-10">
304
+ <a-row>
305
+ <a-col :span="12">表格边框</a-col>
306
+ <a-col :span="12">
307
+ <a-switch v-model:checked="formConfig.tableBordered" checked-children="边框开" un-checked-children="边框关" @change="tableBorderedchangeTheme" />
308
+ </a-col>
309
+ </a-row>
310
+ </div>
311
+ </a-form>
312
+ </a-drawer>
313
+ </div>
314
+ </template>
315
+ <script setup>
316
+ import { ref, reactive, h, provide, getCurrentInstance, watch, onMounted } from 'vue'
317
+ import { menuStore } from '@/stores/menu'
318
+ import { configStore } from '@/stores/config'
319
+ import { useUserStore } from '@/stores/user'
320
+ import router from '@/router'
321
+ import zhCN from 'ant-design-vue/es/locale/zh_CN'
322
+ import enUS from 'ant-design-vue/es/locale/en_US'
323
+ const user = useUserStore()
324
+ const username = user.userInfo.name
325
+ const mobile = user.userInfo.mobile
326
+ // 菜单状态
327
+ const menu = menuStore()
328
+ const config = configStore()
329
+ const collapsed = ref(false)
330
+ const openmenuKey = ref(menu.openmenuKey)
331
+ const selectmenuKey = ref(menu.selectmenuKey)
332
+ const panes = ref(menu.tagmenus)
333
+ // 定义一个布尔状态来跟踪是否处于全屏
334
+ const isFullScreen = ref(false)
335
+ const formConfig = reactive({
336
+ themeClass: config.themeClass,
337
+ tableBordered: config.tableBordered,
338
+ navigationMode: config.navigationMode,
339
+ })
340
+ const menuItems = ref([])
341
+ const { proxy } = getCurrentInstance() // 获取当前实例
342
+ // 菜单项生成
343
+ const menuinit = () => {
344
+ const icons = proxy.$icons // 访问全局图标
345
+ const createMenuItem = menu => {
346
+ return menu.map(item => {
347
+ // 动态解析图标
348
+ const iconComponent = item.icon ? icons[item.icon] : null
349
+ const menuItem = {
350
+ key: item.id,
351
+ parentId: item.parentId,
352
+ icon: iconComponent ? () => h(iconComponent) : undefined, // 动态渲染图标
353
+ label: item.title,
354
+ title: item.title,
355
+ path: item.path,
356
+ type: item.type,
357
+ url: item.url,
358
+ }
359
+ if (item.children && item.children.length > 0) {
360
+ menuItem.children = createMenuItem(item.children)
361
+ }
362
+ return menuItem
363
+ })
364
+ }
365
+ menuItems.value = createMenuItem(menu.menus)
366
+ }
367
+ menuinit()
368
+ // 菜单点击事件
369
+ const menuclick = ({ item, key }) => {
370
+ const type = item.originItemValue.type
371
+ const url = item.url
372
+
373
+ if (type === 4 && url) {
374
+ // 外链:新窗口打开,不生成 tab
375
+ window.open(url, '_blank')
376
+ return
377
+ }
378
+ let current = panes.value.find(x => x.key === key)
379
+ if (!current) {
380
+ current = {
381
+ key,
382
+ parentId: item.parentId,
383
+ title: item.title,
384
+ path: item.path,
385
+ closable: true,
386
+ type,
387
+ url,
388
+ }
389
+ panes.value.push(current)
390
+ }
391
+ updateMenuPanes(current)
392
+ }
393
+ // 点击 Tag 切换选项卡
394
+ const tagchange = key => {
395
+ const current = panes.value.find(x => x.key === key)
396
+ if (current) {
397
+ updateMenuPanes(current)
398
+ }
399
+ }
400
+
401
+ // 删除 Tag
402
+ const tagdelete = (key, action) => {
403
+ if (action === 'add') {
404
+ //add();
405
+ } else {
406
+ if (key == selectmenuKey.value[0]) {
407
+ const index = panes.value.findIndex(item => item.key === key)
408
+ let nextPane = null
409
+ //如果有下一条
410
+ if (index + 1 < panes.value.length) {
411
+ // 有下一个
412
+ nextPane = panes.value[index + 1]
413
+ } else if (index - 1 >= 0) {
414
+ // 有上一个
415
+ nextPane = panes.value[index - 1]
416
+ }
417
+ if (nextPane) {
418
+ updateMenuPanes(nextPane)
419
+ }
420
+ }
421
+ panes.value = panes.value.filter(x => x.key !== key)
422
+ menu.tagmenus = panes.value
423
+ }
424
+ }
425
+ // 下拉菜单点击事件
426
+ const dropdownonClick = ({ key }) => {
427
+ const index = panes.value.findIndex(item => item.key === selectmenuKey.value[0])
428
+ switch (key) {
429
+ case '1':
430
+ panes.value = panes.value.filter((item, i) => i >= index || item.key === 0)
431
+ menu.tagmenus = panes.value
432
+ break
433
+ case '2':
434
+ panes.value = panes.value.filter((item, i) => i <= index)
435
+ menu.tagmenus = panes.value
436
+ break
437
+ case '3':
438
+ panes.value = panes.value.filter(x => x.key === selectmenuKey.value[0] || x.key === 0)
439
+ menu.tagmenus = panes.value
440
+ break
441
+ case '4':
442
+ panes.value = panes.value.filter(x => x.key === 0)
443
+ menu.tagmenus = panes.value
444
+ // 切换到控制台
445
+ const consolePane = {
446
+ key: 'console',
447
+ parentId: 0,
448
+ title: '控制台',
449
+ path: '/console',
450
+ closable: false,
451
+ type: 1, // 普通路由
452
+ url: '',
453
+ }
454
+ updateMenuPanes(consolePane)
455
+ break
456
+ default:
457
+ break
458
+ }
459
+ }
460
+ const findAllByHref = (items, targetHref) => {
461
+ let result = []
462
+ items.forEach(item => {
463
+ if (item.path === targetHref) {
464
+ result.push(item)
465
+ }
466
+ if (item.children && item.children.length > 0) {
467
+ result = result.concat(findAllByHref(item.children, targetHref))
468
+ }
469
+ })
470
+ return result
471
+ }
472
+ //个人中心和退出
473
+ const avatardropdownonClick = ({ key }) => {
474
+ switch (key) {
475
+ case '1':
476
+ const result = findAllByHref(menuItems.value, '/center')
477
+ let pane
478
+ if (result.length === 0) {
479
+ // 如果菜单项中没有个人中心,创建一个默认的个人中心菜单项
480
+ pane = {
481
+ key: 'center',
482
+ parentId: '0',
483
+ title: '个人中心',
484
+ path: '/center',
485
+ closable: true,
486
+ type: 2, // 普通菜单
487
+ url: '',
488
+ }
489
+ } else {
490
+ pane = {
491
+ key: result[0].key,
492
+ parentId: result[0].parentId,
493
+ title: result[0].title,
494
+ path: result[0].path,
495
+ closable: true,
496
+ type: result[0].type,
497
+ url: result[0].url,
498
+ }
499
+ }
500
+ const existingTab = panes.value.find(x => x.key === pane.key)
501
+ if (!existingTab) {
502
+ panes.value.push(pane)
503
+ }
504
+ updateMenuPanes(pane)
505
+ break
506
+ case '2':
507
+ user.$reset()
508
+ menu.$reset()
509
+ router.push('/login')
510
+ break
511
+ default:
512
+ break
513
+ }
514
+ }
515
+
516
+ function findParentKeys(menuArray, targetKey, parentKeys = []) {
517
+ for (const item of menuArray) {
518
+ // 如果找到目标项,返回当前收集的父级key
519
+ if (item.key === targetKey) {
520
+ return parentKeys
521
+ }
522
+
523
+ // 如果有子菜单,递归查找
524
+ if (item.children && item.children.length > 0) {
525
+ const result = findParentKeys(item.children, targetKey, [...parentKeys, item.key])
526
+ if (result) {
527
+ return result
528
+ }
529
+ }
530
+ }
531
+ return null
532
+ }
533
+ // 更新选中的选项卡和路由
534
+ const updateMenuPanes = pane => {
535
+ selectmenuKey.value = [pane.key]
536
+ if (pane.key != 0) {
537
+ const parentKeys = findParentKeys(menuItems.value, pane.key)
538
+ // 设置展开的菜单key,包括所有上级key
539
+ openmenuKey.value = parentKeys ? [...parentKeys] : []
540
+ } else {
541
+ openmenuKey.value = [pane.parentId]
542
+ }
543
+ menu.setSelectmenuKey(selectmenuKey.value)
544
+ menu.setOpenmenuKey(openmenuKey.value)
545
+
546
+ if (pane.type === 3 && pane.url) {
547
+ // 内链:用 iframe 打开
548
+ router.push({
549
+ path: '/iframePage',
550
+ query: { url: encodeURIComponent(pane.url) },
551
+ })
552
+ } else {
553
+ // 普通菜单:走路由
554
+ router.push(pane.path)
555
+ }
556
+ }
557
+ //国际化
558
+ const dropdownonLanguageClick = ({ key }) => {
559
+ if (key === 'zhCN') {
560
+ config.setLocale(zhCN)
561
+ } else {
562
+ config.setLocale(enUS)
563
+ }
564
+ }
565
+ // 抽屉状态
566
+ const open = ref(false)
567
+ const showDrawer = () => {
568
+ open.value = true
569
+ }
570
+ const onClose = () => {
571
+ open.value = false
572
+ }
573
+ // 主题切换
574
+ const themeClasschangeTheme = e => {
575
+ config.themeClass = e.target.value
576
+ }
577
+ //导航模式
578
+ const navigationModechangeTheme = e => {
579
+ config.navigationMode = e.target.value
580
+ }
581
+
582
+ // 定义全屏切换方法
583
+ const toggleFullScreen = () => {
584
+ const element = document.documentElement
585
+ if (!isFullScreen.value) {
586
+ // 进入全屏模式
587
+ if (element.requestFullscreen) {
588
+ element.requestFullscreen()
589
+ } else if (element.mozRequestFullScreen) {
590
+ // Firefox
591
+ element.mozRequestFullScreen()
592
+ } else if (element.webkitRequestFullscreen) {
593
+ // Chrome, Safari and Opera
594
+ element.webkitRequestFullscreen()
595
+ } else if (element.msRequestFullscreen) {
596
+ // IE/Edge
597
+ element.msRequestFullscreen()
598
+ }
599
+ } else {
600
+ // 退出全屏模式
601
+ if (document.exitFullscreen) {
602
+ document.exitFullscreen()
603
+ } else if (document.mozCancelFullScreen) {
604
+ // Firefox
605
+ document.mozCancelFullScreen()
606
+ } else if (document.webkitExitFullscreen) {
607
+ // Chrome, Safari and Opera
608
+ document.webkitExitFullscreen()
609
+ } else if (document.msExitFullscreen) {
610
+ // IE/Edge
611
+ document.msExitFullscreen()
612
+ }
613
+ }
614
+ isFullScreen.value = !isFullScreen.value // 切换全屏状态
615
+ }
616
+ //表格边框切换
617
+ const tableBorderedchangeTheme = checked => {
618
+ config.tableBordered = checked
619
+ }
620
+ const reload = () => {
621
+ window.location.reload()
622
+ }
623
+ const scrollContainer = ref(null)
624
+ const onWheelScroll = e => {
625
+ if (scrollContainer.value) {
626
+ scrollContainer.value.scrollLeft += e.deltaY // 将垂直滚动转为水平滚动
627
+ }
628
+ }
629
+ // 生成面包屑导航项
630
+ const breadcrumbItems = ref([])
631
+
632
+ /**
633
+ * 根据当前选中的菜单项生成面包屑导航
634
+ */
635
+ const generateBreadcrumbItems = () => {
636
+ breadcrumbItems.value = []
637
+
638
+ if (selectmenuKey.value.length === 0) return
639
+
640
+ const currentKey = selectmenuKey.value[0]
641
+ const findItemPath = (items, targetKey, path = []) => {
642
+ for (const item of items) {
643
+ if (item.key === targetKey) {
644
+ return [...path, item]
645
+ }
646
+ if (item.children && item.children.length > 0) {
647
+ const result = findItemPath(item.children, targetKey, [...path, item])
648
+ if (result) return result
649
+ }
650
+ }
651
+ return null
652
+ }
653
+
654
+ const itemPath = findItemPath(menuItems.value, currentKey)
655
+ if (itemPath) {
656
+ breadcrumbItems.value = itemPath.map(item => ({
657
+ key: item.key,
658
+ title: item.title,
659
+ path: item.path,
660
+ type: item.type,
661
+ url: item.url,
662
+ }))
663
+ }
664
+ }
665
+
666
+ // 监听选中菜单项变化,更新面包屑
667
+ watch(
668
+ selectmenuKey,
669
+ () => {
670
+ generateBreadcrumbItems()
671
+ },
672
+ { immediate: true },
673
+ )
674
+
675
+ // 组件挂载时恢复路由状态
676
+ onMounted(() => {
677
+ // 检查是否有持久化的选中菜单项
678
+ if (selectmenuKey.value && selectmenuKey.value.length > 0 && selectmenuKey.value[0] !== 0) {
679
+ const currentKey = selectmenuKey.value[0]
680
+
681
+ // 在tagmenus中查找对应的pane
682
+ const savedPane = panes.value.find(pane => pane.key === currentKey)
683
+
684
+ if (savedPane) {
685
+ // 如果找到了对应的pane,恢复路由状态
686
+ updateMenuPanes(savedPane)
687
+ } else {
688
+ // 如果没有找到对应的pane,检查当前路由是否与持久化状态匹配
689
+ const currentRoute = router.currentRoute.value
690
+ if (currentRoute.path === '/' || currentRoute.path === '/home') {
691
+ // 如果当前在首页,但持久化状态不是首页,尝试恢复
692
+ const menuItem = findMenuItemByKey(menuItems.value, currentKey)
693
+ if (menuItem) {
694
+ // 创建新的pane并跳转
695
+ const newPane = {
696
+ key: menuItem.key,
697
+ parentId: menuItem.parentId,
698
+ title: menuItem.title,
699
+ path: menuItem.path,
700
+ closable: true,
701
+ type: menuItem.type,
702
+ url: menuItem.url,
703
+ }
704
+ panes.value.push(newPane)
705
+ updateMenuPanes(newPane)
706
+ }
707
+ }
708
+ }
709
+ }
710
+ })
711
+
712
+ // 根据key在菜单项中查找对应项
713
+ function findMenuItemByKey(items, targetKey) {
714
+ for (const item of items) {
715
+ if (item.key === targetKey) {
716
+ return item
717
+ }
718
+ if (item.children && item.children.length > 0) {
719
+ const result = findMenuItemByKey(item.children, targetKey)
720
+ if (result) return result
721
+ }
722
+ }
723
+ return null
724
+ }
725
+
726
+ // 提供方法
727
+ provide('menuinit', menuinit)
728
+ provide('menuclick', menuclick)
729
+ </script>
730
+ <style scoped>
731
+ .trigger {
732
+ font-size: 18px;
733
+ line-height: 64px;
734
+ padding: 0 24px;
735
+ cursor: pointer;
736
+ transition: color 0.3s;
737
+ }
738
+ .trigger:hover {
739
+ color: #1890ff;
740
+ }
741
+ .logo {
742
+ text-align: center;
743
+ line-height: 32px;
744
+ height: 32px;
745
+ margin: 16px;
746
+ font-size: 16px;
747
+ }
748
+ .site-layout .site-layout-background {
749
+ background: #fff;
750
+ }
751
+ .zc-layout-header {
752
+ background: #fff !important;
753
+ padding: 0 !important;
754
+ border-bottom: 1px solid rgba(5, 5, 5, 0.06);
755
+ }
756
+ .zc-layout-header-top {
757
+ padding: 0 !important;
758
+ }
759
+ .zc-layout-content-tabs {
760
+ align-items: center;
761
+ position: fixed;
762
+ top: 64px;
763
+ left: 200px;
764
+ right: 0;
765
+ z-index: 100;
766
+ background: #fff;
767
+ border-bottom: 1px solid rgba(5, 5, 5, 0.06);
768
+ transition: left 0.3s ease;
769
+ }
770
+
771
+ .zc-layout-content-tabs.collapsed {
772
+ left: 80px;
773
+ }
774
+
775
+ .zc-layout-content-tabs.top-layout {
776
+ left: 0;
777
+ }
778
+ .zc-layout-content-tabs-left {
779
+ flex: 0.99;
780
+ width: 90%;
781
+ }
782
+ .zc-layout-content-tabs-right {
783
+ flex: 0.01;
784
+ text-align: right;
785
+ padding-right: 15px;
786
+ }
787
+
788
+ /* 使用深度选择器修改所有标签项的样式 */
789
+ .my-tabs-wrapper :deep(.ant-tabs-tab) {
790
+ border-radius: 4px !important; /* 圆角 */
791
+ color: #999999 !important;
792
+ background-color: #f5f5f5 !important;
793
+ border: 0px !important;
794
+ margin-right: 4px;
795
+ margin: 8px;
796
+ padding: 5px 10px !important;
797
+ }
798
+ /* 修改激活标签的样式 */
799
+ .my-tabs-wrapper :deep(.ant-tabs-tab-active .ant-tabs-tab-btn .ant-tabs-tab-remove) {
800
+ color: #1890ff !important;
801
+ }
802
+ .my-tabs-wrapper :deep(.ant-tabs-tab-active .ant-tabs-tab-remove) {
803
+ color: #1890ff !important;
804
+ }
805
+ /* 标签悬停效果 */
806
+ .my-tabs-wrapper :deep(.ant-tabs-tab:hover, .ant-tabs-tab-remove:hover) {
807
+ color: #1890ff !important;
808
+ }
809
+ .my-tabs-wrapper :deep(.ant-tabs-content-holder) {
810
+ display: none !important;
811
+ }
812
+ .my-tabs-wrapper :deep(.ant-tabs-nav) {
813
+ margin: 0px !important;
814
+ border-bottom: none !important; /* 移除底部分割线 */
815
+ }
816
+ .my-tabs-wrapper :deep(.ant-tabs-tab-remove) {
817
+ margin-left: 0px !important;
818
+ }
819
+
820
+ .breadcrumb {
821
+ display: inline-block;
822
+ margin-left: 16px;
823
+ }
824
+ .breadcrumb-top {
825
+ margin-top: 8px;
826
+ padding-left: 16px;
827
+ }
828
+
829
+ .username {
830
+ line-height: 1.2;
831
+ margin-bottom: 2px;
832
+ }
833
+
834
+ .userphone {
835
+ line-height: 1.2;
836
+ margin-bottom: 4px;
837
+ }
838
+ </style>