Pārlūkot izejas kodu

feat: 菜单功能

Gaokun Wang 3 nedēļas atpakaļ
vecāks
revīzija
a927eae4dd

+ 8 - 1
src/api/interface/system/dict.ts

@@ -40,7 +40,7 @@ export interface DictBO {
 /**
  * 接收后端返回树信息
  */
-export interface DictTreeVO extends BaseEntity {
+export interface DictTreeVO {
   dictId: string
   parentId: string
   dictLabel: string
@@ -51,3 +51,10 @@ export interface DictTreeVO extends BaseEntity {
   remark?: string
   children: DictTreeVO[]
 }
+
+type TagType = 'primary' | 'success' | 'warning' | 'info' | 'danger'
+
+export interface DictItem {
+  dictValue: TagType // 限制 dictValue 只能是合法的 type 值
+  dictLabel: string
+}

+ 46 - 49
src/api/interface/system/menu.ts

@@ -1,70 +1,67 @@
 import { MenuTypeEnum } from '@/enums/MenuTypeEnum'
 
 /**
- * 菜单树形结构类型
- */
-export interface MenuTreeOption {
-  id: string | number
-  label: string
-  parentId: string | number
-  weight: number
-  children?: MenuTreeOption[]
-}
-
-export interface RoleMenuTree {
-  menus: MenuTreeOption[]
-  checkedKeys: string[]
-}
-
-/**
- * 菜单查询参数类型
+ * 查询参数
  */
 export interface MenuQuery {
-  keywords?: string
   menuName?: string
   status?: string
 }
 
 /**
- * 菜单视图对象类型
+ * 接收后端返回信息
  */
 export interface MenuVO extends BaseEntity {
-  parentName: string
-  parentId: string | number
-  children: MenuVO[]
-  menuId: string | number
-  menuName: string
+  menuId?: string
+  name: string
+  title?: string
+  parentId?: string
+  path?: string
+  icon?: string
+  component?: string
+  queryParam?: string
+  redirect?: string
+  menuType?: MenuTypeEnum
+  visible?: string
+  displayLayout?: string
+  isLink?: string
+  affix?: string
+  keepLive?: string
   orderNum: number
-  path: string
-  component: string
-  queryParam: string
-  isFrame: string
-  isCache: string
-  menuType: MenuTypeEnum
-  visible: string
-  status: string
-  icon: string
-  remark: string
+  remark?: string
 }
 
-export interface MenuForm {
-  parentName?: string
-  parentId?: string | number
-  children?: MenuForm[]
-  menuId?: string | number
-  menuName: string
-  orderNum: number
-  path: string
+/**
+ * 传入后端的表单信息
+ */
+export interface MenuBO {
+  menuId?: string
+  name: string
+  title?: string
+  parentId?: string
+  path?: string
+  icon?: string
   component?: string
   queryParam?: string
-  isFrame?: string
-  isCache?: string
-  menuType?: MenuTypeEnum
+  redirect?: string
+  menuType?: string
   visible?: string
+  displayLayout?: string
+  isLink?: string
+  affix?: string
+  keepLive?: string
+  orderNum: number
   status?: string
-  icon?: string
   remark?: string
-  query?: string
-  perms?: string
-  version?: number
+}
+
+/**
+ * 接收后端返回树信息
+ */
+export type MenuTreeVO = {
+  id?: string
+  menuId: string
+  parentId: string
+  title: string
+  children: MenuTreeVO[]
 }

+ 1 - 1
src/api/interface/system/org.ts

@@ -28,7 +28,7 @@ export interface OrgVO extends BaseEntity {
 /**
  * 组织树类型
  */
-export interface OrgTreeVO extends BaseEntity {
+export interface OrgTreeVO {
   id?: number | string
   orgId: string
   parentName?: string

+ 40 - 0
src/api/module/system/menu.ts

@@ -1,7 +1,47 @@
+import { MenuBO, MenuTreeVO, MenuVO } from '@/api/interface/system/menu'
 import http from '@/axios'
 class MenuApi {
   static getMenuList = (): Promise<ResultData<any>> => {
     return http.get<Menu.MenuOptions[]>({ url: '/system/menu/getRouters' })
   }
+
+  /**
+   * @name 查询树结构
+   * @returns returns
+   */
+  static tree = (): Promise<ResultData<any>> => {
+    return http.get<MenuTreeVO>({ url: '/system/menu/tree' })
+  }
+
+  /**
+   * @name 查询列表
+   * @returns returns
+   */
+  static list = (params: MenuBO): Promise<ResultData<any>> => {
+    return http.get<Menu.MenuOptions[]>({ url: '/system/menu/list', params })
+  }
+
+  /**
+   * @name 添加
+   * @returns returns
+   */
+  static add = (data: MenuBO): Promise<ResultData<any>> => {
+    return http.post({ url: '/system/menu/add', data })
+  }
+  /**
+   * @name 更新
+   * @returns returns
+   */
+  static edit = (data: MenuBO): Promise<ResultData<any>> => {
+    return http.post({ url: '/system/menu/edit', data })
+  }
+
+  /**
+   * @name 删除
+   * @returns returns
+   */
+  static delete = (data: string[]): Promise<ResultData<any>> => {
+    return http.delete({ url: '/system/menu/delete', data })
+  }
 }
 export default MenuApi

+ 4 - 4
src/enums/MenuTypeEnum.ts

@@ -2,14 +2,14 @@ export enum MenuTypeEnum {
   /**
    * 目录
    */
-  M = 'M',
+  C = 'C',
   /**
    * 菜单
    */
-  C = 'C',
+  M = 'M',
 
   /**
-   * 按钮
+   * 外链
    */
-  F = 'F'
+  L = 'L'
 }

+ 1 - 0
src/types/auto-components.d.ts

@@ -68,6 +68,7 @@ declare module 'vue' {
     HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
     IconChoose: typeof import('./../components/IconChoose/index.vue')['default']
     LoginForm: typeof import('./../views/login/components/LoginForm.vue')['default']
+    MenuDrawer: typeof import('./../views/system/menu/components/MenuDrawer.vue')['default']
     MenuItem: typeof import('./../layouts/components/AppMenu/MenuItem.vue')['default']
     MoreButton: typeof import('./../layouts/components/AppTabs/MoreButton.vue')['default']
     OrgDrawer: typeof import('./../views/system/org/components/OrgDrawer.vue')['default']

+ 1 - 8
src/views/demo/index.vue

@@ -1,13 +1,6 @@
 <template>
   <div class="table-box">
-    <ProTable
-      ref="proTableRef"
-      title="角色列表"
-      row-key="roleId"
-      :indent="20"
-      :columns="columns"
-      :search-columns="searchColumns"
-      :request-api="getTableList">
+    <ProTable ref="proTableRef" title="角色列表" row-key="roleId" :columns="columns" :search-columns="searchColumns" :request-api="getTableList">
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" icon="CirclePlus"> 新增角色 </el-button>

+ 1 - 8
src/views/demo/tree.vue

@@ -3,14 +3,7 @@
     <TreeFilter title="组织架构" :request-api="OrgApi.tree" label="name" :default-value="'0'" @change="changeTreeFilter" />
 
     <div class="table-box">
-      <ProTable
-        ref="proTableRef"
-        title="角色列表"
-        row-key="roleId"
-        :indent="20"
-        :columns="columns"
-        :search-columns="searchColumns"
-        :request-api="getTableList">
+      <ProTable ref="proTableRef" title="角色列表" row-key="roleId" :columns="columns" :search-columns="searchColumns" :request-api="getTableList">
         <!-- 表格 header 按钮 -->
         <template #tableHeader="scope">
           <el-button type="primary" icon="CirclePlus"> 新增角色 </el-button>

+ 1 - 8
src/views/system/config/index.vue

@@ -1,13 +1,6 @@
 <template>
   <div class="table-box">
-    <ProTable
-      ref="proTableRef"
-      title="参数配置"
-      row-key="configId"
-      :indent="20"
-      :columns="columns"
-      :search-columns="searchColumns"
-      :request-api="getTableList">
+    <ProTable ref="proTableRef" title="参数配置" row-key="configId" :columns="columns" :search-columns="searchColumns" :request-api="getTableList">
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" icon="CirclePlus" @click="openDrawer('新增')"> 新增 </el-button>

+ 3 - 3
src/views/system/dict/components/DictDrawer.vue

@@ -65,13 +65,13 @@
 </template>
 
 <script setup lang="ts" name="OrgForm">
-import { DictBO, DictTreeVO } from '@/api/interface/system/dict'
+import { DictBO, DictItem, DictTreeVO } from '@/api/interface/system/dict'
 import DictApi from '@/api/module/system/dict'
 import { ResultEnum } from '@/enums/HttpEnum'
 import { useDictOptions } from '@/hooks'
 
 const commonStatus = useDictOptions('COMMON_STATUS')
-const callbackShowStyles = useDictOptions('CALLBACK_SHOW_STYLE')
+const callbackShowStyles = useDictOptions('CALLBACK_SHOW_STYLE') as unknown as DictItem[]
 const dictTypes = useDictOptions('DICT_TYPE')
 
 import { FormInstance } from 'element-plus'
@@ -140,7 +140,7 @@ const handleConfirm = () => {
       const { code } = await drawerProps.value.api!(drawerProps.value.row)
       if (code == ResultEnum.SUCCESS) {
         ElMessage.success({ message: `${drawerProps.value.title}成功!` })
-        drawerProps.value.getTableList!()
+        // drawerProps.value.getTableList!()
         emit('submit')
         drawerVisible.value = false
       }

+ 1 - 1
src/views/system/dict/index.vue

@@ -13,7 +13,6 @@
         ref="proTableRef"
         title="字典数据"
         row-key="dictId"
-        :indent="20"
         :columns="columns"
         :init-param="initParam"
         :search-columns="searchColumns"
@@ -61,6 +60,7 @@ const changeTree = (val: string) => {
 // 刷新树表
 const refreshTree = () => {
   treeFilterRef.value?.refresh()
+  // reset()
 }
 
 // 表格配置项

+ 168 - 0
src/views/system/menu/components/MenuDrawer.vue

@@ -0,0 +1,168 @@
+<template>
+  <ElDrawer ref="elDrawerRef" v-model="drawerVisible" :title="drawerProps.title" v-bind="$attrs" destroy-on-close>
+    <template #header="scope">
+      <slot name="header" v-bind="scope">
+        <div style="display: flex">
+          <slot name="title">
+            <span style="flex: 1">{{ drawerProps.title }}</span>
+          </slot>
+        </div>
+      </slot>
+    </template>
+
+    <el-form ref="ruleFormRef" label-width="100px" label-suffix=" :" :rules="rules" :model="drawerProps.row" @submit.enter.prevent="handleConfirm">
+      <el-form-item label="上级" prop="parentId">
+        <el-tree-select
+          v-model="drawerProps.row.parentId"
+          :data="treeData"
+          check-strictly
+          placeholder="请选择上级"
+          :render-after-expand="false"
+          clearable
+          :default-expand-all="true"
+          :props="treeProps" />
+      </el-form-item>
+      <el-form-item label="菜单名称" prop="title">
+        <el-input v-model="drawerProps.row.title" placeholder="请填写名称" clearable />
+      </el-form-item>
+      <el-form-item label="菜单类型" prop="menuType">
+        <el-input v-model="drawerProps.row.menuType" placeholder="请填写名称" clearable />
+      </el-form-item>
+      <el-form-item label="图标" prop="icon">
+        <el-input v-model="drawerProps.row.icon" placeholder="请选择图标" clearable />
+      </el-form-item>
+      <el-form-item label="路由名称" prop="name">
+        <el-input v-model="drawerProps.row.name" placeholder="请填写路由名称" clearable />
+      </el-form-item>
+      <el-form-item label="路由地址" prop="path">
+        <el-input v-model="drawerProps.row.path" placeholder="请填写路由地址" clearable />
+      </el-form-item>
+      <el-form-item label="组件路径" prop="component">
+        <el-input v-model="drawerProps.row.component" placeholder="请填写组件路径" clearable />
+      </el-form-item>
+      <el-form-item label="显示状态" prop="visible">
+        <el-radio-group v-model="drawerProps.row.visible">
+          <el-radio-button :value="item.dictValue" v-for="(item, index) in commonStatus" :key="index" :label="item.dictLabel" />
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="是否全屏" prop="displayLayout">
+        <el-radio-group v-model="drawerProps.row.displayLayout">
+          <el-radio-button :value="item.dictValue" v-for="(item, index) in commonStatus" :key="index" :label="item.dictLabel" />
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="固定标签页" prop="affix">
+        <el-radio-group v-model="drawerProps.row.affix">
+          <el-radio-button :value="item.dictValue" v-for="(item, index) in commonStatus" :key="index" :label="item.dictLabel" />
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="是否缓存" prop="keepLive">
+        <el-radio-group v-model="drawerProps.row.keepLive">
+          <el-radio-button :value="item.dictValue" v-for="(item, index) in commonStatus" :key="index" :label="item.dictLabel" />
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="drawerProps.row.status">
+          <el-radio-button :value="item.dictValue" v-for="(item, index) in commonStatus" :key="index" :label="item.dictLabel" />
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="drawerProps.row.remark" placeholder="请填写备注" clearable />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <slot name="footer">
+        <ElButton @click="drawerVisible = false">取 消</ElButton>
+        <ElButton type="primary" @click="handleConfirm()">确 定</ElButton>
+      </slot>
+    </template>
+  </ElDrawer>
+</template>
+
+<script setup lang="ts" name="MenuForm">
+import { MenuBO } from '@/api/interface/system/menu'
+import MenuApi from '@/api/module/system/menu'
+import { ResultEnum } from '@/enums/HttpEnum'
+import { FormInstance } from 'element-plus'
+import { useDictOptions } from '@/hooks'
+import { MenuTreeVO } from '@/api/interface/system/menu'
+
+const commonStatus = useDictOptions('COMMON_STATUS')
+const rules = reactive({
+  account: [{ required: true, message: '请填写账号' }],
+  menuName: [{ required: true, message: '请填写姓名' }],
+  status: [{ required: true, message: '状态不能为空' }]
+})
+
+interface EcoDrawerProps {
+  api?: (params: any) => Promise<any> // 调用接口
+  title: string // 顶部标题
+  isView: boolean
+  row: Partial<MenuBO>
+  getTableList?: () => void
+}
+
+const treeData = ref<MenuTreeVO[]>([])
+const treeProps = {
+  label: 'title',
+  value: 'menuId'
+}
+
+const drawerVisible = ref(false)
+const drawerProps = ref<EcoDrawerProps>({
+  isView: false,
+  title: '',
+  row: {}
+})
+
+// emit
+const emit = defineEmits(['submit'])
+
+// 接收父组件传过来的参数
+const acceptParams = (params: EcoDrawerProps) => {
+  drawerProps.value = params
+  drawerVisible.value = true
+  loadTree()
+  drawerProps.value.row.status = drawerProps.value.row.status || '1'
+}
+
+const loadTree = () => {
+  MenuApi.tree().then(res => {
+    treeData.value = [
+      {
+        parentId: '-1',
+        menuId: '0',
+        title: '顶层',
+        children: res.data
+      }
+    ]
+  })
+}
+
+// 提交数据(新增/编辑)
+const ruleFormRef = ref<FormInstance>()
+const handleConfirm = () => {
+  ruleFormRef.value!.validate(async valid => {
+    if (!valid) return
+    try {
+      console.log(drawerProps.value.row)
+
+      const { code } = await drawerProps.value.api!(drawerProps.value.row)
+      if (code == ResultEnum.SUCCESS) {
+        ElMessage.success({ message: `${drawerProps.value.title}成功!` })
+        drawerProps.value.getTableList!()
+        emit('submit')
+        drawerVisible.value = false
+      }
+    } catch (error) {
+      console.log(error)
+    }
+  })
+}
+
+defineExpose({
+  acceptParams
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 99 - 1
src/views/system/menu/index.vue

@@ -1,3 +1,101 @@
 <template>
-  <div>菜单</div>
+  <div class="table-box">
+    <ProTable
+      ref="proTableRef"
+      title="菜单管理"
+      row-key="menuId"
+      :pagination="false"
+      :indent="20"
+      stripe
+      :columns="columns"
+      :request-api="getTableList"
+      :default-expand-all="defaultExpandAllKey">
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader>
+        <el-button type="primary" icon="CirclePlus" @click="openDrawer('新增')"> 新增 </el-button>
+        <el-button type="info" icon="Sort" @click="changeExpand"> 展开/折叠 </el-button>
+      </template>
+      <!-- 图标 -->
+      <template #icon="scope">
+        <el-icon :size="18" v-if="scope.row.meta.icon">
+          <SvgIcon v-if="scope.row.meta.icon.startsWith('svg-')" :name="scope.row.meta.icon.substring(4)" />
+          <component v-else :is="scope.row.meta.icon" />
+        </el-icon>
+      </template>
+
+      <template #operation="{ row }">
+        <el-button type="primary" link icon="EditPen" @click="openDrawer('编辑', row)"> 编辑 </el-button>
+        <el-button v-if="row.isLock !== '1'" type="primary" link icon="Delete" @click="deleteRow(row)"> 删除 </el-button>
+      </template>
+    </ProTable>
+    <MenuDrawer ref="drawerRef" size="40%" />
+  </div>
 </template>
+<script lang="tsx" setup name="MenuManage">
+import MenuApi from '@/api/module/system/menu'
+import MenuDrawer from './components/MenuDrawer.vue'
+import { ColumnProps, ProTableInstance } from '@/components/ProTable/interface'
+import { useDictOptions, useHandleData } from '@/hooks'
+import { MenuBO, MenuVO } from '@/api/interface/system/menu'
+const defaultExpandAllKey = ref(true)
+
+const proTableRef = ref<ProTableInstance>()
+
+// 表格配置项
+const columns: ColumnProps<Menu.MenuOptions[]>[] = [
+  { type: 'index', width: 60, label: '序号' },
+  { prop: 'meta.title', label: '名称', align: 'left', width: 150 },
+  {
+    prop: 'menuType',
+    label: '类型',
+    tag: true,
+    enum: useDictOptions('MENU_TYPE'),
+    width: 80,
+    fieldNames: { label: 'dictLabel', value: 'dictValue', tagType: 'callbackShowStyle' }
+  },
+  { prop: 'meta.icon', label: '图标', width: 100 },
+  {
+    prop: 'status',
+    label: '状态',
+    tag: true,
+    enum: useDictOptions('COMMON_STATUS'),
+    width: 80,
+    fieldNames: { label: 'dictLabel', value: 'dictValue', tagType: 'callbackShowStyle' }
+  },
+  { prop: 'name', label: '路由名称' },
+  { prop: 'path', label: '路由地址' },
+  { prop: 'component', label: '组件路径' },
+  { prop: 'orderNum', label: '排序' },
+  { prop: 'remark', label: '备注' },
+  { prop: 'createByName', label: '创建人' },
+  { prop: 'createTime', label: '创建时间' },
+  { prop: 'operation', label: '操作', width: 250, fixed: 'right' }
+]
+
+const changeExpand = () => {
+  defaultExpandAllKey.value = !defaultExpandAllKey.value
+  proTableRef.value?.refresh()
+}
+
+// 获取table列表
+const getTableList = (params: MenuBO) => MenuApi.list(params)
+
+// 单行删除
+const deleteRow = async (row: MenuVO) => {
+  await useHandleData(MenuApi.delete, [row.menuId], `删除【${row.title}】数据`)
+  proTableRef.value?.getTableList()
+}
+
+// 打开 drawer(新增、查看、编辑)
+const drawerRef = ref<InstanceType<typeof MenuDrawer> | null>(null)
+const openDrawer = (title: string, row: Partial<MenuBO> = {}) => {
+  const params = {
+    title,
+    row: { ...row },
+    isView: title === '查看',
+    api: title === '新增' ? MenuApi.add : title === '编辑' ? MenuApi.edit : undefined,
+    getTableList: proTableRef.value?.getTableList
+  }
+  drawerRef.value?.acceptParams(params)
+}
+</script>

+ 2 - 3
src/views/system/org/index.vue

@@ -6,7 +6,6 @@
         ref="proTableRef"
         title="组织列表"
         row-key="roleId"
-        :indent="20"
         :columns="columns"
         :init-param="initParam"
         :search-columns="searchColumns"
@@ -27,7 +26,7 @@
   </div>
 </template>
 <script lang="tsx" setup name="OrgManage">
-import { RoleInfo, RoleQuery } from '@/api/interface/system/role'
+import { RoleVO, RoleQuery } from '@/api/interface/system/role'
 import OrgApi from '@/api/module/system/org'
 import OrgDrawer from './components/OrgDrawer.vue'
 import TreeFilter from '@/components/TreeFilter/index.vue'
@@ -54,7 +53,7 @@ const refreshTree = () => {
 }
 
 // 表格配置项
-const columns: ColumnProps<RoleInfo>[] = [
+const columns: ColumnProps<RoleVO>[] = [
   { type: 'selection', width: 60 },
   { prop: 'name', label: '组织名称' },
   { prop: 'orderNum', label: '排序' },

+ 0 - 1
src/views/system/position/index.vue

@@ -12,7 +12,6 @@
         ref="proTableRef"
         title="组织列表"
         row-key="positionId"
-        :indent="20"
         :columns="columns"
         :init-param="initParam"
         :search-columns="searchColumns"

+ 1 - 8
src/views/system/role/index.vue

@@ -1,13 +1,6 @@
 <template>
   <div class="table-box">
-    <ProTable
-      ref="proTableRef"
-      title="参数配置"
-      row-key="roleId"
-      :indent="20"
-      :columns="columns"
-      :search-columns="searchColumns"
-      :request-api="getTableList">
+    <ProTable ref="proTableRef" title="参数配置" row-key="roleId" :columns="columns" :search-columns="searchColumns" :request-api="getTableList">
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" icon="CirclePlus" @click="openDrawer('新增')"> 新增 </el-button>

+ 0 - 1
src/views/system/user/index.vue

@@ -6,7 +6,6 @@
         ref="proTableRef"
         title="组织列表"
         row-key="userId"
-        :indent="20"
         :columns="columns"
         :init-param="initParam"
         :search-columns="searchColumns"