Vue3+Vite+Ts 项目实战 06 自动注册全局组件、封装分页和 Dialog 组件、权限管理

创建全局组件

页面容器组件

<!-- src\components\PageContainer\index.vue -->
<template>
  <el-space
    alignment="flex-start"
    class="content-space"
    direction="vertical"
    prefix-cls="content-space"
    size="large"
  >
    <slot />
  </el-space>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped>
.content-space {
      
      
  display: flex;
  :deep(.content-space__item) {
      
      
    width: 100%;
  }
  :deep(.content-space__item:last-child) {
      
      
    padding-bottom: unset !important;
  }
}
</style>

Card 组件

<!-- src\components\Card\index.vue -->
<template>
  <el-card>
    <template #header>
      <el-space wrap>
        <slot name="header" />
      </el-space>
    </template>
    <el-space
      alignment="flex-end"
      class="content-space"
      direction="vertical"
      prefix-cls="content-space"
      size="large"
    >
      <slot />
    </el-space>
  </el-card>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped>
.content-space {
      
      
  display: flex;
  :deep(.content-space__item) {
      
      
    width: 100%;
  }
}
</style>

自动注册全局组件

Vite 可以通过 Glob 函数从文件系统导入多个模块,为每个匹配的文件生成 import() 代码。

可以在每个组件目录下创建注册脚本,通过 Glob 导入这些模块,遍历执行:

// src\components\PageContainer\install.ts
import {
    
     App } from 'vue'
import Component from './index.vue'

export default {
    
    
  install(app: App) {
    
    
    app.component('PageContainer', Component)
  }
}

// src\components\Card\install.ts
import {
    
     App } from 'vue'
import Component from './index.vue'

export default {
    
    
  install(app: App) {
    
    
    app.component('AppCard', Component)
  }
}

// src\components\install.ts
/*  统一注册 components 目录下的全部组件 */
import {
    
     App } from 'vue'

export default {
    
    
  install: (app: App) => {
    
    
    // 引入所有组件下的安装模块
    const modules = import.meta.globEager('./**/install.ts')

    for (const path in modules) {
    
    
      app.use(modules[path].default)
    }
  }
}

// src\main.ts
...
import componentsInstall from '@/components/install'

...

(window as any).vm = createApp(App)
  .use(router)
  .use(createPinia())
  .use(elementPlus)
  // 自动注册全部本地组件
  .use(componentsInstall)
  .mount('#app')

封装分页组件组件

Vue3 提供 v-model:prop 语法进行双向绑定,和默认的属性变更监听事件 update:prop,可以用于绑定并监听封装组件的分页 page 和每页条数 limit

Element Plus 未来将会删除分页组件的 size-change current-change 事件,建议改为监听属性的 update:prop 事件。

Vue 3 中可以直接使用 TypeScript 语法声明 props 和 emits,不过定义 props 默认值还需要使用 withDefaults 编译器宏,详细参考《仅限类型的 props/emit 声明》

<!-- src\components\Pagination\index.vue -->
<template>
  <el-pagination
    :current-page="props.page"
    :page-size="props.limit"
    :page-sizes="[10, 20, 30, 40, 50, 100]"
    background
    layout="total, sizes, prev, pager, next, jumper"
    :total="props.listCount"
    @update:page-size="handleSizeChange"
    @update:current-page="handleCurrentChange"
  />
</template>

<script setup lang="ts">
import {
      
       PropType } from 'vue'

/* 使用 TypeScript 纯类型语法声明 props 和默认值 */
/*
// 使用 TS 方式声明 props
interface PropsType {
  page: number
  limit: number
  listCount: number
  loadList: () => void
}

// 定义 props 默认值
const props = withDefaults(defineProps<PropsType>(), {
  page: 1,
  limit: 10,
  listCount: 0,
  loadList: () => {}
})
*/

/* 使用运行时声明 */
/* 这种方式声明 props 也支持类型声明,并且在使用默认值的情况下使用这种方式还直观些 */
const props = defineProps({
      
      
  // 页码
  page: {
      
      
    type: Number,
    default: 1
  },
  // 每页条数
  limit: {
      
      
    type: Number,
    default: 1
  },
  // 数据总条数
  listCount: {
      
      
    type: Number,
    default: 1
  },
  // 页码/每页条数变更触发的方法
  loadList: {
      
      
    type: Function as PropType<() => void>,
    default: () => {
      
      }
  }
})

/* 使用 TypeScript 纯类型语法声明 emits */
interface EmitsType {
      
      
  (e: 'update:page', page: number): void
  (e: 'update:limit', size: number): void
}

const emit = defineEmits<EmitsType>()

/* 使用运行时声明 */
/*
const emit = defineEmits(['update:page', 'update:limit'])
*/

// elementPlus 将在未来删除 size-change current-change 事件
// 建议改为监听 update 事件
const handleCurrentChange = (page: number) => {
      
      
  emit('update:page', page)
  props.loadList()
}

const handleSizeChange = (size: number) => {
      
      
  emit('update:page', 1)
  emit('update:limit', size)
  props.loadList && props.loadList()
}

</script>

<style lang="scss" scoped>
.el-pagination {
      
      
  display: flex;
  justify-content: flex-end;
}
</style>

// src\components\Pagination\install.ts
import {
    
     App } from 'vue'
import Component from './index.vue'

export default {
    
    
  install(app: App) {
    
    
    app.component('AppPagination', Component)
  }
}

封装 Dialog 对话框组件

<!-- src\components\Dialog\index.vue -->
<template>
  <el-dialog
    ref="dialogRef"
    width="50%"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
  >
    <slot />
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="handleCancel">取 消</el-button>
        <el-button
          type="primary"
          :loading="confirmLoading"
          @click="handleConfirm"
        >确 定</el-button>
      </span>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import type {
      
       PropType } from 'vue'
import {
      
       ElDialogType } from '@/types/element-plus'

const props = defineProps({
      
      
  confirm: {
      
      
    type: Function as PropType<() => Promise<void>>,
    default: () => Promise.resolve()
  }
})

const dialogRef = ref<ElDialogType>()
const confirmLoading = ref(false)

const handleCancel = () => {
      
      
  if (dialogRef.value) {
      
      
    dialogRef.value.visible = false
  }
}

const handleConfirm = async () => {
      
      
  confirmLoading.value = true
  await props.confirm().finally(() => {
      
      
    confirmLoading.value = false
  })
}
</script>

<style scoped></style>

// src\components\Dialog\install.ts
import {
    
     App } from 'vue'
import Component from './index.vue'

export default {
    
    
  install(app: App) {
    
    
    app.component('AppDialog', Component)
  }
}

添加 ElDialog 组件类型定义:

// src\types\element-plus.d.ts
import {
    
     ElDialog } from 'element-plus'

export type ElDialogType = InstanceType<typeof ElDialog>

配置权限管理路由

// src\router\modules\permission.ts
import {
    
     RouteRecordRaw, RouterView } from 'vue-router'

const routes:RouteRecordRaw = {
    
    
  path: 'permission',
  component: RouterView,
  meta: {
    
    
    title: '权限管理'
  },
  children: [
    {
    
    
      path: 'admin',
      name: 'permission_admin',
      component: () => import('@/views/permission/admin/index.vue'),
      meta: {
    
    
        title: '管理员'
      }
    },
    {
    
    
      path: 'role',
      name: 'permission_role',
      component: () => import('@/views/permission/role/index.vue'),
      meta: {
    
    
        title: '角色'
      }
    },
    {
    
    
      path: 'menu',
      name: 'permission_menu',
      component: () => import('@/views/permission/menu/index.vue'),
      meta: {
    
    
        title: '菜单'
      }
    }
  ]
}

export default routes

配置权限管理菜单

<!-- src\layout\components\AppMenu.vue -->
<template>
  <el-menu
    ...
  >
    ...
    <el-sub-menu index="2">
      <template #title>
        <el-icon><location /></el-icon>
        <span>权限管理</span>
      </template>
      <el-menu-item index="/permission/admin">
        <el-icon><Menu /></el-icon>
        <span>管理员</span>
      </el-menu-item>
      <el-menu-item index="/permission/role">
        <el-icon><Menu /></el-icon>
        <span>角色</span>
      </el-menu-item>
      <el-menu-item index="/permission/menu">
        <el-icon><Menu /></el-icon>
        <span>菜单</span>
      </el-menu-item>
    </el-sub-menu>
  </el-menu>
</template>

...

管理员页面

相关接口

// src\api\admin.ts
// 管理员相关
import request from '@/utils/request'
import {
    
     ListParams, Admin, AdminPostData } from '@/api/types/admin'

// 获取管理员列表
export const getAdmins = (params: ListParams) => {
    
    
  return request<{
    
    
    count: number
    list: Admin[]
  }>({
    
    
    method: 'GET',
    url: '/setting/admin',
    params
  })
}

// 添加管理员
export const createAdmin = (data: AdminPostData) => {
    
    
  return request({
    
    
    method: 'POST',
    url: '/setting/admin',
    data
  })
}

// 修改管理员信息
export const updateAdmin = (id: number, data: AdminPostData) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/admin/${
      
      id}`,
    data
  })
}

// 获取管理员信息
export const getAdmin = (id: number) => {
    
    
  return request<Admin>({
    
    
    method: 'GET',
    url: `/setting/admin/${
      
      id}`
  })
}

// 删除管理员
export const deleteAdmin = (id: number) => {
    
    
  return request({
    
    
    method: 'DELETE',
    url: `/setting/admin/${
      
      id}`
  })
}

// 修改管理员状态
export const updateAdminStatus = (id: number, status: number) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/admin/${
      
      id}/set_status/${
      
      status}`
  })
}

// 修改管理员密码
export const updateAdminPassword = (id: number, data: {
     
     
  pwd: string,
  pwdConfirm: string
}) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/admin/${
      
      id}/set_password`,
    data
  })
}

TS 类型定义

// src\api\types\admin.ts
export interface ListParams {
    
    
  page?: number
  limit?: number
  name?: string
  status?: 0 | 1 | ''
}

export interface Admin {
    
    
  id: number
  account: string
  realName: string
  roles: ({
    
    name: string, id: number})[]
  status: 0 | 1
  isDel: 0 | 1
  _lastIp: string
  _lastTime: string
  _addTime: string
  _updateTime: string
  statusLoading?: boolean
}

export interface AdminPostData {
    
    
  id?: number
  account: string
  pwd?: string
  pwdConfirm?: string
  realName: string
  roles: number[]
  status: 0 | 1
}

列表页

<!-- src\views\permission\admin\index.vue -->
<template>
  <page-container>
    <app-card>
      <template #header>
        数据筛选
      </template>

      <el-form
        ref="formRef"
        :model="listParams"
        :disabled="listLoading"
        inline
        @submit.prevent="handleQuery"
      >
        <el-form-item
          label="状态"
        >
          <el-select
            v-model="listParams.status"
            placeholder="请选择"
            clearable
          >
            <el-option
              label="启用"
              :value="1"
            />
            <el-option
              label="禁用"
              :value="0"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="搜索">
          <el-input
            v-model="listParams.name"
            clearable
            placeholder="请输入姓名或者账号"
          />
        </el-form-item>
        <el-form-item>
          <el-button native-type="submit">
            查询
          </el-button>
        </el-form-item>
      </el-form>
    </app-card>

    <app-card>
      <template #header>
        <el-button
          type="primary"
          @click="formVisible = true"
        >
          添加管理员
        </el-button>
      </template>

      <el-table
        :data="list"
        stripe
        style="width: 100%"
        v-loading="listLoading"
      >
        <el-table-column
          prop="realName"
          label="姓名"
        />
        <el-table-column
          prop="account"
          label="账号"
        />
        <el-table-column
          label="角色"
          min-width="180"
        >
          <template #default="scope">
            <el-space wrap>
              <el-tag
                v-for="item in scope.row.roles"
                :key="item.id"
              >
                {
   
   { item.name }}
              </el-tag>
            </el-space>
          </template>
        </el-table-column>
        <el-table-column
          prop="_lastTime"
          label="最后一次登录时间"
          min-width="180"
        />
        <el-table-column
          prop="_lastIp"
          label="最后一次登录IP"
          min-width="180"
        />
        <el-table-column
          label="状态"
        >
          <template #default="scope">
            <el-switch
              v-model="scope.row.status"
              active-color="#13ce66"
              inactive-color="#ff4949"
              :active-value="1"
              :inactive-value="0"
              :loading="scope.row.statusLoading"
              @change="handleStatusChange(scope.row)"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          fixed="right"
          min-width="100"
          align="center"
        >
          <template #default="scope">
            <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>
        </el-table-column>
      </el-table>

      <app-pagination
        v-model:page="listParams.page"
        v-model:limit="listParams.limit"
        :list-count="listCount"
        :load-list="loadList"
        :disabled="listLoading"
      />
    </app-card>
  </page-container>

  <admin-form
    v-model="formVisible"
    v-model:admin-id="adminId"
    @success="handleFormSuccess"
  />
</template>

<script setup lang="ts">
import {
      
       deleteAdmin, getAdmins, updateAdminStatus } from '@/api/admin'
import {
      
       Admin, ListParams } from '@/api/types/admin'
import {
      
       ElMessage } from 'element-plus'
import AdminForm from './AdminForm.vue'

const list = ref<Admin[]>([]) // 列表数据
const listCount = ref(0) // 总条数
const listLoading = ref(true)
const listParams = reactive({
      
      
  page: 1, // 当前页码
  limit: 10, // 每页条数
  name: '', // 姓名或账号
  // vue3 自动推断 status 为 string 类型,与 interface 定义的联合类型不一致,所以要手动断言一下
  status: '' as ListParams['status'] // 状态
}) // 列表数据查询参数
const formVisible = ref(false) // 编辑对话框显示控制器
const adminId = ref<number>()

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

const loadList = async () => {
      
      
  listLoading.value = true
  const data = await getAdmins(listParams).finally(() => {
      
      
    listLoading.value = false
  })

  // 添加修改状态 loading 控制器
  data.list.forEach(item => {
      
      
    item.statusLoading = false
  })

  list.value = data.list
  listCount.value = data.count
}

const handleQuery = () => {
      
      
  // 默认从第一页开始查询
  listParams.page = 1
  loadList()
}

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

const handleStatusChange = async (item:Admin) => {
      
      
  item.statusLoading = true
  try {
      
      
    await updateAdminStatus(item.id, item.status).finally(() => {
      
      
      item.statusLoading = false
    })
    ElMessage.success(`${ 
        item.status === 1 ? '启用' : '禁用'}成功`)
  } catch (error) {
      
      
    item.status = item.status === 1 ? 0 : 1
  }
}

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

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

<style scoped></style>

Admin 类型添加 statusLoading 字段:

// src\api\types\admin.ts
...

export interface Admin {
    
    
  ...
  statusLoading?: boolean
}

...

编辑表单

<!-- src\views\permission\admin\AdminForm.vue -->
<template>
  <app-dialog
    :title="props.adminId ? '编辑管理员' : '添加管理员'"
    :confirm="handleSubmit"
    @closed="handleDialogClosed"
    @open="handleDialogOpen"
  >
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="100px"
      v-loading="formLoading"
    >
      <el-form-item
        label="管理员账号"
        prop="account"
      >
        <el-input
          v-model="formData.account"
          placeholder="请输入管理员账号"
          :disabled="props.adminId"
        />
      </el-form-item>
      <template v-if="!props.adminId">
        <el-form-item
          label="管理员密码"
          prop="pwd"
        >
          <el-input
            v-model="formData.pwd"
            type="password"
            placeholder="请输入管理员密码"
          />
        </el-form-item>
        <el-form-item
          label="确认密码"
          prop="pwdConfirm"
        >
          <el-input
            v-model="formData.pwdConfirm"
            type="password"
            placeholder="请输入确认密码"
          />
        </el-form-item>
      </template>
      <el-form-item
        label="管理员姓名"
        prop="realName"
      >
        <el-input
          v-model="formData.realName"
          placeholder="请输入管理员姓名"
        />
      </el-form-item>
      <el-form-item
        label="管理员身份"
        prop="roles"
      >
        <el-select
          v-model="formData.roles"
          multiple
          placeholder="请选择管理员身份"
          style="width:100%"
        >
          <el-option
            v-for="item in roles"
            :key="item.id"
            :label="item.name"
            :value="item.id"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="状态">
        <el-radio-group v-model="formData.status">
          <el-radio
            :label="1"
          >
            开启
          </el-radio>
          <el-radio
            :label="0"
          >
            关闭
          </el-radio>
        </el-radio-group>
      </el-form-item>
    </el-form>
  </app-dialog>
</template>

<script setup lang="ts">
import {
      
       ElMessage, FormInstance, FormItemRule } from 'element-plus'
import {
      
       createAdmin, getAdmin, updateAdmin } from '@/api/admin'
import {
      
       getRoles } from '@/api/role'

const props = defineProps({
      
      
  // 编辑的管理员 ID
  adminId: {
      
      
    type: Number,
    default: null
  }
})

interface EmitsType {
      
      
  (e: 'update:admin-id', value:number | null):void
  (e: 'success'):void
}

const emit = defineEmits<EmitsType>()

const formRef = ref<FormInstance>()
const formLoading = ref(false)
const roles = ref<{
      
      id: number, name: string}[]>([])
const formData = ref({
      
      
  account: '',
  pwd: '',
  pwdConfirm: '',
  roles: [] as number[],
  status: 0 as 0 | 1,
  realName: ''
})
const formRules = ref<Record<string, FormItemRule[]>>({
      
      
  account: [
    {
      
       required: true, message: '请输入管理员账号', trigger: 'blur' }
  ],
  pwd: [
    {
      
       required: true, message: '请输入管理员密码', trigger: 'blur' }
  ],
  pwdConfirm: [
    {
      
       required: true, message: '请输入确认密码', trigger: 'blur' }
  ],
  roles: [
    {
      
       required: true, message: '请输入选择管理员角色', trigger: 'change' }
  ],
  realName: [
    {
      
       required: true, message: '请输入管理员姓名', trigger: 'blur' }
  ]
})

const handleDialogOpen = () => {
      
      
  formLoading.value = true
  Promise.all([loadRoles(), loadAdmin()])
    .finally(() => {
      
      
      formLoading.value = false
    })
}

const loadRoles = async () => {
      
      
  const data = await getRoles({
      
       limit: 9999 })
  roles.value = data.list
}

const loadAdmin = async () => {
      
      
  if (!props.adminId) {
      
      
    return
  }

  const data = await getAdmin(props.adminId)

  formData.value = {
      
      
    ...data,
    pwd: '',
    pwdConfirm: '',
    roles: data.roles.map(item => item.id)
  }
}

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

const handleSubmit = async () => {
      
      
  const valid = await formRef.value?.validate()
  if (!valid) {
      
      
    return
  }

  if (props.adminId) {
      
      
    // 更新管理员
    await updateAdmin(props.adminId, formData.value)
  } else {
      
      
    // 添加管理员
    await createAdmin(formData.value)
  }
  emit('success')
  ElMessage.success('保存成功')
}

</script>

<style scoped></style>

菜单管理页面

相关接口

// src\api\menu.ts
// 菜单相关
import request from '@/utils/request'
import {
    
     ListParams, Menu, MenuPostData } from '@/api/types/menu'

// 获取菜单列表
export const getMenus = (params: ListParams) => {
    
    
  return request<{
    
    
    count: number
    list: Menu[]
  }>({
    
    
    method: 'GET',
    url: '/setting/menu',
    params
  })
}

// 添加菜单
export const createMenu = (data: MenuPostData) => {
    
    
  return request({
    
    
    method: 'POST',
    url: '/setting/menu',
    data
  })
}

// 修改菜单
export const updateMenu = (id: number, data: MenuPostData) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/menu/${
      
      id}`,
    data
  })
}

// 获取菜单
export const getMenu = (id: number) => {
    
    
  return request<Menu>({
    
    
    method: 'GET',
    url: `/setting/menu/${
      
      id}`
  })
}

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

// 修改菜单状态
export const updateMenuStatus = (id: number, status: number) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/menu/${
      
      id}/set_status/${
      
      status}`
  })
}

TS 类型定义

// src\api\types\menu.ts
export interface ListParams {
    
    
  page?: number
  limit?: number
  status?: 0 | 1 | ''
}

export interface Menu {
    
    
  id: number
  pid: number
  name: string
  icon: string
  params: string
  path: string
  uniqueAuth: string
  order: number
  chidlren: Menu[]
  isHidden: 0 | 1
  status: 0 | 1
  isDel: 0 | 1
  _addTime: string
  _updateTime: string
  statusLoading?: boolean
}

export interface MenuPostData {
    
    
  id?: number
  pid: number
  name: string
  icon: string
  params: string
  path: string
  uniqueAuth: string
  order: number
  chidlren?: Menu[]
  isHidden: 0 | 1
  status: 0 | 1
}

列表页

<!-- src\views\permission\menu\index.vue -->
<template>
  <page-container>
    <app-card>
      <template #header>
        数据筛选
      </template>

      <el-form
        ref="formRef"
        :model="listParams"
        :disabled="listLoading"
        inline
        @submit.prevent="handleQuery"
      >
        <el-form-item
          label="状态"
        >
          <el-select
            v-model="listParams.status"
            placeholder="请选择"
            clearable
          >
            <el-option
              label="启用"
              :value="1"
            />
            <el-option
              label="禁用"
              :value="0"
            />
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button native-type="submit">
            查询
          </el-button>
        </el-form-item>
      </el-form>
    </app-card>

    <app-card>
      <template #header>
        <el-button
          type="primary"
          @click="formVisible = true"
        >
          添加菜单
        </el-button>
      </template>

      <el-table
        :data="list"
        stripe
        style="width: 100%"
        v-loading="listLoading"
        row-key="id"
      >
        <el-table-column
          prop="name"
          label="菜单名称"
        />
        <el-table-column
          label="页面地址"
        >
          <template #default="scope">
            {
    
    {
    
     scope.row.path }}{
    
    {
    
     scope.row.params }}
          </template>
        </el-table-column>
        <el-table-column
          prop="uniqueAuth"
          label="前端标识"
        />
        <el-table-column
          prop="isHidden"
          label="是否为隐藏菜单"
        >
          <template #default="scope">
            {
    
    {
    
     scope.row.isHidden===1?'是':'否' }}
          </template>
        </el-table-column>
        <el-table-column
          label="状态"
        >
          <template #default="scope">
            <el-switch
              v-model="scope.row.status"
              active-color="#13ce66"
              inactive-color="#ff4949"
              :active-value="1"
              :inactive-value="0"
              :loading="scope.row.statusLoading"
              @change="handleStatusChange(scope.row)"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          fixed="right"
          min-width="100"
          align="center"
        >
          <template #default="scope">
            <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>
        </el-table-column>
      </el-table>

      <app-pagination
        v-model:page="listParams.page"
        v-model:limit="listParams.limit"
        :list-count="listCount"
        :load-list="loadList"
        :disabled="listLoading"
      />
    </app-card>
  </page-container>

  <menu-form
    v-model="formVisible"
    v-model:menu-id="menuId"
    @success="handleFormSuccess"
  />
</template>

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

const list = ref<Menu[]>([]) // 列表数据
const listCount = ref(0) // 总条数
const listLoading = ref(true)
const listParams = reactive({
    
    
  page: 1, // 当前页码
  limit: 10, // 每页条数
  status: '' as ListParams['status'] // 状态
}) // 列表数据查询参数
const formVisible = ref(false) // 编辑对话框显示控制器
const menuId = ref<number>()

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

const loadList = async () => {
    
    
  listLoading.value = true
  const data = await getMenus(listParams).finally(() => {
    
    
    listLoading.value = false
  })

  // 添加修改状态 loading 控制器
  data.list.forEach(item => {
    
    
    item.statusLoading = false
  })

  list.value = data.list
  listCount.value = data.count
}

const handleQuery = () => {
    
    
  // 默认从第一页开始查询
  listParams.page = 1
  loadList()
}

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

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

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

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

<style scoped></style>

编辑表单

<!-- src\views\permission\menu\MenuForm.vue -->
<template>
  <app-dialog
    :title="props.menuId ? '编辑菜单' : '添加菜单'"
    :confirm="handleSubmit"
    @closed="handleDialogClosed"
    @open="handleDialogOpen"
  >
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="120px"
      v-loading="formLoading"
    >
      <el-row>
        <el-col :span="12">
          <el-form-item
            label="菜单名称"
            prop="name"
          >
            <el-input
              v-model="formData.name"
              placeholder="请输入菜单名称"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="父级菜单"
            prop="pid"
          >
            <el-cascader
              v-model="formData.pid"
              :options="menus"
              clearable
              :props="{
    
    
                label: 'name',
                value: 'id',
                checkStrictly:true,
                emitPath: false,
                disabled: (item: {
     
     id:number}) => item.id === props.menuId
              }"
              @change="handleChange"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="路由地址"
            prop="path"
          >
            <el-input
              v-model="formData.path"
              placeholder="请输入路由地址"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="路由参数"
            prop="params"
          >
            <el-input
              v-model="formData.params"
              placeholder="请输入路由参数"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="前端标识"
            prop="uniqueAuth"
          >
            <el-input
              v-model="formData.uniqueAuth"
              placeholder="请输入前端标识"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="图标"
            prop="icon"
          >
            <el-input
              v-model="formData.icon"
              placeholder="请输入图标"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="排序"
            prop="order"
          >
            <el-input-number
              v-model="formData.order"
              placeholder="请输入排序"
            />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item
            label="是否为隐藏菜单"
            prop="isHidden"
          >
            <el-radio-group v-model="formData.isHidden">
              <el-radio
                :label="1"
              ></el-radio>
              <el-radio
                :label="0"
              ></el-radio>
            </el-radio-group>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="状态">
            <el-radio-group v-model="formData.status">
              <el-radio
                :label="1"
              >
                开启
              </el-radio>
              <el-radio
                :label="0"
              >
                关闭
              </el-radio>
            </el-radio-group>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </app-dialog>
</template>

<script setup lang="ts">
import {
    
     ElMessage, FormInstance, FormItemRule } from 'element-plus'
import {
    
     getMenus, createMenu, getMenu, updateMenu } from '@/api/menu'
import type {
    
     Menu } from '@/api/types/menu'

const props = defineProps({
    
    
  // 编辑的菜单 ID
  menuId: {
    
    
    type: Number,
    default: null
  }
})

interface EmitsType {
    
    
  (e: 'update:menu-id', value:number | null):void
  (e: 'success'):void
}

const emit = defineEmits<EmitsType>()

const formRef = ref<FormInstance>()
const formLoading = ref(false)
const menus = ref<Menu[]>([])
const formData = ref({
    
    
  pid: 0,
  name: '',
  icon: '',
  params: '',
  path: '',
  uniqueAuth: '',
  order: 0,
  isHidden: 0 as 0 | 1,
  status: 0 as 0 | 1
})
const formRules = ref<Record<string, FormItemRule[]>>({
    
    
  name: [
    {
    
     required: true, message: '请输入菜单名称', trigger: 'blur' }
  ],
  order: [
    {
    
     required: true, message: '请输入排序', trigger: 'blur' }
  ]
})

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

const loadMenus = async () => {
    
    
  const data = await getMenus({
    
     limit: 9999 })
  menus.value = data.list
}

const loadMenu = async () => {
    
    
  if (!props.menuId) {
    
    
    return
  }

  const data = await getMenu(props.menuId)

  formData.value = data
}

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

const handleSubmit = async () => {
    
    
  const valid = await formRef.value?.validate()
  if (!valid) {
    
    
    return
  }

  if (props.menuId) {
    
    
    // 更新菜单
    await updateMenu(props.menuId, formData.value)
  } else {
    
    
    // 添加菜单
    await createMenu(formData.value)
  }
  emit('success')
  ElMessage.success('保存成功')
}

const handleChange = () => {
    
    }

</script>

<style scoped></style>

角色页面

相关接口

// src\api\role.ts
// 角色相关
import request from '@/utils/request'
import {
    
     ListParams, Role, RolePostData } from '@/api/types/role'

// 获取角色列表
export const getRoles = (params: ListParams) => {
    
    
  return request<{
    
    
    count: number
    list: Role[]
  }>({
    
    
    method: 'GET',
    url: '/setting/role',
    params
  })
}

// 添加角色
export const createRole = (data: RolePostData) => {
    
    
  return request({
    
    
    method: 'POST',
    url: '/setting/role',
    data
  })
}

// 修改角色
export const updateRole = (id: number, data: RolePostData) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/role/${
      
      id}`,
    data
  })
}

// 获取角色
export const getRole = (id: number) => {
    
    
  return request<Role>({
    
    
    method: 'GET',
    url: `/setting/role/${
      
      id}`
  })
}

// 删除角色
export const deleteRole = (id: number) => {
    
    
  return request({
    
    
    method: 'DELETE',
    url: `/setting/role/${
      
      id}`
  })
}

// 修改角色状态
export const updateRoleStatus = (id: number, status: number) => {
    
    
  return request({
    
    
    method: 'PUT',
    url: `/setting/role/${
      
      id}/set_status/${
      
      status}`
  })
}

TS 类型定义

// src\api\types\role.ts
export interface ListParams {
    
    
  page?: number
  limit?: number
  name?: string
  status?: 0 | 1 | ''
}

export interface Role {
    
    
  id: number
  name: string
  menus: ({
    
    name: string, id: number})[]
  status: 0 | 1
  isDel: 0 | 1
  _addTime: string
  _updateTime: string
  statusLoading?: boolean
}

export interface RolePostData {
    
    
  id?: number
  name: string
  menus: number[]
  status: 0 | 1
}

列表页

<!-- src\views\permission\role\index.vue -->
<template>
  <page-container>
    <app-card>
      <template #header>
        数据筛选
      </template>

      <el-form
        ref="formRef"
        :model="listParams"
        :disabled="listLoading"
        inline
        @submit.prevent="handleQuery"
      >
        <el-form-item
          label="状态"
        >
          <el-select
            v-model="listParams.status"
            placeholder="请选择"
            clearable
          >
            <el-option
              label="启用"
              :value="1"
            />
            <el-option
              label="禁用"
              :value="0"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="搜索">
          <el-input
            v-model="listParams.name"
            clearable
            placeholder="请输入角色名称"
          />
        </el-form-item>
        <el-form-item>
          <el-button native-type="submit">
            查询
          </el-button>
        </el-form-item>
      </el-form>
    </app-card>

    <app-card>
      <template #header>
        <el-button
          type="primary"
          @click="formVisible = true"
        >
          添加角色
        </el-button>
      </template>

      <el-table
        :data="list"
        stripe
        style="width: 100%"
        v-loading="listLoading"
      >
        <el-table-column
          prop="name"
          label="角色名称"
        />
        <el-table-column
          label="权限"
          min-width="180"
        >
          <template #default="scope">
            <el-space wrap>
              <el-tag
                v-for="item in scope.row.menus"
                :key="item.id"
              >
                {
    
    {
    
     item.name }}
              </el-tag>
            </el-space>
          </template>
        </el-table-column>
        <el-table-column
          label="状态"
        >
          <template #default="scope">
            <el-switch
              v-model="scope.row.status"
              active-color="#13ce66"
              inactive-color="#ff4949"
              :active-value="1"
              :inactive-value="0"
              :loading="scope.row.statusLoading"
              @change="handleStatusChange(scope.row)"
            />
          </template>
        </el-table-column>
        <el-table-column
          label="操作"
          fixed="right"
          min-width="100"
          align="center"
        >
          <template #default="scope">
            <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>
        </el-table-column>
      </el-table>

      <app-pagination
        v-model:page="listParams.page"
        v-model:limit="listParams.limit"
        :list-count="listCount"
        :load-list="loadList"
        :disabled="listLoading"
      />
    </app-card>
  </page-container>

  <role-form
    v-model="formVisible"
    v-model:role-id="roleId"
    @success="handleFormSuccess"
  />
</template>

<script setup lang="ts">
import {
    
     deleteRole, getRoles, updateRoleStatus } from '@/api/role'
import {
    
     Role, ListParams } from '@/api/types/role'
import {
    
     ElMessage } from 'element-plus'
import RoleForm from './RoleForm.vue'

const list = ref<Role[]>([]) // 列表数据
const listCount = ref(0) // 总条数
const listLoading = ref(true)
const listParams = reactive({
    
    
  page: 1, // 当前页码
  limit: 10, // 每页条数
  name: '', // 姓名或账号
  status: '' as ListParams['status'] // 状态
}) // 列表数据查询参数
const formVisible = ref(false) // 编辑对话框显示控制器
const roleId = ref<number>()

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

const loadList = async () => {
    
    
  listLoading.value = true
  const data = await getRoles(listParams).finally(() => {
    
    
    listLoading.value = false
  })

  // 添加修改状态 loading 控制器
  data.list.forEach(item => {
    
    
    item.statusLoading = false
  })

  list.value = data.list
  listCount.value = data.count
}

const handleQuery = () => {
    
    
  // 默认从第一页开始查询
  listParams.page = 1
  loadList()
}

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

const handleStatusChange = async (item:Role) => {
    
    
  item.statusLoading = true
  try {
    
    
    await updateRoleStatus(item.id, item.status).finally(() => {
    
    
      item.statusLoading = false
    })
    ElMessage.success(`${
      
      item.status === 1 ? '启用' : '禁用'}成功`)
  } catch (error) {
    
    
    item.status = item.status === 1 ? 0 : 1
  }
}

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

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

<style scoped></style>

编辑表单

<!-- src\views\permission\admin\RoleForm.vue -->
<template>
  <app-dialog
    :title="props.roleId ? '编辑角色' : '添加角色'"
    :confirm="handleSubmit"
    @closed="handleDialogClosed"
    @open="handleDialogOpen"
  >
    <el-form
      ref="formRef"
      :model="formData"
      :rules="formRules"
      label-width="100px"
      v-loading="formLoading"
    >
      <el-form-item
        label="角色名称"
        prop="name"
      >
        <el-input
          v-model="formData.name"
          placeholder="请输入角色名称"
          :disabled="props.roleId"
        />
      </el-form-item>
      <el-form-item label="状态">
        <el-radio-group v-model="formData.status">
          <el-radio
            :label="1"
          >
            开启
          </el-radio>
          <el-radio
            :label="0"
          >
            关闭
          </el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="角色权限">
        <el-tree
          ref="treeRef"
          :data="menus"
          node-key="id"
          show-checkbox
          :props="{
    
    
            label: 'name'
          }"
          style="width: 100%;"
        />
      </el-form-item>
    </el-form>
  </app-dialog>
</template>

<script setup lang="ts">
import {
    
     ElMessage, FormInstance, FormItemRule } from 'element-plus'
import type {
    
     ElTreeType } from '@/types/element-plus'
import {
    
     createRole, getRole, updateRole } from '@/api/role'
import {
    
     getMenus } from '@/api/menu'
import {
    
     Menu } from '@/api/types/menu'

const props = defineProps({
    
    
  // 编辑的角色 ID
  roleId: {
    
    
    type: Number,
    default: null
  }
})

interface EmitsType {
    
    
  (e: 'update:role-id', value:number | null):void
  (e: 'success'):void
}

const emit = defineEmits<EmitsType>()

const formRef = ref<FormInstance>()
const formLoading = ref(false)
const formData = ref({
    
    
  name: '',
  menus: [] as number[],
  status: 0 as 0 | 1
})
const formRules = ref<Record<string, FormItemRule[]>>({
    
    
  name: [
    {
    
     required: true, message: '请输入角色名称', trigger: 'blur' }
  ]
})
const menus = ref<Menu[]>([]) // 菜单列表
const treeRef = ref<ElTreeType>()

const handleDialogOpen = () => {
    
    
  formLoading.value = true
  Promise.all([loadMenus(), loadRole()])
    .then(async () => {
    
    
      await nextTick() // 等待菜单树渲染完成
      setCheckMenus(formData.value.menus)
    })
    .finally(() => {
    
    
      formLoading.value = false
    })
}

const loadMenus = async () => {
    
    
  const data = await getMenus({
    
     limit: 9999 })
  menus.value = data.list
}

const loadRole = async () => {
    
    
  if (!props.roleId) {
    
    
    return
  }

  const data = await getRole(props.roleId)

  formData.value = {
    
    
    ...data,
    menus: data.menus.map(v => v.id)
  }
}

const setCheckMenus = (checkedIds: number[]) => {
    
    
  // 只选中叶子节点,避免选中父节点触发全部勾选
  const leafIds:number[] = []
  checkedIds.forEach(id => {
    
    
    const node = treeRef.value?.getNode(id)
    if (node && node.isLeaf) {
    
    
      leafIds.push(id)
    }
  })

  treeRef.value?.setCheckedKeys(leafIds)
}

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

const handleSubmit = async () => {
    
    
  const valid = await formRef.value?.validate()
  if (!valid) {
    
    
    return
  }

  formData.value.menus = [
    ...treeRef.value?.getCheckedKeys() as number[],
    ...treeRef.value?.getHalfCheckedKeys() as number[]
  ]

  if (props.roleId) {
    
    
    // 更新角色
    await updateRole(props.roleId, formData.value)
  } else {
    
    
    // 添加角色
    await createRole(formData.value)
  }
  emit('success')
  ElMessage.success('保存成功')
}

</script>

<style scoped></style>

添加 ElTree 类型定义:

// src\types\element-plus.d.ts
import {
    
     ElDialog, ElTree } from 'element-plus'

export type ElDialogType = InstanceType<typeof ElDialog>
export type ElTreeType = InstanceType<typeof ElTree>

接口动态生成导航菜单

缓存菜单权限

// src\store\index.ts
...

type User = ({
    
     token: string, menus: Menu[], uniqueAuth: string[] } & UserInfo) | null

...

<!-- src\views\login\index.vue -->
...

<script lang="ts" setup>
...

// 表单提交
const handleSubmit = async () => {
      
      
  if (!formRef.value) return

  // 表单验证
  const valid = await formRef.value.validate()

  if (!valid) return false

  // 验证通过 展示 loading
  loading.value = true

  // 请求提交
  try {
      
      
    const data = await login(user).finally(() => {
      
      
      loading.value = false
    })

    store.setUser({
      
      
      ...data.userInfo,
      menus: data.menus,
      uniqueAuth: data.uniqueAuth,
      token: data.token
    })
  } catch (error) {
      
      
    loadCaptcha()
    return
  }

...

</script>
...

修改导航组件

<!-- src\layout\AppMenu\MenuItem.vue -->
<template>
  <el-sub-menu
    index="1"
    v-if="props.menu.children && props.menu.children.length >0"
  >
    <template #title>
      <el-icon><component :is="props.menu.icon" /></el-icon>
      <span>{
   
   { props.menu.name }}</span>
    </template>
    <MenuItem
      v-for="item in props.menu.children"
      :menu="item"
      :key="item.id"
    />
  </el-sub-menu>
  <el-menu-item
    :index="props.menu.routePath"
    v-else
  >
    <el-icon><component :is="props.menu.icon" /></el-icon>
    <span>{
   
   { props.menu.name }}</span>
  </el-menu-item>
</template>

<script setup lang="ts">
import type {
      
       PropType } from 'vue'
import type {
      
       Menu } from '@/api/types/common'

const props = defineProps({
      
      
  menu: {
      
      
    type: Object as PropType<Menu>,
    required: true
  }
})
</script>

<style scoped></style>

<!-- src\layout\components\AppMenu.vue -->
<template>
  <el-menu
    active-text-color="#ffd04b"
    background-color="#304156"
    class="el-menu-vertical-demo"
    default-active="2"
    text-color="#fff"
    :collapse="store.isCollapse"
    router
  >
    <MenuItem
      v-for="item in store.user?.menus"
      :menu="item"
      :index="item.routePath"
      :key="item.id"
    />
  </el-menu>
</template>

<script setup lang="ts">
import useStore from '@/store'
import MenuItem from './MenuItem.vue'

const store = useStore()
</script>

<style scoped lang="scss">
.el-menu {
      
      
  border-right: none;
}
.el-menu:not(.el-menu--collapse) {
      
      
  width: 200px;
  min-height: 400px;
}
</style>

校验页面访问权限

// src\router\index.ts
...

router.beforeEach(to => {
    
    
  const store = useStore()

  // 校验登录状态
  if (to.meta.requiresAuth && !store.user) {
    
    
    return {
    
    
      name: 'login',
      query: {
    
     redirect: to.fullPath }
    }
  }

  if (store.user && to.name === 'login') {
    
    
    return {
    
    
      name: 'home',
      replace: true
    }
  }

  // 校验访问权限
  if (store.user && to.name !== 'home' && !store.user?.uniqueAuth.includes(to.name as string)) {
    
    
    ElMessage.warning('您没有此菜单的访问权限')
    return false
  }

  // 开始加载进度条
  nprogress.start()
})

router.afterEach(() => {
    
    
  // 结束加载进度条
  nprogress.done()
})

export default router

猜你喜欢

转载自blog.csdn.net/u012961419/article/details/124300253