create-jnrs-template-vue 1.1.5 → 1.1.7
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/jnrs-template-vue/.env.development +4 -5
- package/jnrs-template-vue/.env.production +1 -1
- package/jnrs-template-vue/.prettierrc.json +1 -1
- package/jnrs-template-vue/auto-imports.d.ts +1 -0
- package/jnrs-template-vue/components.d.ts +14 -0
- package/jnrs-template-vue/package.json +1 -1
- package/jnrs-template-vue/public/system/menu.json +27 -8
- package/jnrs-template-vue/src/App.vue +2 -2
- package/jnrs-template-vue/src/api/request.ts +4 -1
- package/jnrs-template-vue/src/api/{base → system}/index.ts +48 -20
- package/jnrs-template-vue/src/directives/permissions.ts +28 -0
- package/jnrs-template-vue/src/layout/RouterTabs.vue +1 -1
- package/jnrs-template-vue/src/layout/SideMenu.vue +2 -4
- package/jnrs-template-vue/src/layout/SideMenuItem.vue +1 -1
- package/jnrs-template-vue/src/layout/TopHeader.vue +20 -28
- package/jnrs-template-vue/src/layout/index.vue +35 -5
- package/jnrs-template-vue/src/locales/index.ts +10 -3
- package/jnrs-template-vue/src/main.ts +3 -2
- package/jnrs-template-vue/src/router/index.ts +41 -6
- package/jnrs-template-vue/src/router/routes.ts +28 -28
- package/jnrs-template-vue/src/stores/mock.ts +4 -2
- package/jnrs-template-vue/src/utils/permissions.ts +16 -0
- package/jnrs-template-vue/src/views/common/403.vue +5 -9
- package/jnrs-template-vue/src/views/common/404.vue +5 -9
- package/jnrs-template-vue/src/views/home/index.vue +54 -20
- package/jnrs-template-vue/src/views/login/index.vue +36 -20
- package/jnrs-template-vue/src/views/system/dict/index.vue +174 -0
- package/jnrs-template-vue/src/views/system/menu/index.vue +65 -0
- package/jnrs-template-vue/src/views/system/mine/baseInfo.vue +38 -30
- package/jnrs-template-vue/src/views/system/mine/securitySettings.vue +12 -20
- package/jnrs-template-vue/src/views/system/role/editDialog.vue +94 -0
- package/jnrs-template-vue/src/views/system/role/index.vue +45 -4
- package/jnrs-template-vue/vite.config.ts +2 -1
- package/jnrs-template-vue/viteMockServe/dictItemRes.json +27 -0
- package/jnrs-template-vue/viteMockServe/dictRes.json +141 -0
- package/jnrs-template-vue/viteMockServe/fail.ts +26 -0
- package/jnrs-template-vue/viteMockServe/index.ts +36 -27
- package/jnrs-template-vue/viteMockServe/{login.json → loginRes_admin.json} +4 -3
- package/jnrs-template-vue/viteMockServe/loginRes_user.json +713 -0
- package/jnrs-template-vue/viteMockServe/roleRes.json +37 -0
- package/jnrs-template-vue/viteMockServe/success.ts +31 -0
- package/package.json +1 -1
- package/jnrs-template-vue/.env.example +0 -14
- package/jnrs-template-vue/src/types/index.ts +0 -2
- package/jnrs-template-vue/src/views/system/user/index.vue +0 -9
|
@@ -3,11 +3,10 @@ ENV = 'development'
|
|
|
3
3
|
# 是否是 mock 模式
|
|
4
4
|
VITE_USE_MOCK = true
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
VITE_BASE_URL = '192.168.1.120:5001'
|
|
6
|
+
# 后端接口基地址 - 开发环境
|
|
7
|
+
VITE_BASE_URL = '192.168.1.120:6001'
|
|
9
8
|
|
|
10
|
-
#
|
|
9
|
+
# 应用运行主机(置空默认为 localhost)
|
|
11
10
|
VITE_APP_HOST = ''
|
|
12
11
|
# 应用运行端口
|
|
13
|
-
VITE_APP_PORT =
|
|
12
|
+
VITE_APP_PORT = 5788
|
|
@@ -17,21 +17,35 @@ declare module 'vue' {
|
|
|
17
17
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
|
18
18
|
ElCard: typeof import('element-plus/es')['ElCard']
|
|
19
19
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
|
20
|
+
ElCol: typeof import('element-plus/es')['ElCol']
|
|
21
|
+
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
|
20
22
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
|
21
23
|
ElDatePickerPanel: typeof import('element-plus/es')['ElDatePickerPanel']
|
|
24
|
+
ElDialog: typeof import('element-plus/es')['ElDialog']
|
|
22
25
|
ElForm: typeof import('element-plus/es')['ElForm']
|
|
23
26
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
|
24
27
|
ElHeader: typeof import('element-plus/es')['ElHeader']
|
|
25
28
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
|
26
29
|
ElInput: typeof import('element-plus/es')['ElInput']
|
|
30
|
+
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
|
27
31
|
ElMain: typeof import('element-plus/es')['ElMain']
|
|
28
32
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
|
29
33
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
34
|
+
ElOption: typeof import('element-plus/es')['ElOption']
|
|
30
35
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
|
36
|
+
ElRadio: typeof import('element-plus/es')['ElRadio']
|
|
37
|
+
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
|
38
|
+
ElRow: typeof import('element-plus/es')['ElRow']
|
|
39
|
+
ElSelect: typeof import('element-plus/es')['ElSelect']
|
|
31
40
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
|
41
|
+
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
|
42
|
+
ElTable: typeof import('element-plus/es')['ElTable']
|
|
43
|
+
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
|
32
44
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
33
45
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
46
|
+
ElTag: typeof import('element-plus/es')['ElTag']
|
|
34
47
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
|
48
|
+
ElWatermark: typeof import('element-plus/es')['ElWatermark']
|
|
35
49
|
}
|
|
36
50
|
export interface GlobalDirectives {
|
|
37
51
|
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"name": "SystemMine",
|
|
36
36
|
"meta": {
|
|
37
37
|
"title": "个人中心",
|
|
38
|
-
"
|
|
39
|
-
"
|
|
38
|
+
"todoCount": 99,
|
|
39
|
+
"permissions": ["mine:view", "mine:edit"]
|
|
40
40
|
},
|
|
41
41
|
"component": "/system/mine/index"
|
|
42
42
|
},
|
|
@@ -45,19 +45,38 @@
|
|
|
45
45
|
"name": "SystemUser",
|
|
46
46
|
"meta": {
|
|
47
47
|
"title": "用户管理",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
48
|
+
"todoCount": 0,
|
|
49
|
+
"permissions": ["user:view", "user:edit"]
|
|
50
50
|
},
|
|
51
|
-
"component": "/system/user/index"
|
|
51
|
+
"component": "/system/user/index",
|
|
52
|
+
"redirect": "/crud"
|
|
52
53
|
},
|
|
53
54
|
{
|
|
54
55
|
"path": "/system/role",
|
|
55
56
|
"name": "SystemRole",
|
|
56
57
|
"meta": {
|
|
57
|
-
"title": "
|
|
58
|
+
"title": "角色管理",
|
|
59
|
+
"permissions": ["role:view", "role:edit"]
|
|
58
60
|
},
|
|
59
|
-
"component": "/system/role/index"
|
|
60
|
-
|
|
61
|
+
"component": "/system/role/index"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"path": "/system/dict",
|
|
65
|
+
"name": "SystemDict",
|
|
66
|
+
"meta": {
|
|
67
|
+
"title": "字典管理",
|
|
68
|
+
"permissions": ["dict:view", "dict:edit"]
|
|
69
|
+
},
|
|
70
|
+
"component": "/system/dict/index"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"path": "/system/menu",
|
|
74
|
+
"name": "SystemMenu",
|
|
75
|
+
"meta": {
|
|
76
|
+
"title": "菜单管理",
|
|
77
|
+
"permissions": ["menu:view", "menu:edit"]
|
|
78
|
+
},
|
|
79
|
+
"component": "/system/menu/index"
|
|
61
80
|
}
|
|
62
81
|
]
|
|
63
82
|
}
|
|
@@ -5,8 +5,8 @@ import { ElConfigProvider } from 'element-plus'
|
|
|
5
5
|
import zhCn from 'element-plus/es/locale/lang/zh-CN'
|
|
6
6
|
import en from 'element-plus/es/locale/lang/en'
|
|
7
7
|
import { useI18n } from 'vue-i18n'
|
|
8
|
-
import { changeLocales as changeLocalesForShared } from '@jnrs/shared'
|
|
9
|
-
import { changeLocales as changeLocalesForCore } from '@jnrs/core'
|
|
8
|
+
import { changeLocales as changeLocalesForShared } from '@jnrs/shared/locales'
|
|
9
|
+
import { changeLocales as changeLocalesForCore } from '@jnrs/core/locales'
|
|
10
10
|
|
|
11
11
|
const { locale } = useI18n()
|
|
12
12
|
const { theme } = useSystemStore()
|
|
@@ -10,6 +10,7 @@ import { ElMessage } from 'element-plus'
|
|
|
10
10
|
import { createAxiosInstance } from '@jnrs/core/axios'
|
|
11
11
|
import type { BusinessRequest, BusinessResponse } from '@jnrs/core/axios'
|
|
12
12
|
import { useMockStore, useAuthStore } from '@/stores'
|
|
13
|
+
import { handleRouter } from '@jnrs/vue-core/router'
|
|
13
14
|
|
|
14
15
|
const axiosInstance = createAxiosInstance({
|
|
15
16
|
handleMessageFn: ElMessage,
|
|
@@ -17,7 +18,9 @@ const axiosInstance = createAxiosInstance({
|
|
|
17
18
|
const { token } = useAuthStore()
|
|
18
19
|
return token
|
|
19
20
|
},
|
|
20
|
-
handleNoAuthFn: () => {
|
|
21
|
+
handleNoAuthFn: () => {
|
|
22
|
+
handleRouter({ name: 'Login' })
|
|
23
|
+
}
|
|
21
24
|
})
|
|
22
25
|
|
|
23
26
|
/**
|
|
@@ -1,35 +1,25 @@
|
|
|
1
1
|
import { request } from '../request'
|
|
2
|
-
import type { Dict } from '
|
|
3
|
-
import type { User } from '@/types'
|
|
2
|
+
import type { Dict, DictItem, User, Role } from '@jnrs/shared'
|
|
4
3
|
import type { MenuItem } from '@jnrs/vue-core'
|
|
5
4
|
|
|
6
|
-
// 登录结果
|
|
7
|
-
export interface LoginResult extends User {
|
|
8
|
-
token: string
|
|
9
|
-
dict: Dict
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// 修改密码
|
|
13
|
-
interface PasswordChange {
|
|
14
|
-
userId: number
|
|
15
|
-
password: string
|
|
16
|
-
}
|
|
17
|
-
|
|
18
5
|
// 菜单
|
|
19
|
-
export const MenuApi = () => {
|
|
20
|
-
return request
|
|
6
|
+
export const MenuApi = (): Promise<MenuItem[]> => {
|
|
7
|
+
return request({
|
|
21
8
|
url: '/system/menu.json', // /public 文件夹下
|
|
22
9
|
mockUrl: '/mock/menu',
|
|
23
10
|
method: 'get'
|
|
24
11
|
})
|
|
25
12
|
}
|
|
26
13
|
|
|
14
|
+
// 登录
|
|
27
15
|
interface LoginParams {
|
|
28
16
|
account: string
|
|
29
17
|
password: string
|
|
30
18
|
}
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
export interface LoginResult extends User {
|
|
20
|
+
token: string
|
|
21
|
+
dict: Dict
|
|
22
|
+
}
|
|
33
23
|
export const LoginApi = (data: LoginParams): Promise<LoginResult> => {
|
|
34
24
|
return request({
|
|
35
25
|
url: '/api/auth/login',
|
|
@@ -48,14 +38,18 @@ export const LogoutApi = () => {
|
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
// 获取用户信息
|
|
51
|
-
export const UserInfoApi = () => {
|
|
41
|
+
export const UserInfoApi = (): Promise<User> => {
|
|
52
42
|
return request({
|
|
53
43
|
url: '/api/auth/user-info',
|
|
54
44
|
method: 'get'
|
|
55
45
|
})
|
|
56
46
|
}
|
|
57
47
|
|
|
58
|
-
|
|
48
|
+
// 修改密码
|
|
49
|
+
interface PasswordChange {
|
|
50
|
+
userId: number
|
|
51
|
+
password: string
|
|
52
|
+
}
|
|
59
53
|
export const PasswordChangeApi = (data: PasswordChange) => {
|
|
60
54
|
return request({
|
|
61
55
|
url: '/api/auth/change-password',
|
|
@@ -63,3 +57,37 @@ export const PasswordChangeApi = (data: PasswordChange) => {
|
|
|
63
57
|
data
|
|
64
58
|
})
|
|
65
59
|
}
|
|
60
|
+
|
|
61
|
+
// 获取字典列表
|
|
62
|
+
export const DictApi = (): Promise<DictItem[]> => {
|
|
63
|
+
return request({
|
|
64
|
+
url: '/api/dict-manager',
|
|
65
|
+
method: 'get'
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 获取单个字典
|
|
70
|
+
export const DictDetailApi = (id: string) => {
|
|
71
|
+
return request({
|
|
72
|
+
url: `/api/dict-manager/detail/${id}`,
|
|
73
|
+
method: 'get'
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 修改单个字典
|
|
78
|
+
export const DictChangeApi = (data: DictItem) => {
|
|
79
|
+
return request({
|
|
80
|
+
url: '/api/dict-manager/detail',
|
|
81
|
+
method: 'post',
|
|
82
|
+
data
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 获取角色列表
|
|
87
|
+
export const RoleApi = (): Promise<Role[]> => {
|
|
88
|
+
return request({
|
|
89
|
+
url: '/api/role-manager',
|
|
90
|
+
method: 'get',
|
|
91
|
+
noAuth: true
|
|
92
|
+
})
|
|
93
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Directive } from 'vue'
|
|
2
|
+
import { hasPermission } from '@/utils/permissions'
|
|
3
|
+
|
|
4
|
+
export type PermissionsDirective = Directive<HTMLElement, string[]>
|
|
5
|
+
|
|
6
|
+
declare module 'vue' {
|
|
7
|
+
export interface ComponentCustomProperties {
|
|
8
|
+
vPermissions: PermissionsDirective
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const permissions = {
|
|
13
|
+
mounted(el, binding) {
|
|
14
|
+
const { value, modifiers } = binding
|
|
15
|
+
const hasPerm = hasPermission(value)
|
|
16
|
+
|
|
17
|
+
if (!hasPerm) {
|
|
18
|
+
if (modifiers.display) {
|
|
19
|
+
el.style.display = 'none'
|
|
20
|
+
} else if (modifiers.disabled) {
|
|
21
|
+
el.setAttribute('disabled', 'disabled')
|
|
22
|
+
el.classList.add('disabled')
|
|
23
|
+
} else {
|
|
24
|
+
el.remove()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} satisfies PermissionsDirective
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, watch, computed, onMounted } from 'vue'
|
|
3
3
|
import type { TabsPaneContext } from 'element-plus'
|
|
4
|
-
import type { MenuItem } from '
|
|
4
|
+
import type { MenuItem } from '@jnrs/vue-core'
|
|
5
5
|
import { handleRouter, getRoutes, useRoute } from '@jnrs/vue-core/router'
|
|
6
6
|
|
|
7
7
|
// 初始化时添加当前路由
|
|
@@ -3,9 +3,7 @@ import SideMenuItem from './SideMenuItem.vue'
|
|
|
3
3
|
import { storeToRefs } from 'pinia'
|
|
4
4
|
import { useRoute } from '@jnrs/vue-core/router'
|
|
5
5
|
import { useSystemStore, useMenuStore } from '@/stores'
|
|
6
|
-
import { useI18n } from 'vue-i18n'
|
|
7
6
|
|
|
8
|
-
const { t } = useI18n()
|
|
9
7
|
const systemStore = useSystemStore()
|
|
10
8
|
const { menuCollapse } = storeToRefs(systemStore)
|
|
11
9
|
const { toggleCollapse } = systemStore
|
|
@@ -18,7 +16,7 @@ const route = useRoute()
|
|
|
18
16
|
<el-aside class="sideMenu">
|
|
19
17
|
<div class="logo" :class="{ logo_collapse: menuCollapse }">
|
|
20
18
|
<img class="logo_img" src="@/assets/images/common/jnrs-white.svg" alt="jnrs" />
|
|
21
|
-
<span class="logo_text">{{ t('main.title') }}</span>
|
|
19
|
+
<span class="logo_text">{{ $t('main.title') }}</span>
|
|
22
20
|
</div>
|
|
23
21
|
<el-icon
|
|
24
22
|
class="collapseBtn"
|
|
@@ -136,7 +134,7 @@ $mainFontColor: rgba(255, 255, 255, 0.8);
|
|
|
136
134
|
|
|
137
135
|
.leftSide_menu {
|
|
138
136
|
border: none;
|
|
139
|
-
min-width:
|
|
137
|
+
min-width: 200px;
|
|
140
138
|
--el-menu-text-color: $mainFontColor;
|
|
141
139
|
--el-menu-active-color: $mainFontColor;
|
|
142
140
|
--el-menu-bg-color: none;
|
|
@@ -4,16 +4,17 @@ import { ref, computed } from 'vue'
|
|
|
4
4
|
import { storeToRefs } from 'pinia'
|
|
5
5
|
import { ElMessageBox } from 'element-plus'
|
|
6
6
|
import { handleRouter } from '@jnrs/vue-core/router'
|
|
7
|
-
import { useSystemStore, useAuthStore } from '@/stores'
|
|
7
|
+
import { useSystemStore, useAuthStore, useMenuStore } from '@/stores'
|
|
8
8
|
import { useAvatar } from '@/composables/common/useAvatar'
|
|
9
|
-
import { LogoutApi } from '@/api/
|
|
9
|
+
import { LogoutApi } from '@/api/system'
|
|
10
10
|
import { getDictLabel, getDictColor } from '@/utils/common'
|
|
11
11
|
|
|
12
12
|
const { avatar } = useAvatar()
|
|
13
|
-
const { userInfo,
|
|
13
|
+
const { userInfo, clearAuth } = useAuthStore()
|
|
14
|
+
const { clearMenu } = useMenuStore()
|
|
14
15
|
|
|
15
|
-
const roleLabel = computed(() => getDictLabel('role', userInfo
|
|
16
|
-
const roleColor = computed(() => getDictColor('role', userInfo
|
|
16
|
+
const roleLabel = computed(() => getDictLabel('role', userInfo?.role || ''))
|
|
17
|
+
const roleColor = computed(() => getDictColor('role', userInfo?.role || ''))
|
|
17
18
|
|
|
18
19
|
const systemStore = useSystemStore()
|
|
19
20
|
const { documentFullscreen } = storeToRefs(systemStore)
|
|
@@ -28,7 +29,8 @@ const handleLogout = async () => {
|
|
|
28
29
|
type: 'warning'
|
|
29
30
|
})
|
|
30
31
|
await LogoutApi()
|
|
31
|
-
await
|
|
32
|
+
await clearAuth()
|
|
33
|
+
await clearMenu()
|
|
32
34
|
handleRouter({ name: 'Login' }, 'replace')
|
|
33
35
|
} catch {}
|
|
34
36
|
}
|
|
@@ -61,34 +63,24 @@ const showGlobalSetting = () => {
|
|
|
61
63
|
<component :is="!documentFullscreen ? 'FullScreen' : 'Rank'" />
|
|
62
64
|
</el-icon>
|
|
63
65
|
<!-- 头像和用户名 -->
|
|
64
|
-
<el-popover
|
|
65
|
-
placement="bottom"
|
|
66
|
-
trigger="click"
|
|
67
|
-
:teleported="false"
|
|
68
|
-
:width="260"
|
|
69
|
-
:hide-after="0"
|
|
70
|
-
>
|
|
66
|
+
<el-popover placement="bottom" trigger="click" :teleported="false" :width="260" :hide-after="0">
|
|
71
67
|
<template #reference>
|
|
72
68
|
<span class="userMenu_reference">
|
|
73
69
|
<img class="userMenu_avatar" :src="avatar" alt="avatar" />
|
|
74
|
-
<span>{{ userInfo
|
|
75
|
-
<span class="userMenu_roleName" :style="{ color: roleColor }" v-if="userInfo
|
|
76
|
-
[{{ roleLabel }}]
|
|
77
|
-
</span>
|
|
70
|
+
<span>{{ userInfo?.name }}</span>
|
|
71
|
+
<span class="userMenu_roleName" :style="{ color: roleColor }" v-if="userInfo?.role">[{{ roleLabel }}]</span>
|
|
78
72
|
<el-icon class="userMenu_icon"><arrow-down /></el-icon>
|
|
79
73
|
</span>
|
|
80
74
|
</template>
|
|
81
75
|
<div class="userMenu_dropdown">
|
|
82
76
|
<img class="userMenu_dropdown_avatar" :src="avatar" alt="avatar" />
|
|
83
77
|
<b>
|
|
84
|
-
{{ userInfo
|
|
85
|
-
<span class="userMenu_roleName" :style="{ color: roleColor }" v-if="userInfo
|
|
86
|
-
[{{ roleLabel }}]
|
|
87
|
-
</span>
|
|
78
|
+
<span>{{ userInfo?.name }}</span>
|
|
79
|
+
<span class="userMenu_roleName" :style="{ color: roleColor }" v-if="userInfo?.role">[{{ roleLabel }}]</span>
|
|
88
80
|
</b>
|
|
89
|
-
<div class="loginDateTime">
|
|
81
|
+
<div class="loginDateTime" v-if="userInfo?.loginDateTime">
|
|
90
82
|
<span>登录时间</span>
|
|
91
|
-
<p>{{ userInfo
|
|
83
|
+
<p>{{ userInfo?.loginDateTime }}</p>
|
|
92
84
|
</div>
|
|
93
85
|
<div class="userMenu_dropdown_btn">
|
|
94
86
|
<el-button
|
|
@@ -102,9 +94,7 @@ const showGlobalSetting = () => {
|
|
|
102
94
|
>
|
|
103
95
|
个人中心
|
|
104
96
|
</el-button>
|
|
105
|
-
<el-button type="danger" icon="SwitchButton" @click="handleLogout()">
|
|
106
|
-
退出系统
|
|
107
|
-
</el-button>
|
|
97
|
+
<el-button type="danger" icon="SwitchButton" @click="handleLogout()">退出系统</el-button>
|
|
108
98
|
</div>
|
|
109
99
|
</div>
|
|
110
100
|
</el-popover>
|
|
@@ -164,7 +154,9 @@ $topHoverSize: 35px;
|
|
|
164
154
|
transition: all 0.25s ease;
|
|
165
155
|
cursor: pointer;
|
|
166
156
|
&:hover {
|
|
167
|
-
|
|
157
|
+
span {
|
|
158
|
+
color: var(--jnrs-color-primary) !important;
|
|
159
|
+
}
|
|
168
160
|
.userMenu_icon {
|
|
169
161
|
color: var(--jnrs-color-primary);
|
|
170
162
|
}
|
|
@@ -200,7 +192,7 @@ $topHoverSize: 35px;
|
|
|
200
192
|
}
|
|
201
193
|
}
|
|
202
194
|
.userMenu_roleName {
|
|
203
|
-
|
|
195
|
+
margin-left: 4px;
|
|
204
196
|
color: var(--jnrs-color-primary);
|
|
205
197
|
}
|
|
206
198
|
}
|
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, toRefs } from 'vue'
|
|
2
3
|
import SideMenu from './SideMenu.vue'
|
|
3
4
|
import TopHeader from './TopHeader.vue'
|
|
4
5
|
import RouterTabs from './RouterTabs.vue'
|
|
6
|
+
import { useAuthStore, useSystemStore } from '@/stores'
|
|
7
|
+
|
|
8
|
+
const watermarkFont = ref({
|
|
9
|
+
color: 'rgba(0, 0, 0, 0)',
|
|
10
|
+
fontSize: 12
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const { userInfo } = useAuthStore()
|
|
14
|
+
|
|
15
|
+
const { computedThemeMode } = toRefs(useSystemStore())
|
|
16
|
+
watch(
|
|
17
|
+
computedThemeMode,
|
|
18
|
+
(newVal) => {
|
|
19
|
+
if (newVal === 'dark') {
|
|
20
|
+
watermarkFont.value.color = 'rgba(255, 255, 255, .06)'
|
|
21
|
+
} else {
|
|
22
|
+
watermarkFont.value.color = 'rgba(0, 0, 0, .06)'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
immediate: true
|
|
27
|
+
}
|
|
28
|
+
)
|
|
5
29
|
</script>
|
|
6
30
|
|
|
7
31
|
<template>
|
|
@@ -13,11 +37,17 @@ import RouterTabs from './RouterTabs.vue'
|
|
|
13
37
|
</el-header>
|
|
14
38
|
<RouterTabs />
|
|
15
39
|
<el-main>
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
40
|
+
<el-watermark
|
|
41
|
+
:font="watermarkFont"
|
|
42
|
+
:content="[userInfo?.name, userInfo?.workNo]"
|
|
43
|
+
style="width: 100%; height: 100%"
|
|
44
|
+
>
|
|
45
|
+
<router-view v-slot="{ Component }">
|
|
46
|
+
<keep-alive>
|
|
47
|
+
<component :is="Component" />
|
|
48
|
+
</keep-alive>
|
|
49
|
+
</router-view>
|
|
50
|
+
</el-watermark>
|
|
21
51
|
</el-main>
|
|
22
52
|
</el-container>
|
|
23
53
|
</el-container>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createI18n } from 'vue-i18n'
|
|
2
|
-
import
|
|
2
|
+
import zhCn from './zhCn'
|
|
3
3
|
import en from './en'
|
|
4
|
+
import { zhCn as vueCore_zhCn, en as vueCore_en } from '@jnrs/vue-core/locales'
|
|
4
5
|
|
|
5
6
|
const i18n = createI18n({
|
|
6
7
|
legacy: false, // 启用 Composition API 模式
|
|
@@ -8,8 +9,14 @@ const i18n = createI18n({
|
|
|
8
9
|
locale: 'zhCn',
|
|
9
10
|
fallbackLocale: 'en',
|
|
10
11
|
messages: {
|
|
11
|
-
zhCn:
|
|
12
|
-
|
|
12
|
+
zhCn: {
|
|
13
|
+
...zhCn,
|
|
14
|
+
...vueCore_zhCn
|
|
15
|
+
},
|
|
16
|
+
en: {
|
|
17
|
+
...en,
|
|
18
|
+
...vueCore_en
|
|
19
|
+
}
|
|
13
20
|
}
|
|
14
21
|
})
|
|
15
22
|
|
|
@@ -10,11 +10,12 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
|
|
10
10
|
import App from './App.vue'
|
|
11
11
|
import { router } from './router'
|
|
12
12
|
import locales from './locales/index'
|
|
13
|
-
import {
|
|
14
|
-
changeLocalesForShared('en')
|
|
13
|
+
import { permissions } from './directives/permissions'
|
|
15
14
|
|
|
16
15
|
const app = createApp(App)
|
|
17
16
|
|
|
17
|
+
app.directive('permissions', permissions)
|
|
18
|
+
|
|
18
19
|
// element-plus 自动引入所有图标
|
|
19
20
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|
20
21
|
app.component(key, component)
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { LAYOUT_NAME, GLOBAL_COMPONENT, routes } from './routes'
|
|
2
2
|
import { createVueRouter } from '@jnrs/vue-core/router'
|
|
3
3
|
import type { FileModules, RouteLocationNormalizedGeneric } from '@jnrs/vue-core/router'
|
|
4
|
+
import type { MenuItem } from '@jnrs/vue-core'
|
|
4
5
|
import { useAuthStore, useMenuStore } from '@/stores'
|
|
5
|
-
import { MenuApi } from '@/api/
|
|
6
|
+
import { MenuApi } from '@/api/system'
|
|
7
|
+
import { hasMenuViewPermission } from '@/utils/permissions'
|
|
6
8
|
|
|
7
9
|
const fileModules = import.meta.glob('/src/views/**/*.vue') as FileModules
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 递归过滤菜单树,仅保留当前用户有权限访问的节点
|
|
13
|
+
* TODO 如果是后端返回的数据就没必要判断了,后端会判断
|
|
14
|
+
*/
|
|
15
|
+
function filterMenuByPermission(menus: MenuItem[]): MenuItem[] {
|
|
16
|
+
return menus
|
|
17
|
+
.map((item) => {
|
|
18
|
+
const children = item.children ? filterMenuByPermission(item.children) : []
|
|
19
|
+
|
|
20
|
+
const hasSelfPermission = hasMenuViewPermission(item.meta?.permissions)
|
|
21
|
+
|
|
22
|
+
if (hasSelfPermission) {
|
|
23
|
+
return { ...item, ...(children.length > 0 ? { children } : {}) }
|
|
24
|
+
}
|
|
25
|
+
if (children.length > 0) {
|
|
26
|
+
return { ...item, children }
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
})
|
|
30
|
+
.filter(Boolean) as MenuItem[]
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
const router = createVueRouter({
|
|
10
34
|
options: {
|
|
11
35
|
routes
|
|
@@ -14,21 +38,32 @@ const router = createVueRouter({
|
|
|
14
38
|
layoutName: LAYOUT_NAME,
|
|
15
39
|
globalComponent: GLOBAL_COMPONENT,
|
|
16
40
|
handleBeforeEach: async (to: RouteLocationNormalizedGeneric) => {
|
|
17
|
-
//
|
|
41
|
+
// 如果是不需要[身份验证]的页面,直接放行
|
|
18
42
|
if (to.meta.noAuth) return true
|
|
19
43
|
|
|
20
|
-
//
|
|
44
|
+
// 页面刷新处理:未[身份验证]时,重定向到[身份验证]页
|
|
21
45
|
const { hasAuthenticated } = useAuthStore()
|
|
22
46
|
if (!hasAuthenticated) {
|
|
23
47
|
return { name: 'Login', query: { redirect: to.path }, replace: true }
|
|
24
48
|
}
|
|
25
49
|
|
|
26
|
-
//
|
|
50
|
+
// 校验访问权限
|
|
51
|
+
if (to.meta.permissions && !hasMenuViewPermission(to.meta.permissions)) {
|
|
52
|
+
return { name: '403' }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 页面刷新处理:菜单未获取时,先获取菜单,再重定向当前路由,即重新开始进入 handleBeforeEach
|
|
27
56
|
const { hasFetchedAsyncMenus, asyncSetMenus } = useMenuStore()
|
|
28
57
|
if (!hasFetchedAsyncMenus) {
|
|
29
58
|
try {
|
|
30
|
-
const
|
|
31
|
-
|
|
59
|
+
const menuRes = await MenuApi()
|
|
60
|
+
/**
|
|
61
|
+
* 校验用户的菜单访问权限,进行菜单筛选
|
|
62
|
+
* TODO 如果是后端返回的数据就没必要校验筛选了,后端会筛选
|
|
63
|
+
*/
|
|
64
|
+
const perRes = filterMenuByPermission(menuRes)
|
|
65
|
+
|
|
66
|
+
await asyncSetMenus(perRes)
|
|
32
67
|
return to.fullPath
|
|
33
68
|
} catch (error) {
|
|
34
69
|
throw error
|