Vue3+Vite+TS后台项目 ~ 9.权限管理_规则

日拱一卒无有尽, 功不唐捐终入海


一、接口封装

新建 src / api / menu.ts 文件

import request from '@/utils/request'
import { IFormData } from './types/form'
import type { Menu } from './types/menu'

// 获取权限列表
export const getMenus = (params: {
  is_show: 0 | 1 | ''
  keyword: string
}) => {
  return request<Menu[]>({
    method: 'GET',
    url: '/setting/menus',
    params
  })
}

// 添加权限
export const createMenu = (data: {
  path: number[]
} & Omit<Menu, 'id' | 'children' | 'is_del' | 'path'>) => {
  return request({
    method: 'POST',
    url: '/setting/menus',
    data
  })
}

// 获取添加权限规则表单
export const getMenuTree = () => {
  return request<IFormData>({
    method: 'GET',
    url: '/setting/menus/create'
  }).then(data => {
    const findData = data.rules.find(item => item.field === 'menu_list')
    return (findData && findData.props && findData.props.data) || []
  })
}

// 修改权限规则
export const updateMenu = (id: number, data: { path: number[] } & Omit<Menu, 'id' | 'children' | 'is_del' | 'path'>) => {
  return request({
    method: 'PUT',
    url: `/setting/menus/${id}`,
    data
  })
}

// 删除权限
export const deleteMenu = (id: number) => {
  return request({
    method: 'DELETE',
    url: `/setting/menus/${id}`
  })
}

// 获取单个规则
export const getMenu = (id: number) => {
  return request<{
    path: number[]
  } & Omit<Menu, 'path'>>({
    method: 'GET',
    url: `/setting/menus/${id}`
  })
}

// 修改状态
export const updateMenuStatus = (id: number, isShow: 0 | 1) => {
  return request({
    method: 'PUT',
    url: `/setting/menus/show/${id}`,
    data: {
      is_show: isShow
    }
  })
}

新建 src / api / types / menu.ts 文件

export interface Menu {
  id: number
  pid: number
  icon: string
  menu_name: string
  module: string
  controller: string
  action: string
  api_url: string
  methods: string
  params: string
  sort: number
  is_show: 0 | 1
  is_show_path: number
  access: number
  menu_path: string
  path: string
  auth_type: 1 | 2
  header: string
  is_header: number
  unique_auth: string
  is_del: number
  statusLoading?: boolean
  children: Menu[]
}

二、折叠表格插件

Vxe-table 官方文档

// 安装(右上角选择 vue3 版本(V4)
 npm install xe-utils@3 vxe-table@next

全局引用:
编辑 src / main.ts 文件

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { store, key } from './store'
import elementPlus from './plugins/element-plus'
import 'xe-utils'
import VXETable from 'vxe-table'

// 全局样式
import './styles/index.scss'
import 'vxe-table/lib/style.css'

createApp(App)
  .use(router)
  .use(store, key)
  .use(elementPlus, { size: 'small', zIndex: 2000 })
  .use(VXETable)
  .mount('#app')

三、规则列表

新建 src / views / setting / permission / rule / index.vue 文件

<template>
  <!-- <page-container> -->
  <el-card>
    <template #header>
      数据筛选
    </template>
    <el-form
      :inline="true"
      ref="form"
      :model="listParams"
      :disabled="listLoading"
      @submit.prevent="handleQuery"
    >
      <el-form-item label="状态">
        <el-select
          v-model="listParams.is_show"
          placeholder="请选择"
          clearable
        >
          <el-option
            label="全部"
            value=""
          />
          <el-option
            label="显示"
            :value="1"
          />
          <el-option
            label="不显示"
            :value="0"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="规则名称">
        <el-input
          v-model="listParams.keyword"
          clearable
          placeholder="请输入规则名称"
        />
      </el-form-item>
      <el-form-item>
        <el-button native-type="submit">
          查询
        </el-button>
      </el-form-item>
    </el-form>
  </el-card>
  <el-card>
    <template #header>
      <el-button
        type="primary"
        @click="formVisible = true"
      >
        添加规则
      </el-button>
    </template>
    <!--
        启用树菜单:
          1. data 数据需要是树结构
          2. 给 vxe-table 组件设置 row-id
          3. 给 vxe-column 设置 tree-node
       -->
    <vxe-table
      :data="list"
      row-id="id"
      :tree-config="{ children: 'children' }"
      v-loading="listLoading"
    >
      <vxe-column
        field="id"
        title="ID"
      />
      <vxe-column
        field="menu_name"
        title="名称"
        tree-node
      />
      <vxe-column
        title="接口路径"
      >
        <template #default="{ row }">
          {{ row.api_url ? `[${row.methods}] ${row.api_url}` : '' }}
        </template>
      </vxe-column>
      <vxe-column
        field="unique_auth"
        title="前端权限"
      />
      <vxe-column
        field="menu_path"
        title="页面路由"
      />
      <vxe-column title="状态">
        <template #default="{ row }">
          <el-switch
            v-model="row.is_show"
            active-color="#13ce66"
            inactive-color="#ff4949"
            :active-value="1"
            :inactive-value="0"
            :loading="row.statusLoading"
            @change="handleStatusChange(row)"
          />
        </template>
      </vxe-column>
      <vxe-column
        title="操作"
        min-width="100"
      >
        <template #default="scope">
          <el-button
            type="text"
            @click="handleCreate(scope.row.id)"
          >
            添加规则
          </el-button>
          <el-button
            type="text"
            @click="handleUpdate(scope.row.id)"
          >
            编辑
          </el-button>
          <el-popconfirm
            title="确认删除吗?"
            @confirm="handleDelete(scope.row.id)"
          >
            <template #reference>
              <el-button type="text">
                删除
              </el-button>
            </template>
          </el-popconfirm>
        </template>
      </vxe-column>
    </vxe-table>
  </el-card>
  <!-- </page-container> -->
  <rule-form
    v-model="formVisible"
    v-model:rule-id="ruleId"
    v-model:pid="pid"
    @success="handleFormSuccess"
  />
</template>

<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import { getMenus, deleteMenu, updateMenuStatus } from '@/api/menu'
import type { Menu } from '@/api/types/menu'
import { ElMessage } from 'element-plus'
import RuleForm from './RuleForm.vue'

const list = ref<Menu[]>([]) // 列表数据
const listLoading = ref(true)
const listParams = reactive({ // 列表数据查询参数
  keyword: '',
  is_show: '' as 0 | 1 | ''
})
const formVisible = ref(false)
const ruleId = ref<number | null>(null)
const pid = ref<number | null>(null)

onMounted(() => {
  loadList()
})

const loadList = async () => {
  listLoading.value = true
  const data = await getMenus(listParams).finally(() => {
    listLoading.value = false
  })
  data.forEach(item => {
    item.statusLoading = false // 控制切换状态的 loading 效果
  })
  list.value = data
}

const handleQuery = async () => {
  loadList()
}

const handleDelete = async (id: number) => {
  await deleteMenu(id)
  ElMessage.success('删除成功')
  loadList()
}

const handleStatusChange = async (item: Menu) => {
  item.statusLoading = true
  await updateMenuStatus(item.id, item.is_show).finally(() => {
    item.statusLoading = false
  })
  ElMessage.success(`${item.is_show === 1 ? '启用' : '禁用'}成功`)
}

const handleUpdate = (id: number) => {
  ruleId.value = id
  formVisible.value = true
}

const handleFormSuccess = () => {
  formVisible.value = false
  loadList()
}

const handleCreate = (id: number) => {
  pid.value = id
  formVisible.value = true
}

</script>

<style lang="scss" scoped></style>

四、新建 / 编辑 弹窗

新建 src / views / setting / permission / rule / RuleForm.vue 文件

<template>
  <!-- :confirm="handleSubmit" -->
  <el-dialog
    ref="dialog"
    :title="props.ruleId ? '编辑规则' : '添加规则'"
    width="60%"
    append-to-body
    @closed="handleDialogClosed"
    @open="handleDialogOpen"
  >
    <el-form
      label-width="110px"
      v-loading="formLoading"
      :model="formData"
      :rules="formRules"
      ref="form"
      :validate-on-rule-change="false"
    >
      <el-row>
        <el-col :span="24">
          <el-form-item label="类型">
            <el-radio-group v-model="formData.auth_type">
              <el-radio :label="2">
                接口
              </el-radio>
              <el-radio :label="1">
                菜单(只显示三级)
              </el-radio>
            </el-radio-group>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item
            label="名称"
            prop="menu_name"
          >
            <el-input v-model="formData.menu_name" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="父级分类"
            prop="path"
          >
            <el-cascader
              v-model="formData.path"
              :options="menus"
              clearable
              :props="{ checkStrictly: true }"
              @change="handleChange"
            />
          </el-form-item>
        </el-col>
        <template v-if="formData.auth_type === 2">
          <el-col :span="12">
            <el-form-item
              label="请求方式"
              prop="methods"
            >
              <el-select v-model="formData.methods">
                <el-option
                  v-for="item in methods"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item
              label="接口地址"
              prop="api_url"
            >
              <el-input v-model="formData.api_url" />
            </el-form-item>
          </el-col>
        </template>
        <template v-else>
          <el-col :span="12">
            <el-form-item
              label="接口参数"
              prop="params"
            >
              <el-input v-model="formData.params" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item
              label="路由路径"
              prop="menu_path"
            >
              <el-input v-model="formData.menu_path" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item
              label="图标"
              prop="icon"
            >
              <el-input v-model="formData.icon" />
            </el-form-item>
          </el-col>
        </template>
        <el-col :span="12">
          <el-form-item
            label="权限标识"
            prop="unique_auth"
          >
            <el-input v-model="formData.unique_auth" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="排序"
            prop="sort"
          >
            <el-input v-model.number="formData.sort" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="状态"
            prop="is_show"
          >
            <el-radio-group v-model="formData.is_show">
              <el-radio :label="0">
                关闭
              </el-radio>
              <el-radio :label="1">
                开启
              </el-radio>
            </el-radio-group>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="是否展示"
            prop="is_show_path"
          >
            <el-radio-group v-model="formData.is_show_path">
              <el-radio :label="0"></el-radio>
              <el-radio :label="1"></el-radio>
            </el-radio-group>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="handleCancel">取消</el-button>
        <el-button
          type="primary"
          @click="handleSubmit"
        >确认</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script lang="ts" setup>
import { computed, ref } from 'vue'
import * as menuApi from '@/api/menu'
import { ElMessage } from 'element-plus'
import type { Menu } from '@/api/types/menu'
import type { PropType } from 'vue'
import type { IElForm, IElDialog } from '@/types/element-plus'

const props = defineProps({
  ruleId: {
    type: Number as PropType<number | null>,
    default: null
  },
  pid: {
    type: Number as PropType<number | null>,
    default: null
  }
})
const emit = defineEmits(['success', 'update:rule-id', 'update:pid'])

const form = ref<IElForm | null>(null)
const formData = ref<{ path: number[] } & Omit<Menu, 'id' | 'children' | 'is_del' | 'path'>>({
  auth_type: 1,
  menu_name: '',
  pid: 0,
  params: '',
  controller: '',
  module: '',
  action: '',
  icon: '',
  path: [],
  menu_path: '',
  api_url: '',
  methods: '',
  unique_auth: '',
  header: '',
  is_header: 0,
  sort: 0,
  access: 0,
  is_show: 0,
  is_show_path: 1
})

const menuRules = {
  menu_path: [
    { message: '请输入路由路径', required: true, trigger: 'change' }
  ],
  unique_auth: [
    { message: '请输入权限标识', required: true, trigger: 'change' }
  ]
}

const apiRules = {
  methods: [
    { message: '请选择请求方式', required: true, trigger: 'change' }
  ],
  api_url: [
    { message: '请输入接口地址', required: true, trigger: 'change' }
  ]
}

const commonRules = {
  menu_name: [
    { message: '请输入按钮名称', required: true, trigger: 'change' }
  ]
}

const formRules = computed(() => {
  // 清除验证结果
  form.value?.clearValidate()
  return formData.value.auth_type === 1
    ? { ...menuRules, ...commonRules }
    : { ...apiRules, ...commonRules }
})

const menus = ref<Menu[]>([])

const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].map(item => ({
  label: item,
  value: item
}))

const formLoading = ref(false)

const handleDialogOpen = async () => {
  formLoading.value = true
  await Promise.all([
    loadMenus(),
    loadMenu(),
    setDefaultMenuPath()
  ]).finally(() => {
    formLoading.value = false
  })
}

const loadMenus = async () => {
  const data = await menuApi.getMenuTree()
  menus.value = data
}

const setDefaultMenuPath = async () => {
  if (props.pid) {
    const menu = await menuApi.getMenu(props.pid)
    formData.value.pid = props.pid
    formData.value.path = [...menu.path, props.pid]
  }
}

const loadMenu = async () => {
  if (!props.ruleId) {
    return
  }
  const data = await menuApi.getMenu(props.ruleId)
  data.path = [
    ...data.path,
    data.id
  ]
  formData.value = data
}

const handleChange = (value: any) => {
  formData.value.pid = value[value.length - 1]
}

const handleSubmit = async () => { // 确认
  const valid = await form.value?.validate()
  if (!valid) return
  if (props.ruleId) {
    await menuApi.updateMenu(props.ruleId, formData.value)
  } else {
    await menuApi.createMenu(formData.value)
  }
  ElMessage.success('保存成功')
  emit('success')
}

const dialog = ref<IElDialog | null>(null)

const handleCancel = () => { // 取消
  if (dialog.value) {
    dialog.value.visible = false
  }
}

const handleDialogClosed = () => {
  emit('update:rule-id', null)
  emit('update:pid', null)
  form.value?.clearValidate() // 清除验证结果
  form.value?.resetFields() // 清除表单数据
}
</script>

<style lang="scss" scoped></style>

五、页面展示

在这里插入图片描述




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

后海 0_o

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值