Créer des composants globaux
composant de conteneur de page
<!-- 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>
Composant de la carte
<!-- 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>
Enregistrer automatiquement les composants globaux
Vite peut importer plusieurs modules du système de fichiers via la fonction Glob et générer du code pour chaque fichier correspondant import()
.
Vous pouvez créer des scripts d'enregistrement sous chaque répertoire de composants, importer ces modules via Glob et les exécuter de manière transversale :
// 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')
Encapsuler le composant de pagination
Vue3 fournit v-model:prop
une syntaxe pour la liaison bidirectionnelle et l'événement de surveillance de changement de propriété par défaut update:prop
, qui peut être utilisé pour lier et surveiller la pagination page
et le nombre d'éléments par page du composant empaqueté limit
.
Element Plus supprimera size-change current-change
l'événement du composant de pagination à l'avenir, et il est recommandé d'écouter l'événement de l'attribut à la place update:prop
.
Vue 3 peut directement utiliser la syntaxe TypeScript pour déclarer les accessoires et les émissions, mais la définition de la valeur par défaut des accessoires nécessite également l'utilisation de withDefaults
macros de compilation. Pour plus de détails, reportez-vous à "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)
}
}
Composants de la boîte de dialogue Encapsuler la boîte de dialogue
<!-- 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)
}
}
Ajoutez la définition du type de composant ElDialog :
// src\types\element-plus.d.ts
import {
ElDialog } from 'element-plus'
export type ElDialogType = InstanceType<typeof ElDialog>
Configurer le routage de la gestion des droits
// 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
Configurer le menu de gestion des droits
<!-- 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>
...
page d'administration
interface associée
// 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
})
}
Définition du type 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
}
Liste
<!-- 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>
Ajouter un champ au Admin
type statusLoading
:
// src\api\types\admin.ts
...
export interface Admin {
...
statusLoading?: boolean
}
...
modifier le formulaire
<!-- 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>
Page de gestion des menus
interface associée
// 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}`
})
}
Définition du type 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
}
Liste
<!-- 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>
modifier le formulaire
<!-- 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>
page de rôle
interface associée
// 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}`
})
}
Définition du type 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
}
Liste
<!-- 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>
modifier le formulaire
<!-- 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>
Ajoutez la définition de type 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>
L'interface génère dynamiquement le menu de navigation
autorisations du menu cache
// 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>
...
Modifier le composant de navigation
<!-- 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>
Vérifier les autorisations d'accès aux pages
// 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