<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>
<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 声明》
<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(), {
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)
}
}
<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
<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
})
}
// 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
}
<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
}
...
<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}`
})
}
// 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}`
})
}
// 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
...
...
<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>
...
<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>
<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