image.png
顶级菜单支持拖拽排序,侧边栏菜单会按照顶级菜单拖放顺序生成

点击左侧节点 编辑
image.png
添加节点
image.png
路由name目前没用到 之前想通过路由name筛选路由

删除
image.png

3-1 菜单管理页面

image.png
src/views/system/menu/index.vue

  1. <template>
  2. <div class="menu-container">
  3. <!-- 菜单树 -->
  4. <el-card class="tree-card">
  5. <template #header>
  6. <el-button @click="handleCreateRootMenu">新增顶级菜单</el-button>
  7. </template>
  8. <div class="block">
  9. <div class="menu-tree">
  10. <el-tree
  11. ref="menuTreeRef"
  12. :data="menus"
  13. highlight-current
  14. node-key="id"
  15. :expand-on-click-node="false"
  16. :check-strictly="true"
  17. @node-click="handleNodeClick"
  18. :props="defaultProps"
  19. draggable
  20. :allow-drop="allowDrop"
  21. :allow-drag="allowDrag"
  22. @node-drop="handleNodeDrop"
  23. >
  24. <template #default="{ node, data }">
  25. <span class="custom-tree-node">
  26. <span>{{ node.label }}</span>
  27. <span>
  28. <el-button type="text" @click.stop="handleCreateChildMenu(data)">
  29. 添加
  30. </el-button>
  31. <el-button type="text" @click.stop="handleRemoveMenu(node, data)">
  32. 删除
  33. </el-button>
  34. </span>
  35. </span>
  36. </template>
  37. </el-tree>
  38. </div>
  39. </div>
  40. </el-card>
  41. <el-card class="edit-card">
  42. <template #header>
  43. 编辑菜单
  44. </template>
  45. <editor-menu v-show="editData && editData.id" :data="editData" />
  46. <span v-if="editData == null">从菜单列表选择一项后,进行编辑</span>
  47. </el-card>
  48. <!-- 添加菜单 -->
  49. <right-panel v-model="dialogVisible" :title="panelTitle">
  50. <div class="menu-form">
  51. <el-form
  52. ref="menuFormRef"
  53. :model="menuFormData"
  54. :rules="menuFormRules"
  55. label-width="100px"
  56. >
  57. <el-form-item label="菜单名称" prop="title">
  58. <el-input
  59. v-model="menuFormData.title"
  60. placeholder="请输入菜单名称"
  61. />
  62. </el-form-item>
  63. <el-form-item label="路径" prop="path">
  64. <el-input
  65. v-model="menuFormData.path"
  66. placeholder="请输入路由路径"
  67. />
  68. </el-form-item>
  69. <el-form-item label="路由Name" prop="name">
  70. <el-input
  71. v-model="menuFormData.name"
  72. placeholder="请输入路由名称"
  73. />
  74. </el-form-item>
  75. <el-form-item label="图标" prop="icon">
  76. <el-input
  77. v-model="menuFormData.icon"
  78. placeholder="请输入icon名称"
  79. />
  80. </el-form-item>
  81. <el-form-item>
  82. <el-button type="primary" @click="submitMenuForm"
  83. >创建菜单</el-button
  84. >
  85. </el-form-item>
  86. </el-form>
  87. </div>
  88. </right-panel>
  89. </div>
  90. </template>
  91. <script lang="ts">
  92. import { defineComponent, ref, reactive, computed, onMounted, watch, getCurrentInstance } from 'vue'
  93. import { ElTree, ElForm } from 'element-plus'
  94. import RightPanel from '@/components/RightPanel/index.vue'
  95. import { addNewMenu, removeMenuByID, updateBulkMenu } from '@/api/menu'
  96. import { ITreeItemData, MenuData } from '@/store/modules/menu'
  97. import { useStore } from '@/store'
  98. import EditorMenu from './components/editorMenu.vue'
  99. import { useReloadPage } from '@/hooks/useReload'
  100. interface ITreeNode {
  101. id: number
  102. title: string
  103. children: ITreeNode[]
  104. parentId?: number
  105. sortId: number
  106. parent: {
  107. data: ITreeNode
  108. },
  109. data: ITreeItemData
  110. }
  111. type IMenuTree = InstanceType<typeof ElTree>
  112. type IMenuForm = InstanceType<typeof ElForm>
  113. type IMenuItemNotID = Omit<ITreeItemData, 'id'>
  114. export default defineComponent({
  115. name: 'Menu',
  116. components: {
  117. RightPanel,
  118. EditorMenu
  119. },
  120. setup() {
  121. const store = useStore()
  122. const { proxy } = getCurrentInstance()!
  123. const menuTreeRef = ref<IMenuTree | null>(null)
  124. const treeData = computed(() => store.getters.menusTree)
  125. const menus = ref<ITreeItemData[]>([])
  126. const editData = ref<MenuData|null>()
  127. watch(treeData, (value: ITreeItemData[]) => {
  128. menus.value = JSON.parse(JSON.stringify(value))
  129. editData.value = null
  130. })
  131. onMounted(() => { // 获取全部菜单
  132. store.dispatch('menu/getAllMenuList')
  133. })
  134. // tree props
  135. const defaultProps = ref({
  136. children: 'children',
  137. label: 'title'
  138. })
  139. // 重新刷新整个系统
  140. const { reloadPage } = useReloadPage()
  141. // 添加菜单panel
  142. const dialogVisible = ref(false)
  143. watch(dialogVisible, value => {
  144. if (!value) {
  145. (menuFormRef.value as IMenuForm).resetFields()
  146. }
  147. })
  148. // 分配sortId 根据最后一个数据sortId+1
  149. const getMenuNodeSortID = (list: ITreeItemData[]) => {
  150. if (list && list.length > 0) {
  151. return list[list.length - 1].sort_id + 1
  152. }
  153. return 0
  154. }
  155. // 移除节点
  156. const removeNode = (node: ITreeNode, childId: number) => {
  157. const parent = node.parent
  158. const children = parent.data.children || parent.data
  159. const index = children.findIndex(d => d.id === childId)
  160. children.splice(index, 1)
  161. menus.value = [...menus.value]
  162. }
  163. /**
  164. * node: 当前node对象
  165. * menuData: 当前节点数据
  166. */
  167. const handleRemoveMenu = (node: ITreeNode, menuData: ITreeItemData) => {
  168. proxy?.$confirm(`您确认要删除菜单${menuData.title}吗?`, '删除确认', {
  169. type: 'warning'
  170. }).then(() => {
  171. // 根据id删除菜单
  172. removeMenuByID(menuData.id).then(res => {
  173. if (res.code === 0) {
  174. proxy?.$message.success('删除成功')
  175. removeNode(node, menuData.id)
  176. // 如果删除的是当前编辑的菜单 就重置编辑表单
  177. if (editData.value && menuData.id === editData.value.id) {
  178. editData.value = null
  179. }
  180. // 是否重新刷新整个系统
  181. reloadPage()
  182. }
  183. })
  184. }).catch(() => {
  185. proxy?.$message({
  186. type: 'info',
  187. message: '已取消删除'
  188. })
  189. })
  190. }
  191. // 新增顶级菜单
  192. // 添加菜单表单
  193. const menuFormRef = ref<IMenuForm | null>(null)
  194. // 菜单表单数据
  195. const menuFormData = reactive<IMenuItemNotID>({
  196. title: '',
  197. path: '',
  198. name: '',
  199. icon: '',
  200. parent_id: '',
  201. sort_id: 0
  202. })
  203. const menuType = ref(0) // 添加菜单类型 0顶级 1子级
  204. // 面板title
  205. const panelTitle = computed(() =>
  206. menuType.value === 0 ? '添加顶级菜单' : '添加子菜单'
  207. )
  208. // 重置添加菜单状态
  209. const resetStatus = () => {
  210. dialogVisible.value = false
  211. menuFormRef.value?.resetFields()
  212. parentData.value = null
  213. }
  214. // ············· 添加顶级菜单 ······················
  215. // 点击添加顶级菜单
  216. const handleCreateRootMenu = () => {
  217. menuType.value = 0
  218. dialogVisible.value = true
  219. }
  220. // 顶级菜单分配partentId和sortId
  221. const allocRootMenuId = (data: IMenuItemNotID) => {
  222. const sortId = getMenuNodeSortID(menus.value)
  223. data.sort_id = sortId
  224. data.parent_id = '0'
  225. }
  226. // 顶级菜单 添加到 tree组件中
  227. const appendRootMenu = (id: number, data: IMenuItemNotID) => {
  228. const node = { id, ...data, children: [] }
  229. menus.value.push(node)
  230. menus.value = [...menus.value]
  231. }
  232. // 添加顶级菜单
  233. const handleAddRootMenu = async (data: IMenuItemNotID) => {
  234. allocRootMenuId(data)
  235. await addNewMenu(data).then(res => {
  236. if (res.code === 0) {
  237. const { id } = res.data
  238. appendRootMenu(id, data)
  239. proxy?.$message.success('菜单创建成功')
  240. // 是否重新刷新整个系统
  241. reloadPage()
  242. }
  243. })
  244. }
  245. // ············· 添加子菜单 ······················
  246. // 子菜单分配sortid 和 parentId
  247. const allocChildMenuId = (data: IMenuItemNotID, parentData: ITreeItemData): IMenuItemNotID => {
  248. const pid = parentData.id as number
  249. let sortId = 0
  250. if (!parentData.children) {
  251. parentData.children = []
  252. }
  253. if (parentData.children.length > 0) {
  254. sortId = getMenuNodeSortID(parentData.children)
  255. }
  256. data.sort_id = sortId
  257. data.parent_id = pid
  258. return data
  259. }
  260. // 添加子菜单到tree组件中
  261. const appendChildMenu = (child: ITreeItemData, parentData: ITreeItemData) => {
  262. (parentData.children!).push(child)
  263. menus.value = [...menus.value]
  264. }
  265. // 添加子菜单
  266. const parentData = ref<ITreeItemData | null>(null) // 缓存父菜单data
  267. const handleAddChildMenu = async (data: IMenuItemNotID) => {
  268. const child = allocChildMenuId(data, parentData.value!)
  269. await addNewMenu(data).then(res => {
  270. if (res.code === 0) {
  271. const { id } = res.data
  272. ;(child as ITreeItemData).id = id
  273. appendChildMenu(child as ITreeItemData, parentData.value!)
  274. proxy?.$message.success('菜单创建成功')
  275. // 是否重新刷新整个系统
  276. reloadPage()
  277. }
  278. })
  279. }
  280. // 新增子菜单
  281. const handleCreateChildMenu = (data: ITreeItemData) => {
  282. menuType.value = 1
  283. dialogVisible.value = true
  284. parentData.value = data
  285. }
  286. // 菜单编辑
  287. const handleNodeClick = (data: MenuData) => {
  288. editData.value = { ...data }
  289. }
  290. // 提交menuForm
  291. const submitMenuForm = () => {
  292. (menuFormRef.value as IMenuForm).validate(async valid => {
  293. if (valid) {
  294. if (menuType.value === 0) {
  295. // 添加根菜单
  296. await handleAddRootMenu({ ...menuFormData })
  297. } else if (menuType.value === 1) {
  298. // 添加子菜单
  299. await handleAddChildMenu({ ...menuFormData })
  300. }
  301. // 重置相关状态
  302. resetStatus()
  303. }
  304. })
  305. }
  306. // 实现顶级菜单 拖拽排序
  307. // 拖拽一级节点
  308. const allowDrag = (draggingNode: ITreeNode) => {
  309. const data = draggingNode.data
  310. return data.parent_id === 0 || data.parent_id == null
  311. }
  312. // 拖放一级节点
  313. type DropType = 'before' | 'after' | 'inner'
  314. const allowDrop = (draggingNode: ITreeNode, dropNode: ITreeNode, type: DropType) => {
  315. if (dropNode.data.parent_id === 0 || dropNode.data.parent_id == null) {
  316. return type !== 'inner'
  317. }
  318. }
  319. // 拖放完成事件
  320. const handleNodeDrop = () => {
  321. menus.value.forEach((menu, index) => {
  322. menu.sort_id = index
  323. })
  324. // 批量更新菜单状态 这里是为了更新sort_id
  325. const menuList = menus.value.map(menu => {
  326. const temp = { ...menu }
  327. delete menu.children
  328. return temp
  329. })
  330. // 批量更新
  331. updateBulkMenu(menuList).then(res => {
  332. if (res.code === 0) {
  333. // 重新生成菜单 1 代表是菜单排序更新
  334. store.dispatch('permission/generateRoutes', 1)
  335. }
  336. })
  337. }
  338. // 验证规则
  339. const menuFormRules = reactive({
  340. title: {
  341. required: true,
  342. message: '请输入菜单名称',
  343. trigger: 'blur'
  344. },
  345. path: {
  346. required: true,
  347. message: '请输入路由路径',
  348. trigger: 'blur'
  349. },
  350. name: {
  351. required: true,
  352. message: '请输入路由名称',
  353. trigger: 'blur'
  354. }
  355. })
  356. return {
  357. menus,
  358. handleCreateRootMenu,
  359. handleCreateChildMenu,
  360. handleRemoveMenu,
  361. menuTreeRef,
  362. handleNodeClick,
  363. dialogVisible,
  364. menuFormData,
  365. menuFormRules,
  366. menuFormRef,
  367. submitMenuForm,
  368. defaultProps,
  369. panelTitle,
  370. editData,
  371. allowDrag,
  372. allowDrop,
  373. handleNodeDrop
  374. }
  375. }
  376. })
  377. </script>
  378. <style lang="scss">
  379. .menu-container {
  380. display: flex;
  381. padding: 20px;
  382. justify-content: space-around;
  383. .menu-tree {
  384. height: 400px;
  385. overflow-y: scroll;
  386. }
  387. .tree-card {
  388. min-width: 500px;
  389. padding-bottom: 30px;
  390. }
  391. .edit-card {
  392. flex: 1;
  393. margin-left: 15px;
  394. }
  395. .el-form-item__content {
  396. min-width: 220px;
  397. }
  398. .custom-tree-node {
  399. flex: 1;
  400. display: flex;
  401. align-items: center;
  402. justify-content: space-between;
  403. font-size: 14px;
  404. padding-right: 8px;
  405. }
  406. .menu-form {
  407. padding: 20px 10px 20px 0;
  408. box-sizing: border-box;
  409. }
  410. }
  411. </style>

创建编辑菜单组件

image.png
src/views/system/menu/components/editorMenu.vue

  1. <template>
  2. <div class="editor-container">
  3. <el-form
  4. ref="editFormRef"
  5. :model="editData"
  6. :rules="menuFormRules"
  7. label-width="100px"
  8. >
  9. <el-form-item label="菜单名称" prop="title">
  10. <el-input
  11. v-model="editData.title"
  12. placeholder="请输入菜单名称"
  13. />
  14. </el-form-item>
  15. <el-form-item label="路径" prop="path">
  16. <el-input
  17. v-model="editData.path"
  18. placeholder="请输入路由路径"
  19. />
  20. </el-form-item>
  21. <el-form-item label="路由Name" prop="name">
  22. <el-input
  23. v-model="editData.name"
  24. placeholder="请输入路由名称"
  25. />
  26. </el-form-item>
  27. <el-form-item label="图标" prop="icon">
  28. <el-input
  29. v-model="editData.icon"
  30. placeholder="请输入icon名称"
  31. />
  32. </el-form-item>
  33. <el-form-item>
  34. <el-button
  35. type="primary"
  36. @click="submitMenuForm"
  37. :loading="loading"
  38. >编辑菜单</el-button>
  39. <el-button @click="submitReset">重置</el-button>
  40. </el-form-item>
  41. </el-form>
  42. </div>
  43. </template>
  44. <script lang="ts">
  45. import { defineComponent, PropType, ref, watch, getCurrentInstance } from 'vue'
  46. import { MenuData } from '@/store/modules/menu'
  47. import { ElForm } from 'element-plus'
  48. import { updateMenuByID } from '@/api/menu'
  49. import { useStore } from '@/store'
  50. import { useReloadPage } from '@/hooks/useReload'
  51. type FormInstance = InstanceType<typeof ElForm>
  52. export default defineComponent({
  53. name: 'EditorMenu',
  54. props: {
  55. data: {
  56. type: Object as PropType<MenuData>
  57. }
  58. },
  59. emits: ['updateEdit'],
  60. setup(props) {
  61. const store = useStore()
  62. const { proxy } = getCurrentInstance()!
  63. const loading = ref(false)
  64. const editFormRef = ref<FormInstance|null>(null)
  65. const editData = ref({
  66. id: '',
  67. title: '',
  68. name: '',
  69. path: '',
  70. icon: ''
  71. })
  72. // 验证规则
  73. const menuFormRules = {
  74. title: {
  75. required: true,
  76. message: '请输入菜单名称',
  77. trigger: 'blur'
  78. },
  79. path: {
  80. required: true,
  81. message: '请输入路由路径',
  82. trigger: 'blur'
  83. },
  84. name: {
  85. required: true,
  86. message: '请输入路由名称',
  87. trigger: 'blur'
  88. }
  89. }
  90. const resetFormData = (data: MenuData) => {
  91. if (data) {
  92. const { id, title, name, path, icon } = data
  93. editData.value = { id: String(id), title, name, path, icon }
  94. }
  95. }
  96. watch(() => props.data, (value) => {
  97. if (value) {
  98. resetFormData(value)
  99. }
  100. })
  101. // 刷新系统
  102. const { reloadPage } = useReloadPage()
  103. // 提交编辑菜单
  104. const submitMenuForm = () => {
  105. (editFormRef.value as FormInstance).validate(valid => {
  106. if (valid) {
  107. loading.value = true
  108. updateMenuByID(Number(editData.value.id), editData.value).then(res => {
  109. if (res.code === 0) {
  110. proxy?.$message.success('菜单编辑成功')
  111. // 重新获取菜单
  112. store.dispatch('menu/getAllMenuList')
  113. reloadPage()
  114. }
  115. }).finally(() => {
  116. loading.value = false
  117. })
  118. }
  119. })
  120. }
  121. // 重置编辑菜单
  122. const submitReset = () => {
  123. resetFormData(props.data as MenuData)
  124. }
  125. return {
  126. editData,
  127. submitMenuForm,
  128. submitReset,
  129. editFormRef,
  130. menuFormRules,
  131. loading
  132. }
  133. }
  134. })
  135. </script>

3-2 菜单store

src/store/modules/menu.ts

  1. import { Module, MutationTree, ActionTree } from 'vuex'
  2. import { IRootState } from '@/store'
  3. import { getAllMenus } from '@/api/menu'
  4. import generateTree from '@/utils/generateTree'
  5. import generateMenuTree from '@/utils/generateMenuTree'
  6. import { getAccessByRoles } from '@/api/roleAccess'
  7. /* eslint-disable camelcase */
  8. export interface MenuData {
  9. id: number;
  10. title: string;
  11. path: string;
  12. name: string;
  13. icon: string;
  14. parent_id: string | number;
  15. sort_id: number;
  16. }
  17. export interface ITreeItemData extends MenuData {
  18. children?: ITreeItemData[]
  19. }
  20. // state类型
  21. export interface IMenusState {
  22. menuTreeData: Array<ITreeItemData>; // 树形菜单数据
  23. menuList: Array<MenuData>; // 原始菜单列表数据
  24. authMenuTreeData: Array<ITreeItemData>; // 树形菜单数据
  25. authMenuList: Array<MenuData>; // 原始菜单列表数据
  26. }
  27. // mutations类型
  28. type IMutations = MutationTree<IMenusState>
  29. // actions类型
  30. type IActions = ActionTree<IMenusState, IRootState>
  31. // 定义state
  32. const state: IMenusState = {
  33. menuTreeData: [],
  34. menuList: [],
  35. authMenuTreeData: [],
  36. authMenuList: []
  37. }
  38. // 定义mutations
  39. const mutations: IMutations = {
  40. SET_MENU_LIST(state, data: IMenusState['menuList']) {
  41. state.menuList = data
  42. },
  43. SET_MENU_TREE_DATA(state, data: IMenusState['menuTreeData']) {
  44. state.menuTreeData = data
  45. },
  46. SET_AUTH_MENU_LIST(state, data: IMenusState['menuList']) {
  47. state.authMenuList = data
  48. },
  49. SET_AUTH_MENU_TREE_DATA(state, data: IMenusState['menuTreeData']) {
  50. state.authMenuTreeData = data
  51. }
  52. }
  53. // 定义actions
  54. const actions: IActions = {
  55. getAllMenuList({ dispatch, commit }) {
  56. return new Promise<MenuData[]>((resolve, reject) => {
  57. getAllMenus().then(response => {
  58. const { data } = response
  59. dispatch('generateTreeData', [...data])
  60. commit('SET_MENU_LIST', data)
  61. resolve([...data])
  62. }).catch(reject)
  63. })
  64. },
  65. generateTreeData({ commit }, data: IMenusState['menuList']) {
  66. const treeData = generateTree(data)
  67. commit('SET_MENU_TREE_DATA', treeData)
  68. },
  69. generateAuthTreeData({ commit }, data: IMenusState['menuList']) {
  70. const treeData = generateMenuTree(data)
  71. commit('SET_AUTH_MENU_TREE_DATA', treeData)
  72. },
  73. getAllMenuListByAdmin({ dispatch, commit }) {
  74. return new Promise<MenuData[]>((resolve, reject) => {
  75. getAllMenus().then(response => {
  76. const { data } = response
  77. dispatch('generateAuthTreeData', [...data])
  78. commit('SET_AUTH_MENU_LIST', data)
  79. resolve([...data])
  80. }).catch(reject)
  81. })
  82. },
  83. getAccessByRoles({ dispatch, commit }, roles: number[]) {
  84. return new Promise<MenuData[]>((resolve, reject) => {
  85. getAccessByRoles(roles).then(response => {
  86. const { access } = response.data
  87. dispatch('generateAuthTreeData', [...access])
  88. commit('SET_AUTH_MENU_LIST', access)
  89. resolve([...access])
  90. }).catch(reject)
  91. })
  92. }
  93. }
  94. // 定义menu module
  95. const menu: Module<IMenusState, IRootState> = {
  96. namespaced: true,
  97. state,
  98. mutations,
  99. actions
  100. }
  101. export default menu

store.ts

src/store/index.ts

  1. import { InjectionKey } from 'vue'
  2. import { createStore, Store, useStore as baseUseStore } from 'vuex'
  3. import createPersistedState from 'vuex-persistedstate'
  4. import app, { IAppState } from '@/store/modules/app'
  5. import tagsView, { ITagsViewState } from '@/store/modules/tagsView'
  6. import settings, { ISettingsState } from '@/store/modules/settings'
  7. import user, { IUserState } from '@/store/modules/user'
  8. import getters from './getters'
  9. import menu, { IMenusState } from './modules/menu'
  10. import role, { IRoleState } from './modules/role'
  11. import permission, { IPermissionState } from './modules/permission'
  12. // 模块声明在根状态下
  13. export interface IRootState {
  14. app: IAppState;
  15. user: IUserState;
  16. menu: IMenusState;
  17. role: IRoleState;
  18. tagsView: ITagsViewState;
  19. settings: ISettingsState;
  20. permission: IPermissionState;
  21. }
  22. // 通过下面方式使用 TypeScript 定义 store 能正确地为 store 提供类型声明。
  23. // https://next.vuex.vuejs.org/guide/typescript-support.html#simplifying-usestore-usage
  24. // eslint-disable-next-line symbol-description
  25. export const key: InjectionKey<Store<IRootState>> = Symbol()
  26. // 对于getters在组件使用时没有类型提示
  27. // 有人提交了pr #1896 为getters创建泛型 应该还未发布
  28. // https://github.com/vuejs/vuex/pull/1896
  29. // 代码pr内容详情
  30. // https://github.com/vuejs/vuex/pull/1896/files#diff-093ad82a25aee498b11febf1cdcb6546e4d223ffcb49ed69cc275ac27ce0ccce
  31. // vuex store持久化 默认使用localstorage持久化
  32. const persisteAppState = createPersistedState({
  33. storage: window.sessionStorage, // 指定storage 也可自定义
  34. key: 'vuex_app', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  35. // paths: ['app'] // 针对app这个模块持久化
  36. // 只针对app模块下sidebar.opened状态持久化
  37. paths: ['app.sidebar.opened', 'app.size'] // 通过点连接符指定state路径
  38. })
  39. const persisteSettingsState = createPersistedState({
  40. storage: window.sessionStorage, // 指定storage 也可自定义
  41. key: 'vuex_setting', // 存储名 默认都是vuex 多个模块需要指定 否则会覆盖
  42. // paths: ['app'] // 针对app这个模块持久化
  43. // 只针对app模块下sidebar.opened状态持久化
  44. paths: ['settings.theme', 'settings.originalStyle', 'settings.tagsView', 'settings.sidebarLogo'] // 通过点连接符指定state路径
  45. })
  46. export default createStore<IRootState>({
  47. plugins: [
  48. persisteAppState,
  49. persisteSettingsState
  50. ],
  51. getters,
  52. modules: {
  53. app,
  54. user,
  55. tagsView,
  56. settings,
  57. menu,
  58. role,
  59. permission
  60. }
  61. })
  62. // 定义自己的 `useStore` 组合式函数
  63. // https://next.vuex.vuejs.org/zh/guide/typescript-support.html#%E7%AE%80%E5%8C%96-usestore-%E7%94%A8%E6%B3%95
  64. // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
  65. export function useStore () {
  66. return baseUseStore(key)
  67. }
  68. // vuex持久化 vuex-persistedstate文档说明
  69. // https://www.npmjs.com/package/vuex-persistedstate

3-3 菜单api

src/api/menu.ts

  1. import request from '@/api/config/request'
  2. import { MenuData } from '@/store/modules/menu'
  3. import { ApiResponse } from './type'
  4. // 添加新菜单
  5. export const addNewMenu = (data: Omit<MenuData, 'id'>): Promise<ApiResponse> => {
  6. return request.post(
  7. '/access/menu',
  8. data
  9. )
  10. }
  11. // 获取全部菜单
  12. export const getAllMenus = (): Promise<ApiResponse<MenuData[]>> => {
  13. return request.get('/access/menus')
  14. }
  15. // 删除指定菜单
  16. export const removeMenuByID = (id: number): Promise<ApiResponse<null>> => {
  17. return request.delete(`/access/menu/${id}`)
  18. }
  19. // 更新指定菜单
  20. type UpdateMenuData = Omit<MenuData, 'id'|'parent_id'|'sort_id'>
  21. export const updateMenuByID = (id: number, data: UpdateMenuData): Promise<ApiResponse<null>> => {
  22. return request.put(`/access/menu/${id}`, data)
  23. }
  24. // 批量更新菜单
  25. export const updateBulkMenu = (data: MenuData[]): Promise<ApiResponse<null>> => {
  26. return request.patch('/access/menu/update', {
  27. access: data
  28. })
  29. }
  30. import request from '@/api/config/request'
  31. import { MenuData } from '@/store/modules/menu'
  32. import { ApiResponse } from './type'
  33. // 添加新菜单
  34. export const addNewMenu = (data: Omit<MenuData, 'id'>): Promise<ApiResponse> => {
  35. return request.post(
  36. '/access/menu',
  37. data
  38. )
  39. }
  40. // 获取全部菜单
  41. export const getAllMenus = (): Promise<ApiResponse<MenuData[]>> => {
  42. return request.get('/access/menus')
  43. }
  44. // 删除指定菜单
  45. export const removeMenuByID = (id: number): Promise<ApiResponse<null>> => {
  46. return request.delete(`/access/menu/${id}`)
  47. }
  48. // 更新指定菜单
  49. type UpdateMenuData = Omit<MenuData, 'id'|'parent_id'|'sort_id'>
  50. export const updateMenuByID = (id: number, data: UpdateMenuData): Promise<ApiResponse<null>> => {
  51. return request.put(`/access/menu/${id}`, data)
  52. }

3-4 角色和菜单

image.png

api

src/api/roleAccess.ts

  1. import request from '@/api/config/request'
  2. import { MenuData } from '@/store/modules/menu'
  3. import { IRole, IRoleAccessList } from '@/store/modules/role'
  4. import { ApiResponse } from './type'
  5. /**
  6. * 根据角色分配权限
  7. * @param id 角色id
  8. * @param data 权限id列表
  9. */
  10. export const allocRoleAccess = (id: number, data: number[]): Promise<ApiResponse> => {
  11. return request.post(`/role_access/${id}`, {
  12. access: data
  13. })
  14. }
  15. /**
  16. * 根据角色获取权限
  17. * @param id 角色id
  18. * @param data 权限id列表
  19. */
  20. export const getRoleAccess = (id: number): Promise<ApiResponse<IRoleAccessList>> => {
  21. return request.get(`/role_access/${id}`)
  22. }
  23. // 根据用户角色获取用户菜单
  24. type RolesAccess = MenuData & {
  25. roles: IRole[]
  26. }
  27. interface ApiRolesAccess {
  28. access: RolesAccess[]
  29. }
  30. export const getAccessByRoles = (roles: number[]): Promise<ApiResponse<ApiRolesAccess>> => {
  31. return request.post('/role_access/role/access', {
  32. roles
  33. })
  34. }

3-5 权限store

src/store/modules/permission.ts

  1. import { Module, MutationTree, ActionTree } from 'vuex'
  2. import { RouteRecordRaw } from 'vue-router'
  3. import store, { IRootState } from '../index'
  4. import { asyncRoutes } from '../../router/index'
  5. import { MenuData } from './menu'
  6. import path from 'path'
  7. // 生成路由路径数组
  8. const generateRoutePaths = (menus: Array<MenuData>): string[] => {
  9. return menus.map(menu => menu.path)
  10. }
  11. // 白名单
  12. const whiteList = ['/:pathMatch(.*)*']
  13. // 生成可访问路由表
  14. const generateRoutes = (routes: Array<RouteRecordRaw>, routePaths: string[], basePath = '/') => {
  15. const routerData: Array<RouteRecordRaw> = []
  16. routes.forEach(route => {
  17. const routePath = path.resolve(basePath, route.path)
  18. if (route.children) { // 先看子路由 是否有匹配上的路由
  19. route.children = generateRoutes(route.children, routePaths, routePath)
  20. }
  21. // 如果当前路由子路由 数量大于0有匹配上 或 paths中包含当面路由path 就需要把当前父路由添加上
  22. if (routePaths.includes(routePath) || (route.children && route.children.length >= 1) || whiteList.includes(routePath)) {
  23. routerData.push(route)
  24. }
  25. })
  26. return routerData
  27. }
  28. const filterAsyncRoutes = (menus: Array<MenuData>, routes: Array<RouteRecordRaw>) => {
  29. // 生成要匹配的路由path数组
  30. const routePaths = generateRoutePaths(menus)
  31. // 生成匹配path的路由表
  32. const routerList = generateRoutes(routes, routePaths)
  33. return routerList
  34. }
  35. // 定义state类型
  36. export interface IPermissionState {
  37. routes: Array<RouteRecordRaw>;
  38. accessRoutes: Array<RouteRecordRaw>;
  39. }
  40. // mutations类型
  41. type IMutations = MutationTree<IPermissionState>
  42. // actions类型
  43. type IActions = ActionTree<IPermissionState, IRootState>
  44. // 定义state
  45. const state: IPermissionState = {
  46. routes: [],
  47. accessRoutes: []
  48. }
  49. // 定义mutation
  50. const mutations: IMutations = {
  51. SET_ROUTES(state, data: Array<RouteRecordRaw>) {
  52. state.routes = data
  53. },
  54. SET_ACCESS_ROUTES(state, data: Array<RouteRecordRaw>) {
  55. state.accessRoutes = data
  56. }
  57. }
  58. // 定义actions
  59. const actions: IActions = {
  60. generateRoutes({ dispatch }, type?: number) { // 1 针对菜单排序更新
  61. return new Promise((resolve, reject) => {
  62. let accessedRoutes: Array<RouteRecordRaw> = []
  63. if (store.getters.roleNames.includes('super_admin')) { // 超级管理员角色
  64. accessedRoutes = asyncRoutes
  65. dispatch('menu/getAllMenuListByAdmin', null, { root: true })
  66. resolve(accessedRoutes)
  67. } else { // 根据角色过滤菜单
  68. const roles = store.getters.roleIds
  69. dispatch('menu/getAccessByRoles', roles, { root: true }).then(menus => {
  70. if (type !== 1) { // 菜单重新排序 不需要再过次滤路由
  71. accessedRoutes = filterAsyncRoutes(menus, asyncRoutes)
  72. }
  73. resolve(accessedRoutes)
  74. }).catch(reject)
  75. }
  76. })
  77. }
  78. }
  79. // 定义user module
  80. const permission: Module<IPermissionState, IRootState> = {
  81. namespaced: true,
  82. state,
  83. mutations,
  84. actions
  85. }
  86. export default permission