Vue3+Vite+Ts project combat 06 Automatic registration of global components, encapsulation of paging and Dialog components, permission management

Create global components

page container component

<!-- 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 component

<!-- 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>

Automatically register global components

Vite can import multiple modules from the file system through the Glob function, and generate code for each matching file import().

You can create registration scripts under each component directory, import these modules through Glob, and execute them traversally:

// 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')

Encapsulate the paging component component

Vue3 provides v-model:propsyntax for two-way binding, and the default property change monitoring event update:prop, which can be used to bind and monitor the pagination pageand number of items per page of the packaged component limit.

Element Plus will delete size-change current-changethe event of the pagination component in the future, and it is recommended to listen to the event of the attribute instead update:prop.

Vue 3 can directly use TypeScript syntax to declare props and emits, but defining the default value of props also requires the use of withDefaultscompiler macros. For details, refer to "Type-only props/emit declarations"

<!-- 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)
  }
}

Encapsulate Dialog dialog components

<!-- 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)
  }
}

Add ElDialog component type definition:

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

export type ElDialogType = InstanceType<typeof ElDialog>

Configure rights management routing

// 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

Configure rights management menu

<!-- 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>

...

admin page

related interface

// 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 type definition

// 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
}

List

<!-- 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>

Add a field to Adminthe type statusLoading:

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

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

...

edit form

<!-- 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>

Menu management page

related interface

// 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 type definition

// 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
}

List

<!-- 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>

edit form

<!-- 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>

role page

related interface

// 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 type definition

// 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
}

List

<!-- 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>

edit form

<!-- 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>

Add ElTree type definition:

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

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

The interface dynamically generates the navigation menu

cache menu permissions

// 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>
...

Modify the navigation component

<!-- 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>

Verify page access permissions

// 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

Guess you like

Origin blog.csdn.net/u012961419/article/details/124300253