xto-fronted 0.1.2 → 0.1.3
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/.env.development +3 -0
- package/.env.production +3 -0
- package/bin/cli.js +104 -0
- package/index.html +13 -0
- package/package.json +16 -3
- package/public/vite.svg +10 -0
- package/src/App.vue +20 -0
- package/src/api/auth.ts +35 -0
- package/src/api/menu.ts +13 -0
- package/src/api/system.ts +65 -0
- package/src/api/user.ts +12 -0
- package/src/assets/styles/_dark.scss +407 -0
- package/src/assets/styles/_reset.scss +126 -0
- package/src/assets/styles/_root.scss +140 -0
- package/src/assets/styles/_transition.scss +119 -0
- package/src/assets/styles/_variables.scss +45 -0
- package/src/assets/styles/index.scss +187 -0
- package/src/components/Layout/Footer.vue +17 -0
- package/src/components/Layout/Header.vue +335 -0
- package/src/components/Layout/Sidebar.vue +213 -0
- package/src/components/Layout/Tabs.vue +20 -0
- package/src/components/Layout/index.vue +62 -0
- package/src/composables/index.ts +9 -0
- package/src/composables/useApp.ts +170 -0
- package/src/composables/useAuth.ts +70 -0
- package/src/composables/useForm.ts +79 -0
- package/src/composables/useMenu.ts +141 -0
- package/src/composables/useTable.ts +97 -0
- package/src/config/index.ts +19 -0
- package/src/directives/permission.ts +41 -0
- package/src/enums/index.ts +63 -0
- package/src/env.d.ts +17 -0
- package/src/index.ts +44 -0
- package/src/main.ts +34 -0
- package/src/router/dynamicRoutes.ts +163 -0
- package/src/router/index.ts +71 -0
- package/src/router/staticRoutes.ts +43 -0
- package/src/stores/app.ts +145 -0
- package/src/stores/auth.ts +45 -0
- package/src/stores/index.ts +15 -0
- package/src/stores/menu.ts +158 -0
- package/src/stores/user.ts +41 -0
- package/src/types/api.d.ts +103 -0
- package/src/types/global.d.ts +45 -0
- package/src/types/router.d.ts +48 -0
- package/src/types/xto.d.ts +149 -0
- package/src/utils/auth.ts +86 -0
- package/src/utils/permission.ts +30 -0
- package/src/utils/request.ts +126 -0
- package/src/utils/storage.ts +72 -0
- package/src/views/dashboard/index.vue +32 -0
- package/src/views/error/403.vue +57 -0
- package/src/views/error/404.vue +57 -0
- package/src/views/login/index.vue +141 -0
- package/src/views/system/menu/index.vue +32 -0
- package/src/views/system/role/index.vue +32 -0
- package/src/views/system/user/index.vue +32 -0
- package/tsconfig.json +26 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +139 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 侧边栏组件
|
|
4
|
+
* 使用统一的 useMenu 和 useApp hooks
|
|
5
|
+
*/
|
|
6
|
+
import { useMenu } from '@/composables'
|
|
7
|
+
import { useApp } from '@/composables'
|
|
8
|
+
import { Menu, MenuItem, SubMenu } from '@xto/navigation'
|
|
9
|
+
import { Button } from '@xto/base'
|
|
10
|
+
import { Input } from '@xto/form'
|
|
11
|
+
import config from '@/config'
|
|
12
|
+
|
|
13
|
+
// 使用统一的 hooks
|
|
14
|
+
const { logout, userName, userInfo } = useApp()
|
|
15
|
+
const {
|
|
16
|
+
searchKeyword,
|
|
17
|
+
activeMenu,
|
|
18
|
+
isCollapsed,
|
|
19
|
+
searchResults,
|
|
20
|
+
filteredMenuList,
|
|
21
|
+
getMenuIcon,
|
|
22
|
+
handleMenuSelect,
|
|
23
|
+
handleSearchItemClick
|
|
24
|
+
} = useMenu()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div class="sidebar">
|
|
29
|
+
<!-- Logo -->
|
|
30
|
+
<div class="sidebar__logo">
|
|
31
|
+
<img src="/vite.svg" alt="Logo" class="sidebar__logo-img" />
|
|
32
|
+
<span v-show="!isCollapsed" class="sidebar__logo-text">{{ config.webTitle }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- 搜索框 -->
|
|
36
|
+
<div v-if="!isCollapsed" class="sidebar__search">
|
|
37
|
+
<Input
|
|
38
|
+
v-model="searchKeyword"
|
|
39
|
+
placeholder="搜索菜单..."
|
|
40
|
+
size="small"
|
|
41
|
+
clearable
|
|
42
|
+
/>
|
|
43
|
+
<!-- 搜索结果 -->
|
|
44
|
+
<div v-if="searchResults.length > 0" class="sidebar__search-results">
|
|
45
|
+
<div
|
|
46
|
+
v-for="item in searchResults"
|
|
47
|
+
:key="item.path"
|
|
48
|
+
class="sidebar__search-item"
|
|
49
|
+
@click="handleSearchItemClick(item.path)"
|
|
50
|
+
>
|
|
51
|
+
<span class="menu-icon">{{ getMenuIcon(item.icon) }}</span>
|
|
52
|
+
<div class="sidebar__search-item-info">
|
|
53
|
+
<span class="sidebar__search-item-title">{{ item.title }}</span>
|
|
54
|
+
<span v-if="item.parentTitle" class="sidebar__search-item-parent">{{ item.parentTitle }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<!-- 菜单 -->
|
|
61
|
+
<Menu
|
|
62
|
+
:default-active="activeMenu"
|
|
63
|
+
:collapse="isCollapsed"
|
|
64
|
+
:collapse-transition="false"
|
|
65
|
+
class="sidebar__menu"
|
|
66
|
+
@select="handleMenuSelect"
|
|
67
|
+
>
|
|
68
|
+
<template v-for="menu in filteredMenuList" :key="menu.path">
|
|
69
|
+
<!-- 有子菜单 -->
|
|
70
|
+
<SubMenu v-if="menu.children && menu.children.length > 0" :index="menu.path">
|
|
71
|
+
<template #title>
|
|
72
|
+
<span class="menu-icon">{{ getMenuIcon(menu.icon) }}</span>
|
|
73
|
+
<span>{{ menu.title }}</span>
|
|
74
|
+
</template>
|
|
75
|
+
<MenuItem
|
|
76
|
+
v-for="child in menu.children"
|
|
77
|
+
:key="child.path"
|
|
78
|
+
:index="child.path"
|
|
79
|
+
>
|
|
80
|
+
<span class="menu-icon">{{ getMenuIcon(child.icon) }}</span>
|
|
81
|
+
<span>{{ child.title }}</span>
|
|
82
|
+
</MenuItem>
|
|
83
|
+
</SubMenu>
|
|
84
|
+
<!-- 无子菜单 -->
|
|
85
|
+
<MenuItem v-else :index="menu.path">
|
|
86
|
+
<span class="menu-icon">{{ getMenuIcon(menu.icon) }}</span>
|
|
87
|
+
<span>{{ menu.title }}</span>
|
|
88
|
+
</MenuItem>
|
|
89
|
+
</template>
|
|
90
|
+
</Menu>
|
|
91
|
+
|
|
92
|
+
<!-- 用户信息 -->
|
|
93
|
+
<div class="sidebar__user" v-if="!isCollapsed">
|
|
94
|
+
<div class="sidebar__user-info">
|
|
95
|
+
<span class="sidebar__user-name">{{ userName || '用户' }}</span>
|
|
96
|
+
<span class="sidebar__user-role">{{ userInfo?.departmentName || '' }}</span>
|
|
97
|
+
</div>
|
|
98
|
+
<Button type="text" size="small" @click="logout">退出</Button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<style lang="scss" scoped>
|
|
104
|
+
.sidebar {
|
|
105
|
+
height: 100%;
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
background-color: var(--bg-color);
|
|
109
|
+
|
|
110
|
+
&__logo {
|
|
111
|
+
height: 50px;
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
gap: 10px;
|
|
116
|
+
border-bottom: 1px solid var(--color-border-lighter);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
&__logo-img {
|
|
120
|
+
width: 32px;
|
|
121
|
+
height: 32px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
&__logo-text {
|
|
125
|
+
font-size: 16px;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
color: var(--color-primary);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&__search {
|
|
131
|
+
padding: 10px;
|
|
132
|
+
border-bottom: 1px solid var(--color-border-lighter);
|
|
133
|
+
position: relative;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
&__search-results {
|
|
137
|
+
position: absolute;
|
|
138
|
+
top: 100%;
|
|
139
|
+
left: 0;
|
|
140
|
+
right: 0;
|
|
141
|
+
background-color: var(--bg-color);
|
|
142
|
+
border: 1px solid var(--color-border-lighter);
|
|
143
|
+
border-radius: var(--border-radius-base);
|
|
144
|
+
box-shadow: var(--box-shadow);
|
|
145
|
+
max-height: 300px;
|
|
146
|
+
overflow-y: auto;
|
|
147
|
+
z-index: 100;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
&__search-item {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 10px;
|
|
154
|
+
padding: 10px 12px;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
transition: background-color 0.2s;
|
|
157
|
+
|
|
158
|
+
&:hover {
|
|
159
|
+
background-color: var(--color-fill);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&-info {
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
gap: 2px;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
&-title {
|
|
169
|
+
font-size: 14px;
|
|
170
|
+
color: var(--color-text-primary);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
&-parent {
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
color: var(--color-text-secondary);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
&__menu {
|
|
180
|
+
flex: 1;
|
|
181
|
+
border-right: none;
|
|
182
|
+
overflow-y: auto;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
&__user {
|
|
186
|
+
padding: 10px;
|
|
187
|
+
border-top: 1px solid var(--color-border-lighter);
|
|
188
|
+
display: flex;
|
|
189
|
+
align-items: center;
|
|
190
|
+
justify-content: space-between;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
&__user-info {
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
gap: 2px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
&__user-name {
|
|
200
|
+
font-size: 14px;
|
|
201
|
+
font-weight: 500;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
&__user-role {
|
|
205
|
+
font-size: 12px;
|
|
206
|
+
color: var(--color-text-secondary);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.menu-icon {
|
|
211
|
+
margin-right: 8px;
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
</script>
|
|
3
|
+
|
|
4
|
+
<template>
|
|
5
|
+
<div class="tabs-wrapper">
|
|
6
|
+
<!-- 标签页功能后期自行开发 -->
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<style lang="scss" scoped>
|
|
11
|
+
.tabs-wrapper {
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
color: var(--color-text-secondary);
|
|
18
|
+
font-size: 12px;
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { useAppStore } from '@/stores/app'
|
|
4
|
+
import Sidebar from './Sidebar.vue'
|
|
5
|
+
import Header from './Header.vue'
|
|
6
|
+
|
|
7
|
+
const appStore = useAppStore()
|
|
8
|
+
|
|
9
|
+
const sidebarWidth = computed(() =>
|
|
10
|
+
appStore.isCollapsed ? '64px' : '210px'
|
|
11
|
+
)
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="layout">
|
|
16
|
+
<aside class="layout__aside" :style="{ width: sidebarWidth }">
|
|
17
|
+
<Sidebar />
|
|
18
|
+
</aside>
|
|
19
|
+
<div class="layout__main">
|
|
20
|
+
<Header class="layout__header" />
|
|
21
|
+
<main class="layout__content">
|
|
22
|
+
<router-view />
|
|
23
|
+
</main>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<style lang="scss" scoped>
|
|
29
|
+
.layout {
|
|
30
|
+
display: flex;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
|
|
34
|
+
&__aside {
|
|
35
|
+
transition: width 0.3s;
|
|
36
|
+
overflow: hidden;
|
|
37
|
+
flex-shrink: 0;
|
|
38
|
+
height: 100%;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&__main {
|
|
42
|
+
flex: 1;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
overflow: hidden;
|
|
46
|
+
height: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
&__header {
|
|
50
|
+
height: 50px;
|
|
51
|
+
background-color: var(--bg-color);
|
|
52
|
+
border-bottom: 1px solid var(--color-border-lighter);
|
|
53
|
+
flex-shrink: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&__content {
|
|
57
|
+
flex: 1;
|
|
58
|
+
overflow: auto;
|
|
59
|
+
background-color: var(--bg-color-page);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
</style>
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 应用核心 Hook
|
|
3
|
+
* 封装登录、退出、初始化等核心逻辑
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { computed, ref } from 'vue'
|
|
7
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
8
|
+
import { Message } from '@xto/feedback'
|
|
9
|
+
import { useAuthStore } from '@/stores/auth'
|
|
10
|
+
import { useUserStore } from '@/stores/user'
|
|
11
|
+
import { useMenuStore } from '@/stores/menu'
|
|
12
|
+
import { login as loginApi, logout as logoutApi } from '@/api/auth'
|
|
13
|
+
import { getCurrentUser } from '@/api/user'
|
|
14
|
+
import { getUserMenu } from '@/api/menu'
|
|
15
|
+
import type { UserInfo, RemoteMenuItem, LoginResult } from '@/types/api'
|
|
16
|
+
|
|
17
|
+
export function useApp() {
|
|
18
|
+
const router = useRouter()
|
|
19
|
+
const route = useRoute()
|
|
20
|
+
const authStore = useAuthStore()
|
|
21
|
+
const userStore = useUserStore()
|
|
22
|
+
const menuStore = useMenuStore()
|
|
23
|
+
|
|
24
|
+
const loading = ref(false)
|
|
25
|
+
|
|
26
|
+
// ========== 计算属性 ==========
|
|
27
|
+
|
|
28
|
+
// 是否已登录
|
|
29
|
+
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
|
30
|
+
|
|
31
|
+
// 用户信息
|
|
32
|
+
const userInfo = computed(() => userStore.userInfo)
|
|
33
|
+
|
|
34
|
+
// 用户名
|
|
35
|
+
const userName = computed(() => userStore.userName || '')
|
|
36
|
+
|
|
37
|
+
// 菜单列表
|
|
38
|
+
const menuList = computed(() => menuStore.menuList)
|
|
39
|
+
|
|
40
|
+
// 首页路径
|
|
41
|
+
const indexPath = computed(() => menuStore.index)
|
|
42
|
+
|
|
43
|
+
// ========== 登录 ==========
|
|
44
|
+
|
|
45
|
+
const login = async (uid: string, password: string) => {
|
|
46
|
+
loading.value = true
|
|
47
|
+
try {
|
|
48
|
+
// 1. 清除旧状态
|
|
49
|
+
clearAllState()
|
|
50
|
+
|
|
51
|
+
// 2. 调用登录接口
|
|
52
|
+
const res = await loginApi({ uid, password })
|
|
53
|
+
const loginData = res.data as LoginResult
|
|
54
|
+
|
|
55
|
+
// 3. 保存登录信息
|
|
56
|
+
authStore.login(loginData as unknown as Record<string, unknown>)
|
|
57
|
+
|
|
58
|
+
// 4. 加载用户信息
|
|
59
|
+
await loadUserInfo()
|
|
60
|
+
|
|
61
|
+
// 5. 加载菜单
|
|
62
|
+
await loadMenu()
|
|
63
|
+
|
|
64
|
+
// 6. 处理跳转
|
|
65
|
+
const redirectUrl = route.query['redirectUrl'] as string
|
|
66
|
+
if (redirectUrl) {
|
|
67
|
+
const url = decodeURIComponent(redirectUrl)
|
|
68
|
+
if (url.startsWith('http')) {
|
|
69
|
+
// 外网地址,带上 code 跳转
|
|
70
|
+
const code = loginData.code
|
|
71
|
+
location.href = url.includes('?') ? `${url}&code=${code}` : `${url}?code=${code}`
|
|
72
|
+
} else {
|
|
73
|
+
await router.replace(url)
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
await router.replace(indexPath.value || '/')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Message.success('登录成功')
|
|
80
|
+
return { success: true, data: loginData }
|
|
81
|
+
} catch (error: any) {
|
|
82
|
+
Message.error(error?.message || '登录失败')
|
|
83
|
+
return { success: false, error }
|
|
84
|
+
} finally {
|
|
85
|
+
loading.value = false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ========== 退出登录 ==========
|
|
90
|
+
|
|
91
|
+
const logout = async (showMessage = true) => {
|
|
92
|
+
try {
|
|
93
|
+
await logoutApi()
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('退出登录接口失败', error)
|
|
96
|
+
} finally {
|
|
97
|
+
clearAllState()
|
|
98
|
+
router.push('/login')
|
|
99
|
+
if (showMessage) {
|
|
100
|
+
Message.success('退出登录成功')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ========== 加载用户信息 ==========
|
|
106
|
+
|
|
107
|
+
const loadUserInfo = async () => {
|
|
108
|
+
const res = await getCurrentUser()
|
|
109
|
+
userStore.setUserInfo(res.data as UserInfo)
|
|
110
|
+
return res.data
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ========== 加载菜单 ==========
|
|
114
|
+
|
|
115
|
+
const loadMenu = async () => {
|
|
116
|
+
const res = await getUserMenu()
|
|
117
|
+
menuStore.setMenuFromRemote(res.data as RemoteMenuItem[])
|
|
118
|
+
return menuStore.menuList
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ========== 初始化应用(路由守卫中使用) ==========
|
|
122
|
+
|
|
123
|
+
const initApp = async () => {
|
|
124
|
+
if (!authStore.isLoggedIn) {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// 如果有 token 但没有用户信息,则加载
|
|
130
|
+
if (!userStore.userInfo) {
|
|
131
|
+
await loadUserInfo()
|
|
132
|
+
}
|
|
133
|
+
// 如果没有菜单,则加载
|
|
134
|
+
if (!menuStore.hasMenu) {
|
|
135
|
+
await loadMenu()
|
|
136
|
+
}
|
|
137
|
+
return true
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('初始化应用失败', error)
|
|
140
|
+
clearAllState()
|
|
141
|
+
return false
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ========== 清除所有状态 ==========
|
|
146
|
+
|
|
147
|
+
const clearAllState = () => {
|
|
148
|
+
authStore.logout()
|
|
149
|
+
userStore.clearUserInfo()
|
|
150
|
+
menuStore.clearMenu()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
// 状态
|
|
155
|
+
loading,
|
|
156
|
+
isLoggedIn,
|
|
157
|
+
userInfo,
|
|
158
|
+
userName,
|
|
159
|
+
menuList,
|
|
160
|
+
indexPath,
|
|
161
|
+
|
|
162
|
+
// 方法
|
|
163
|
+
login,
|
|
164
|
+
logout,
|
|
165
|
+
loadUserInfo,
|
|
166
|
+
loadMenu,
|
|
167
|
+
initApp,
|
|
168
|
+
clearAllState
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 权限 Hook
|
|
3
|
+
* 封装权限检查、按钮权限等逻辑
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import { useMenuStore } from '@/stores/menu'
|
|
8
|
+
import { useAuthStore } from '@/stores/auth'
|
|
9
|
+
import { useUserStore } from '@/stores/user'
|
|
10
|
+
|
|
11
|
+
export function useAuth() {
|
|
12
|
+
const menuStore = useMenuStore()
|
|
13
|
+
const authStore = useAuthStore()
|
|
14
|
+
const userStore = useUserStore()
|
|
15
|
+
|
|
16
|
+
// 是否已登录
|
|
17
|
+
const isLoggedIn = computed(() => authStore.isLoggedIn)
|
|
18
|
+
|
|
19
|
+
// 用户名
|
|
20
|
+
const userName = computed(() => userStore.userName)
|
|
21
|
+
|
|
22
|
+
// 用户信息
|
|
23
|
+
const userInfo = computed(() => userStore.userInfo)
|
|
24
|
+
|
|
25
|
+
// ========== 检查按钮权限 ==========
|
|
26
|
+
|
|
27
|
+
const hasPermission = (permission: string | string[]): boolean => {
|
|
28
|
+
const currentPath = window.location.pathname
|
|
29
|
+
const btnList = menuStore.menuBtnListMap[currentPath] || []
|
|
30
|
+
const btnCodes = btnList.map(item => item.code)
|
|
31
|
+
|
|
32
|
+
if (Array.isArray(permission)) {
|
|
33
|
+
return permission.some(p => btnCodes.includes(p))
|
|
34
|
+
}
|
|
35
|
+
return btnCodes.includes(permission)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ========== 检查是否有任一权限 ==========
|
|
39
|
+
|
|
40
|
+
const hasAnyPermission = (permissions: string[]): boolean => {
|
|
41
|
+
return permissions.some(p => hasPermission(p))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ========== 检查是否有全部权限 ==========
|
|
45
|
+
|
|
46
|
+
const hasAllPermissions = (permissions: string[]): boolean => {
|
|
47
|
+
return permissions.every(p => hasPermission(p))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ========== 获取当前页面按钮权限列表 ==========
|
|
51
|
+
|
|
52
|
+
const getCurrentPagePermissions = (): string[] => {
|
|
53
|
+
const currentPath = window.location.pathname
|
|
54
|
+
const btnList = menuStore.menuBtnListMap[currentPath] || []
|
|
55
|
+
return btnList.map(item => item.code)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
// 状态
|
|
60
|
+
isLoggedIn,
|
|
61
|
+
userName,
|
|
62
|
+
userInfo,
|
|
63
|
+
|
|
64
|
+
// 方法
|
|
65
|
+
hasPermission,
|
|
66
|
+
hasAnyPermission,
|
|
67
|
+
hasAllPermissions,
|
|
68
|
+
getCurrentPagePermissions
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 表单组合函数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ref, reactive } from 'vue'
|
|
6
|
+
|
|
7
|
+
export interface FormOptions<T = any> {
|
|
8
|
+
rules?: Record<string, any[]>
|
|
9
|
+
onSubmit?: (data: T) => Promise<void> | void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useForm<T extends Record<string, any> = Record<string, any>>(
|
|
13
|
+
initialValues: T,
|
|
14
|
+
options: FormOptions<T> = {}
|
|
15
|
+
) {
|
|
16
|
+
const { rules, onSubmit } = options
|
|
17
|
+
|
|
18
|
+
const formRef = ref()
|
|
19
|
+
const formData = reactive<T>({ ...initialValues } as T)
|
|
20
|
+
const loading = ref(false)
|
|
21
|
+
const visible = ref(false)
|
|
22
|
+
const isEdit = ref(false)
|
|
23
|
+
|
|
24
|
+
// 打开新增
|
|
25
|
+
const openAdd = () => {
|
|
26
|
+
resetForm()
|
|
27
|
+
isEdit.value = false
|
|
28
|
+
visible.value = true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 打开编辑
|
|
32
|
+
const openEdit = (data: Partial<T>) => {
|
|
33
|
+
Object.assign(formData, data)
|
|
34
|
+
isEdit.value = true
|
|
35
|
+
visible.value = true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 关闭
|
|
39
|
+
const close = () => {
|
|
40
|
+
visible.value = false
|
|
41
|
+
resetForm()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 重置表单
|
|
45
|
+
const resetForm = () => {
|
|
46
|
+
Object.keys(initialValues).forEach(key => {
|
|
47
|
+
(formData as any)[key] = initialValues[key]
|
|
48
|
+
})
|
|
49
|
+
formRef.value?.resetFields()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 提交
|
|
53
|
+
const handleSubmit = async () => {
|
|
54
|
+
try {
|
|
55
|
+
await formRef.value?.validate()
|
|
56
|
+
loading.value = true
|
|
57
|
+
await onSubmit?.(formData as T)
|
|
58
|
+
close()
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(error)
|
|
61
|
+
} finally {
|
|
62
|
+
loading.value = false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
formRef,
|
|
68
|
+
formData,
|
|
69
|
+
rules,
|
|
70
|
+
loading,
|
|
71
|
+
visible,
|
|
72
|
+
isEdit,
|
|
73
|
+
openAdd,
|
|
74
|
+
openEdit,
|
|
75
|
+
close,
|
|
76
|
+
resetForm,
|
|
77
|
+
handleSubmit
|
|
78
|
+
}
|
|
79
|
+
}
|