1660 lines
46 KiB
Vue
1660 lines
46 KiB
Vue
<template>
|
||
<div class="role-management p-1 bg-gray-50 h-screen flex flex-col">
|
||
<!-- 搜索表单和角色表格 -->
|
||
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 flex flex-col overflow-hidden">
|
||
<!-- 搜索表单 -->
|
||
<div class="px-4 py-2 border-b border-gray-100">
|
||
<n-form
|
||
ref="searchFormRef"
|
||
:model="searchForm"
|
||
label-placement="left"
|
||
label-width="auto"
|
||
class="search-form"
|
||
>
|
||
<n-grid :cols="5" :x-gap="8" :y-gap="4">
|
||
<n-grid-item>
|
||
<n-form-item label="角色名称" path="roleName">
|
||
<n-input
|
||
v-model:value="searchForm.roleName"
|
||
placeholder="请输入角色名称"
|
||
clearable
|
||
@keydown.enter="handleSearch"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
|
||
<n-grid-item>
|
||
<n-form-item label="角色编码" path="roleCode">
|
||
<n-input
|
||
v-model:value="searchForm.roleCode"
|
||
placeholder="请输入角色编码"
|
||
clearable
|
||
@keydown.enter="handleSearch"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
|
||
<n-grid-item>
|
||
<n-form-item label="创建时间" path="timeRange">
|
||
<n-date-picker
|
||
v-model:value="searchForm.timeRange"
|
||
type="datetimerange"
|
||
clearable
|
||
placeholder="选择时间范围"
|
||
:shortcuts="{
|
||
今天: () => [new Date().setHours(0, 0, 0, 0), new Date().setHours(23, 59, 59, 999)],
|
||
昨天: () => {
|
||
const yesterday = new Date();
|
||
yesterday.setDate(yesterday.getDate() - 1);
|
||
return [yesterday.setHours(0, 0, 0, 0), yesterday.setHours(23, 59, 59, 999)];
|
||
},
|
||
最近7天: () => [Date.now() - 7 * 24 * 60 * 60 * 1000, Date.now()],
|
||
最近30天: () => [Date.now() - 30 * 24 * 60 * 60 * 1000, Date.now()],
|
||
}"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
|
||
<n-grid-item>
|
||
<n-form-item label="角色状态" path="roleStatus">
|
||
<n-select
|
||
v-model:value="searchForm.roleStatus"
|
||
placeholder="请选择角色状态"
|
||
clearable
|
||
:options="getSelectOptions('sys_switch_status')"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
|
||
<n-grid-item>
|
||
<n-form-item label="" path="">
|
||
<NSpace>
|
||
<NButton type="primary" @click="handleSearch">
|
||
<template #icon>
|
||
<NIcon><icon-park-outline-search /></NIcon>
|
||
</template>
|
||
搜索
|
||
</NButton>
|
||
<NButton @click="handleReset">
|
||
<template #icon>
|
||
<NIcon><icon-park-outline-refresh /></NIcon>
|
||
</template>
|
||
重置
|
||
</NButton>
|
||
</NSpace>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
</n-grid>
|
||
</n-form>
|
||
</div>
|
||
|
||
<!-- 表格头部操作栏 -->
|
||
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||
<div class="flex items-center gap-4">
|
||
<NButton v-button="PERMISSIONS.ROLE.ADD" type="primary" class="px-3 flex items-center" @click="handleAdd">
|
||
<template #icon>
|
||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||
<icon-park-outline-plus />
|
||
</NIcon>
|
||
</template>
|
||
新增
|
||
</NButton>
|
||
|
||
<NButton
|
||
v-button="PERMISSIONS.ROLE.UPDATE"
|
||
type="info"
|
||
:disabled="selectedRows.length !== 1"
|
||
class="px-3 flex items-center"
|
||
@click="handleBatchEdit"
|
||
>
|
||
<template #icon>
|
||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||
<icon-park-outline-edit />
|
||
</NIcon>
|
||
</template>
|
||
修改
|
||
</NButton>
|
||
|
||
<NButton
|
||
v-button="PERMISSIONS.ROLE.DELETE"
|
||
type="error"
|
||
:disabled="selectedRows.length === 0"
|
||
class="px-3 flex items-center"
|
||
@click="handleBatchDelete"
|
||
>
|
||
<template #icon>
|
||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||
<IconParkOutlineDelete />
|
||
</NIcon>
|
||
</template>
|
||
删除
|
||
</NButton>
|
||
|
||
<NButton
|
||
v-button="PERMISSIONS.ROLE.MENU"
|
||
type="warning"
|
||
:disabled="selectedRows.length !== 1 || (selectedRows.length === 1 && selectedRows[0].roleId === 1)"
|
||
class="px-3 flex items-center"
|
||
@click="handleBatchAssignMenu"
|
||
>
|
||
<template #icon>
|
||
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||
<IconParkOutlineKey />
|
||
</NIcon>
|
||
</template>
|
||
分配权限
|
||
</NButton>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||
<span>共 {{ pagination.itemCount }} 条</span>
|
||
<NButton text @click="getRoleList">
|
||
<template #icon>
|
||
<NIcon><icon-park-outline-refresh /></NIcon>
|
||
</template>
|
||
</NButton>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 表格内容 -->
|
||
<div class="table-wrapper flex-1 overflow-auto pt-4">
|
||
<!-- 数据表格 -->
|
||
<n-data-table
|
||
v-if="tableData.length > 0 || loading"
|
||
:columns="columns"
|
||
:data="tableData"
|
||
:loading="loading"
|
||
:row-key="(row: RoleVo) => row.roleId"
|
||
:bordered="false"
|
||
:single-line="false"
|
||
:scroll-x="1200"
|
||
size="medium"
|
||
class="custom-table"
|
||
@update:checked-row-keys="handleRowSelectionChange"
|
||
/>
|
||
|
||
<!-- 空状态 -->
|
||
<CoiEmpty
|
||
v-else
|
||
:type="getEmptyType()"
|
||
:title="getEmptyTitle()"
|
||
:description="getEmptyDescription()"
|
||
:show-action="true"
|
||
:action-text="getEmptyActionText()"
|
||
size="medium"
|
||
@action="handleEmptyAction"
|
||
>
|
||
<template #action>
|
||
<NButton
|
||
type="primary"
|
||
size="medium"
|
||
round
|
||
@click="handleEmptyAction"
|
||
>
|
||
<template #icon>
|
||
<NIcon>
|
||
<icon-park-outline-refresh v-if="hasSearchConditions()" />
|
||
<icon-park-outline-plus v-else />
|
||
</NIcon>
|
||
</template>
|
||
{{ getEmptyActionText() }}
|
||
</NButton>
|
||
</template>
|
||
</CoiEmpty>
|
||
</div>
|
||
|
||
<!-- 分页器 -->
|
||
<div v-if="tableData.length > 0" class="border-t border-gray-100">
|
||
<CoiPagination
|
||
v-model:page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:item-count="pagination.itemCount"
|
||
:show-size-picker="true"
|
||
:page-sizes="[10, 20, 50, 100]"
|
||
:show-quick-jumper="true"
|
||
:show-current-info="true"
|
||
@update:page="handlePageChange"
|
||
@update:page-size="handlePageSizeChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 角色表单弹框 -->
|
||
<CoiDialog
|
||
ref="roleDialogRef"
|
||
:title="modalTitle"
|
||
:width="800"
|
||
height="auto"
|
||
confirm-text="确定"
|
||
cancel-text="取消"
|
||
@coi-confirm="handleSubmit"
|
||
@coi-cancel="handleCancel"
|
||
>
|
||
<template #content>
|
||
<div class="px-3 py-2">
|
||
<n-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="rules"
|
||
label-placement="left"
|
||
label-width="90px"
|
||
require-mark-placement="right-hanging"
|
||
class="compact-form"
|
||
>
|
||
<!-- 第一行:角色名称 和 角色编码 -->
|
||
<n-grid :cols="2" :x-gap="10">
|
||
<n-grid-item>
|
||
<n-form-item label="角色名称" path="roleName" class="mb-2">
|
||
<n-input
|
||
v-model:value="formData.roleName"
|
||
placeholder="请输入角色名称"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
<n-grid-item>
|
||
<n-form-item label="角色编码" path="roleCode" class="mb-2">
|
||
<n-input
|
||
v-model:value="formData.roleCode"
|
||
placeholder="请输入角色编码"
|
||
:disabled="isEdit"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
</n-grid>
|
||
|
||
<!-- 第二行:角色状态 和 显示排序 -->
|
||
<n-grid :cols="2" :x-gap="10">
|
||
<n-grid-item>
|
||
<n-form-item label="角色状态" path="roleStatus" class="mb-2">
|
||
<NRadioGroup v-model:value="formData.roleStatus">
|
||
<NRadio
|
||
v-for="item in dictOptions.sys_switch_status || []"
|
||
:key="item.dictValue"
|
||
:value="item.dictValue"
|
||
>
|
||
{{ item.dictLabel }}
|
||
</NRadio>
|
||
</NRadioGroup>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
<n-grid-item>
|
||
<n-form-item label="显示排序" path="sorted" class="mb-2">
|
||
<n-input-number
|
||
v-model:value="formData.sorted"
|
||
placeholder="请输入排序号"
|
||
:min="1"
|
||
:max="999"
|
||
style="width: 100%"
|
||
/>
|
||
</n-form-item>
|
||
</n-grid-item>
|
||
</n-grid>
|
||
|
||
<!-- 第三行:角色描述(占满整行) -->
|
||
<n-form-item label="角色描述" path="remark" class="mb-2">
|
||
<n-input
|
||
v-model:value="formData.remark"
|
||
type="textarea"
|
||
placeholder="请输入角色描述"
|
||
:rows="3"
|
||
/>
|
||
</n-form-item>
|
||
</n-form>
|
||
</div>
|
||
</template>
|
||
</CoiDialog>
|
||
|
||
<!-- 菜单权限分配弹框 -->
|
||
<CoiDialog
|
||
ref="menuDialogRef"
|
||
title="分配菜单权限"
|
||
:width="500"
|
||
height="auto"
|
||
confirm-text="确定"
|
||
cancel-text="取消"
|
||
@coi-confirm="handleConfirmAssignMenu"
|
||
@coi-cancel="handleCancelAssignMenu"
|
||
>
|
||
<template #content>
|
||
<div class="p-3">
|
||
<div v-if="menuLoading" class="flex items-center justify-center py-8">
|
||
<div class="text-center">
|
||
<n-spin size="medium" />
|
||
<p class="mt-2 text-sm text-gray-500">
|
||
正在加载菜单数据...
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else-if="menuData.length > 0">
|
||
<!-- 功能按钮区域 -->
|
||
<div class="flex items-center gap-2 mb-3 pb-3 border-b border-gray-200">
|
||
<NButton
|
||
size="small"
|
||
:type="allExpanded ? 'default' : 'primary'"
|
||
@click="toggleExpandAll"
|
||
>
|
||
<template #icon>
|
||
<NIcon style="transform: translateY(-1px)">
|
||
<icon-park-outline-minus v-if="allExpanded" />
|
||
<icon-park-outline-plus v-else />
|
||
</NIcon>
|
||
</template>
|
||
{{ allExpanded ? '折叠' : '展开' }}
|
||
</NButton>
|
||
|
||
<NButton
|
||
size="small"
|
||
:type="allSelected ? 'default' : 'primary'"
|
||
@click="toggleSelectAll"
|
||
>
|
||
<template #icon>
|
||
<NIcon style="transform: translateY(-1px)">
|
||
<icon-park-outline-close v-if="allSelected" />
|
||
<icon-park-outline-check v-else />
|
||
</NIcon>
|
||
</template>
|
||
{{ allSelected ? '全不选' : '全选' }}
|
||
</NButton>
|
||
|
||
<NButton
|
||
size="small"
|
||
:type="cascadeEnabled ? 'primary' : 'default'"
|
||
@click="toggleCascade"
|
||
>
|
||
<template #icon>
|
||
<NIcon style="transform: translateY(-1px)">
|
||
<icon-park-outline-link />
|
||
</NIcon>
|
||
</template>
|
||
父子联动
|
||
</NButton>
|
||
|
||
<NButton
|
||
size="small"
|
||
:type="showPermissionCode ? 'primary' : 'default'"
|
||
@click="togglePermissionCode"
|
||
>
|
||
<template #icon>
|
||
<NIcon style="transform: translateY(-1px)">
|
||
<icon-park-outline-battery-charge />
|
||
</NIcon>
|
||
</template>
|
||
权限标识
|
||
</NButton>
|
||
</div>
|
||
|
||
<NTree
|
||
v-model:checked-keys="checkedKeys"
|
||
v-model:expanded-keys="expandedKeys"
|
||
:data="menuData"
|
||
checkable
|
||
:cascade="cascadeEnabled"
|
||
check-strategy="all"
|
||
expand-on-click
|
||
:render-suffix="showPermissionCode ? renderPermissionCode : undefined"
|
||
class="clean-menu-tree"
|
||
/>
|
||
</div>
|
||
|
||
<div v-else class="flex items-center justify-center py-8">
|
||
<div class="text-center">
|
||
<div class="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-2">
|
||
<NIcon size="16" class="text-gray-400">
|
||
<icon-park-outline-folder-close />
|
||
</NIcon>
|
||
</div>
|
||
<p class="text-sm text-gray-500">
|
||
暂无可分配的菜单
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</CoiDialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { h, nextTick, onMounted, ref, watch } from 'vue'
|
||
import type { DataTableColumns, FormInst } from 'naive-ui'
|
||
import { NButton, NIcon, NPopconfirm, NRadio, NRadioGroup, NSpace, NSwitch, NTag, NTree } from 'naive-ui'
|
||
import IconParkOutlineEditOne from '~icons/icon-park-outline/edit-one'
|
||
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||
import IconParkOutlineKey from '~icons/icon-park-outline/key'
|
||
import CoiDialog from '@/components/common/CoiDialog.vue'
|
||
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||
import CoiPagination from '@/components/common/CoiPagination.vue'
|
||
import DictTag from '@/components/common/DictTag.vue'
|
||
import {
|
||
addRole,
|
||
batchDeleteRoles,
|
||
deleteRole,
|
||
getRolePage,
|
||
getRoleSorted,
|
||
updateRole,
|
||
updateRoleStatus,
|
||
} from '@/service/api/system/role'
|
||
import type { RoleForm, RoleSearchForm, RoleVo } from '@/service/api/system/role'
|
||
|
||
import {
|
||
fetchMenuIdsByRoleId,
|
||
fetchMenuPermissionData,
|
||
saveRoleMenuPermission,
|
||
} from '@/service/api/system/menu'
|
||
import type { MenuPermissionData, MenuVo } from '@/service/api/system/menu'
|
||
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||
import { PERMISSIONS } from '@/constants/permissions'
|
||
import { useDict, usePermission } from '@/hooks'
|
||
|
||
// 权限相关
|
||
const { hasButton } = usePermission()
|
||
const { dictOptions, getSelectOptions, getDictLabel } = useDict(['sys_switch_status'])
|
||
|
||
// 响应式数据
|
||
const loading = ref(false)
|
||
const tableData = ref<RoleVo[]>([])
|
||
const modalTitle = ref('新增角色')
|
||
const formRef = ref<FormInst | null>(null)
|
||
const searchFormRef = ref<FormInst | null>(null)
|
||
const isEdit = ref(false)
|
||
const currentRole = ref<RoleVo | null>(null)
|
||
const selectedRows = ref<RoleVo[]>([])
|
||
|
||
// 弹框引用
|
||
const roleDialogRef = ref()
|
||
const menuDialogRef = ref()
|
||
|
||
// 菜单权限分配相关
|
||
const menuModalTitle = ref('分配菜单权限')
|
||
const currentAssignRole = ref<RoleVo | null>(null)
|
||
const menuData = ref<any[]>([])
|
||
const expandedKeys = ref<string[]>([])
|
||
const checkedKeys = ref<string[]>([])
|
||
const menuLoading = ref(false)
|
||
|
||
// 功能按钮状态
|
||
const allExpanded = ref(false)
|
||
const allSelected = ref(false)
|
||
const cascadeEnabled = ref(true)
|
||
const showPermissionCode = ref(false)
|
||
|
||
// 分页数据
|
||
const pagination = ref({
|
||
page: 1,
|
||
pageSize: 10,
|
||
itemCount: 0,
|
||
showSizePicker: true,
|
||
pageSizes: [10, 20, 50, 100],
|
||
})
|
||
|
||
// 搜索表单数据
|
||
const searchForm = ref<RoleSearchForm>({})
|
||
|
||
// 表单数据
|
||
const formData = ref<RoleForm>({
|
||
roleName: '',
|
||
roleCode: '',
|
||
roleStatus: '0',
|
||
remark: '',
|
||
sorted: 1,
|
||
})
|
||
|
||
// 表单验证规则
|
||
const rules = {
|
||
roleName: [
|
||
{ required: true, message: '请输入角色名称', trigger: 'blur' },
|
||
{ min: 2, max: 20, message: '角色名称长度为 2-20 位', trigger: 'blur' },
|
||
],
|
||
roleCode: [
|
||
{ required: true, message: '请输入角色编码', trigger: 'blur' },
|
||
{ min: 2, max: 20, message: '角色编码长度为 2-20 位', trigger: 'blur' },
|
||
{ pattern: /^\w+$/, message: '角色编码只能包含字母、数字和下划线', trigger: 'blur' },
|
||
],
|
||
roleStatus: [
|
||
{ required: true, message: '请选择角色状态', trigger: 'change' },
|
||
],
|
||
}
|
||
|
||
// 表格列定义
|
||
const columns: DataTableColumns<RoleVo> = [
|
||
{
|
||
type: 'selection',
|
||
width: 50,
|
||
disabled: (row: RoleVo) => row.roleId === 1, // 超级管理员不可选择
|
||
},
|
||
{
|
||
title: '序号',
|
||
key: 'index',
|
||
width: 70,
|
||
align: 'center',
|
||
render: (_, index) => {
|
||
return (pagination.value.page - 1) * pagination.value.pageSize + index + 1
|
||
},
|
||
},
|
||
{
|
||
title: '角色名称',
|
||
key: 'roleName',
|
||
width: 150,
|
||
align: 'center',
|
||
render: (row) => {
|
||
return h('span', { class: 'text-gray-600' }, row.roleName)
|
||
},
|
||
},
|
||
{
|
||
title: '角色编码',
|
||
key: 'roleCode',
|
||
width: 150,
|
||
align: 'center',
|
||
render: row => h(NTag, { type: 'primary', size: 'small' }, { default: () => row.roleCode }),
|
||
},
|
||
{
|
||
title: '角色状态',
|
||
key: 'roleStatus',
|
||
width: 120,
|
||
align: 'center',
|
||
render: (row) => {
|
||
return h('div', { class: 'flex items-center justify-center gap-2' }, [
|
||
h(DictTag, { dictType: 'sys_switch_status', value: row.roleStatus }),
|
||
h(NPopconfirm, {
|
||
onPositiveClick: () => handleToggleStatus(row),
|
||
negativeText: '取消',
|
||
positiveText: '确定',
|
||
}, {
|
||
default: () => `确定要将角色「${row.roleName}」状态切换为「${getDictLabel('sys_switch_status', row.roleStatus === '0' ? '1' : '0')}」吗?`,
|
||
trigger: () => h(NSwitch, {
|
||
value: row.roleStatus === '0',
|
||
size: 'small',
|
||
checkedChildren: getDictLabel('sys_switch_status', '0', '启用'),
|
||
uncheckedChildren: getDictLabel('sys_switch_status', '1', '停用'),
|
||
loading: false,
|
||
}),
|
||
}),
|
||
])
|
||
},
|
||
},
|
||
{
|
||
title: '角色描述',
|
||
key: 'remark',
|
||
width: 200,
|
||
align: 'center',
|
||
ellipsis: { tooltip: true },
|
||
render: (row) => {
|
||
return h('span', { class: 'text-gray-600' }, row.remark || '-')
|
||
},
|
||
},
|
||
{
|
||
title: '排序',
|
||
key: 'sorted',
|
||
width: 80,
|
||
align: 'center',
|
||
render: (row) => {
|
||
return h(NTag, { type: 'primary', size: 'small' }, { default: () => row.sorted || '-' })
|
||
},
|
||
},
|
||
{
|
||
title: '创建时间',
|
||
key: 'createTime',
|
||
width: 160,
|
||
align: 'center',
|
||
render: (row) => {
|
||
return h('span', { class: 'text-gray-500 text-sm' }, row.createTime || '-')
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 200,
|
||
align: 'center',
|
||
fixed: 'right',
|
||
render: (row) => {
|
||
const buttons = []
|
||
|
||
// 编辑按钮
|
||
if (hasButton(PERMISSIONS.ROLE.UPDATE)) {
|
||
buttons.push(h(NButton, {
|
||
type: 'primary',
|
||
size: 'small',
|
||
class: 'action-btn-primary',
|
||
onClick: () => handleEdit(row),
|
||
}, {
|
||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||
default: () => h(IconParkOutlineEditOne),
|
||
}),
|
||
default: () => '编辑',
|
||
}))
|
||
}
|
||
|
||
// 删除按钮
|
||
if (hasButton(PERMISSIONS.ROLE.DELETE)) {
|
||
buttons.push(h(NPopconfirm, {
|
||
onPositiveClick: () => handleDelete(row.roleId),
|
||
negativeText: '取消',
|
||
positiveText: '确定',
|
||
}, {
|
||
default: () => row.roleId === 1 ? '超级管理员角色不可删除' : '确定删除此角色吗?',
|
||
trigger: () => h(NButton, {
|
||
type: 'error',
|
||
secondary: true,
|
||
size: 'small',
|
||
class: 'action-btn-secondary action-btn-danger',
|
||
disabled: row.roleId === 1,
|
||
}, {
|
||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||
default: () => h(IconParkOutlineDelete),
|
||
}),
|
||
default: () => '删除',
|
||
}),
|
||
}))
|
||
}
|
||
|
||
// 分配权限按钮 - 超级管理员不显示
|
||
if (hasButton(PERMISSIONS.ROLE.MENU) && row.roleId !== 1) {
|
||
buttons.push(h(NButton, {
|
||
type: 'warning',
|
||
secondary: true,
|
||
size: 'small',
|
||
class: 'action-btn-secondary action-btn-warning',
|
||
onClick: () => handleAssignMenu(row),
|
||
}, {
|
||
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||
default: () => h(IconParkOutlineKey),
|
||
}),
|
||
default: () => '权限',
|
||
}))
|
||
}
|
||
|
||
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
|
||
},
|
||
},
|
||
]
|
||
|
||
// 构建树形菜单数据
|
||
function buildMenuTree(menuList: MenuVo[]): any[] {
|
||
const map = new Map()
|
||
const result: any[] = []
|
||
|
||
// 先将所有菜单存入map,确保key的唯一性和类型一致性
|
||
menuList.forEach((menu) => {
|
||
const permissionCode = menu.auth || menu.perms || menu.permission || ''
|
||
map.set(menu.menuId, {
|
||
key: String(menu.menuId), // 确保key为字符串类型
|
||
label: menu.menuName,
|
||
menuId: menu.menuId,
|
||
parentId: menu.parentId,
|
||
menuType: menu.menuType,
|
||
auth: menu.auth || '', // 权限标识
|
||
perms: permissionCode, // 权限标识
|
||
permissionCode, // 权限标识的别名
|
||
})
|
||
})
|
||
|
||
// 构建树形结构
|
||
menuList.forEach((menu) => {
|
||
const node = map.get(menu.menuId)
|
||
if (menu.parentId === 0) {
|
||
result.push(node)
|
||
}
|
||
else {
|
||
const parent = map.get(menu.parentId)
|
||
if (parent) {
|
||
if (!parent.children) {
|
||
parent.children = []
|
||
}
|
||
parent.children.push(node)
|
||
}
|
||
}
|
||
})
|
||
|
||
// 清理空的children数组,让叶子节点真正成为叶子节点
|
||
function cleanEmptyChildren(nodes: any[]) {
|
||
nodes.forEach((node) => {
|
||
if (node.children && node.children.length === 0) {
|
||
delete node.children
|
||
}
|
||
else if (node.children && node.children.length > 0) {
|
||
cleanEmptyChildren(node.children)
|
||
}
|
||
})
|
||
}
|
||
|
||
cleanEmptyChildren(result)
|
||
return result
|
||
}
|
||
|
||
// 获取角色列表
|
||
async function getRoleList() {
|
||
loading.value = true
|
||
try {
|
||
// 构建请求参数,处理时间范围
|
||
const { timeRange, ...otherParams } = searchForm.value
|
||
|
||
// 过滤掉空值和null值,只保留有效的搜索条件
|
||
const filteredParams = Object.entries(otherParams).reduce((acc, [key, value]) => {
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
acc[key] = value
|
||
}
|
||
return acc
|
||
}, {} as Record<string, any>)
|
||
|
||
const params: any = {
|
||
pageNo: pagination.value.page,
|
||
pageSize: pagination.value.pageSize,
|
||
...filteredParams,
|
||
}
|
||
|
||
// 处理时间范围,转换为beginTime和endTime(需要后端支持)
|
||
if (timeRange && timeRange.length === 2) {
|
||
// 使用本地时间格式,避免时区问题
|
||
const startDate = new Date(timeRange[0])
|
||
const endDate = new Date(timeRange[1])
|
||
|
||
// 格式化为 yyyy-MM-dd HH:mm:ss 格式
|
||
params.beginTime = `${startDate.getFullYear()}-${String(startDate.getMonth() + 1).padStart(2, '0')}-${String(startDate.getDate()).padStart(2, '0')} ${String(startDate.getHours()).padStart(2, '0')}:${String(startDate.getMinutes()).padStart(2, '0')}:${String(startDate.getSeconds()).padStart(2, '0')}`
|
||
params.endTime = `${endDate.getFullYear()}-${String(endDate.getMonth() + 1).padStart(2, '0')}-${String(endDate.getDate()).padStart(2, '0')} ${String(endDate.getHours()).padStart(2, '0')}:${String(endDate.getMinutes()).padStart(2, '0')}:${String(endDate.getSeconds()).padStart(2, '0')}`
|
||
}
|
||
|
||
const { isSuccess, data } = await getRolePage(params)
|
||
|
||
if (isSuccess && data) {
|
||
tableData.value = data.records || []
|
||
pagination.value.itemCount = data.total || 0
|
||
}
|
||
else {
|
||
coiMsgError('获取角色列表失败,请检查网络连接或联系管理员')
|
||
tableData.value = []
|
||
pagination.value.itemCount = 0
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError('获取角色列表失败,请检查网络连接')
|
||
tableData.value = []
|
||
pagination.value.itemCount = 0
|
||
}
|
||
finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 分页变化处理
|
||
function handlePageChange(page: number) {
|
||
pagination.value.page = page
|
||
getRoleList()
|
||
}
|
||
|
||
function handlePageSizeChange(pageSize: number) {
|
||
pagination.value.pageSize = pageSize
|
||
pagination.value.page = 1
|
||
getRoleList()
|
||
}
|
||
|
||
// 搜索
|
||
function handleSearch() {
|
||
pagination.value.page = 1
|
||
getRoleList()
|
||
}
|
||
|
||
// 重置搜索
|
||
async function handleReset() {
|
||
// 重置搜索表单的所有字段
|
||
searchForm.value = {
|
||
roleName: '',
|
||
roleCode: '',
|
||
roleStatus: null,
|
||
timeRange: null,
|
||
}
|
||
|
||
// 使用 nextTick 确保 DOM 更新后再重置表单状态
|
||
await nextTick()
|
||
|
||
// 重置表单验证状态
|
||
if (searchFormRef.value) {
|
||
searchFormRef.value.restoreValidation()
|
||
}
|
||
|
||
// 重置分页到第一页
|
||
pagination.value.page = 1
|
||
// 重新获取数据
|
||
getRoleList()
|
||
}
|
||
|
||
// 行选择变化
|
||
function handleRowSelectionChange(rowKeys: (string | number)[]) {
|
||
// 保持字符串格式避免精度丢失
|
||
const stringKeys = rowKeys.map(key => String(key))
|
||
selectedRows.value = tableData.value.filter(row => stringKeys.includes(String(row.roleId)))
|
||
}
|
||
|
||
// 批量修改(选中一个角色进行修改)
|
||
function handleBatchEdit() {
|
||
if (selectedRows.value.length !== 1) {
|
||
coiMsgWarning('请选择一个角色进行修改')
|
||
return
|
||
}
|
||
handleEdit(selectedRows.value[0])
|
||
}
|
||
|
||
// 批量分配权限(选中一个角色进行权限分配)
|
||
function handleBatchAssignMenu() {
|
||
if (selectedRows.value.length !== 1) {
|
||
coiMsgWarning('请选择一个角色进行权限分配')
|
||
return
|
||
}
|
||
|
||
const selectedRole = selectedRows.value[0]
|
||
if (selectedRole.roleId === 1) {
|
||
coiMsgError('超级管理员角色无需分配权限')
|
||
return
|
||
}
|
||
|
||
handleAssignMenu(selectedRole)
|
||
}
|
||
|
||
// 新增角色
|
||
async function handleAdd() {
|
||
modalTitle.value = '新增角色'
|
||
isEdit.value = false
|
||
currentRole.value = null
|
||
|
||
try {
|
||
// 获取最新排序号
|
||
const { isSuccess, data } = await getRoleSorted()
|
||
const sorted = isSuccess && data ? data : 1
|
||
|
||
formData.value = {
|
||
roleName: '',
|
||
roleCode: '',
|
||
roleStatus: '0',
|
||
remark: '',
|
||
sorted,
|
||
}
|
||
}
|
||
catch {
|
||
formData.value = {
|
||
roleName: '',
|
||
roleCode: '',
|
||
roleStatus: '0',
|
||
remark: '',
|
||
sorted: 1,
|
||
}
|
||
}
|
||
|
||
roleDialogRef.value?.coiOpen()
|
||
}
|
||
|
||
// 编辑角色
|
||
function handleEdit(role: RoleVo) {
|
||
modalTitle.value = '编辑角色'
|
||
isEdit.value = true
|
||
currentRole.value = role
|
||
formData.value = {
|
||
roleId: role.roleId,
|
||
roleName: role.roleName,
|
||
roleCode: role.roleCode,
|
||
roleStatus: role.roleStatus,
|
||
remark: role.remark || '',
|
||
sorted: role.sorted || 1,
|
||
}
|
||
roleDialogRef.value?.coiOpen()
|
||
}
|
||
|
||
// 删除角色
|
||
async function handleDelete(roleId: number) {
|
||
if (roleId === 1) {
|
||
coiMsgError('超级管理员角色不可删除')
|
||
return
|
||
}
|
||
|
||
try {
|
||
const { isSuccess } = await deleteRole(roleId)
|
||
if (isSuccess) {
|
||
coiMsgSuccess('删除成功')
|
||
await getRoleList()
|
||
}
|
||
else {
|
||
coiMsgError('删除失败')
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError('删除失败')
|
||
}
|
||
}
|
||
|
||
// 批量删除角色
|
||
async function handleBatchDelete() {
|
||
if (selectedRows.value.length === 0) {
|
||
coiMsgWarning('请先选择要删除的角色')
|
||
return
|
||
}
|
||
|
||
// 检查是否包含超级管理员角色
|
||
const hasSuperAdmin = selectedRows.value.some(role => role.roleId === 1)
|
||
if (hasSuperAdmin) {
|
||
coiMsgError('所选角色中包含超级管理员,无法批量删除')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 确认删除操作
|
||
await coiMsgBox(`确定要删除选中的 ${selectedRows.value.length} 个角色吗?`, '批量删除确认')
|
||
}
|
||
catch {
|
||
// 用户取消删除操作,直接返回,不显示任何提示
|
||
return
|
||
}
|
||
|
||
// 用户确认删除,执行删除操作
|
||
try {
|
||
const roleIds = selectedRows.value.map(role => role.roleId)
|
||
const { isSuccess } = await batchDeleteRoles(roleIds)
|
||
if (isSuccess) {
|
||
coiMsgSuccess('批量删除成功')
|
||
selectedRows.value = []
|
||
await getRoleList()
|
||
}
|
||
else {
|
||
coiMsgError('批量删除失败')
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError('批量删除失败')
|
||
}
|
||
}
|
||
|
||
// 切换角色状态
|
||
async function handleToggleStatus(role: RoleVo) {
|
||
try {
|
||
const newStatus = role.roleStatus === '0' ? '1' : '0'
|
||
const statusText = getDictLabel('sys_switch_status', newStatus, newStatus === '0' ? '启用' : '停用')
|
||
|
||
const { isSuccess } = await updateRoleStatus(role.roleId, newStatus)
|
||
if (isSuccess) {
|
||
role.roleStatus = newStatus
|
||
coiMsgSuccess(`角色「${role.roleName}」${statusText}成功`)
|
||
// 刷新角色列表以确保数据同步
|
||
await getRoleList()
|
||
}
|
||
else {
|
||
coiMsgError(`角色${statusText}失败`)
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError('状态修改失败,请检查网络连接')
|
||
}
|
||
}
|
||
|
||
// 分配菜单权限
|
||
async function handleAssignMenu(role: RoleVo) {
|
||
if (!role?.roleId) {
|
||
coiMsgError('角色信息无效')
|
||
return
|
||
}
|
||
|
||
try {
|
||
menuLoading.value = true
|
||
currentAssignRole.value = role
|
||
menuModalTitle.value = `分配菜单权限 - ${role.roleName}`
|
||
|
||
// 重置状态
|
||
menuData.value = []
|
||
expandedKeys.value = []
|
||
checkedKeys.value = []
|
||
|
||
// 获取菜单权限数据
|
||
const menuResponse = await fetchMenuPermissionData()
|
||
|
||
if (menuResponse.isSuccess && menuResponse.data) {
|
||
const permissionData = menuResponse.data as MenuPermissionData
|
||
menuData.value = buildMenuTree(permissionData.menuList)
|
||
|
||
// 将展开的节点ID转换为字符串格式,保持与key格式一致
|
||
expandedKeys.value = (permissionData.spreadList || []).map(id => String(id))
|
||
|
||
// 获取当前角色的菜单权限
|
||
const roleMenuResponse = await fetchMenuIdsByRoleId(role.roleId)
|
||
if (roleMenuResponse.isSuccess && roleMenuResponse.data) {
|
||
// 将权限ID转换为字符串格式,保持与key格式一致
|
||
checkedKeys.value = roleMenuResponse.data.map(id => String(id))
|
||
}
|
||
|
||
menuDialogRef.value?.coiOpen()
|
||
}
|
||
else {
|
||
coiMsgError('获取菜单数据失败')
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError('获取菜单数据失败,请检查网络连接')
|
||
}
|
||
finally {
|
||
menuLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 确保选中子菜单时父菜单也被包含
|
||
function ensureParentMenusIncluded(selectedIds: string[], menuTree: any[]): string[] {
|
||
const allMenuMap = new Map<number, any>()
|
||
const result = new Set(selectedIds)
|
||
|
||
// 递归收集所有菜单项到map中
|
||
function collectMenus(menus: any[]) {
|
||
menus.forEach((menu) => {
|
||
allMenuMap.set(menu.menuId, menu)
|
||
if (menu.children && menu.children.length > 0) {
|
||
collectMenus(menu.children)
|
||
}
|
||
})
|
||
}
|
||
|
||
collectMenus(menuTree)
|
||
|
||
// 为每个选中的菜单添加其所有父菜单
|
||
selectedIds.forEach((menuIdStr) => {
|
||
const menuId = Number.parseInt(menuIdStr)
|
||
const menu = allMenuMap.get(menuId)
|
||
if (menu && menu.parentId !== 0) {
|
||
let parentId = menu.parentId
|
||
while (parentId && parentId !== 0) {
|
||
result.add(String(parentId))
|
||
const parentMenu = allMenuMap.get(parentId)
|
||
if (parentMenu && parentMenu.parentId !== 0) {
|
||
parentId = parentMenu.parentId
|
||
}
|
||
else {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
return Array.from(result)
|
||
}
|
||
|
||
// 确认分配菜单权限
|
||
async function handleConfirmAssignMenu() {
|
||
if (!currentAssignRole.value) {
|
||
coiMsgError('角色信息缺失')
|
||
return
|
||
}
|
||
|
||
try {
|
||
menuLoading.value = true
|
||
|
||
// 确保选中子菜单时父菜单也被包含
|
||
const completeMenuIds = ensureParentMenusIncluded(checkedKeys.value, menuData.value)
|
||
|
||
// 保持字符串格式避免精度丢失
|
||
const response = await saveRoleMenuPermission(String(currentAssignRole.value.roleId), completeMenuIds)
|
||
|
||
if (response.isSuccess) {
|
||
coiMsgSuccess('菜单权限分配成功')
|
||
menuDialogRef.value?.coiClose()
|
||
}
|
||
else {
|
||
coiMsgError(response.message || '菜单权限分配失败')
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError('菜单权限分配失败,请检查网络连接')
|
||
}
|
||
finally {
|
||
menuLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 取消分配菜单权限
|
||
function handleCancelAssignMenu() {
|
||
menuDialogRef.value?.coiClose()
|
||
currentAssignRole.value = null
|
||
menuData.value = []
|
||
expandedKeys.value = []
|
||
checkedKeys.value = []
|
||
menuModalTitle.value = '分配菜单权限'
|
||
menuLoading.value = false
|
||
// 重置功能按钮状态
|
||
allExpanded.value = false
|
||
allSelected.value = false
|
||
cascadeEnabled.value = true
|
||
showPermissionCode.value = false
|
||
}
|
||
|
||
// 获取所有菜单节点的key
|
||
function getAllMenuKeys(menus: any[]): string[] {
|
||
const keys: string[] = []
|
||
function traverse(nodes: any[]) {
|
||
nodes.forEach((node) => {
|
||
keys.push(node.key)
|
||
if (node.children && node.children.length > 0) {
|
||
traverse(node.children)
|
||
}
|
||
})
|
||
}
|
||
traverse(menus)
|
||
return keys
|
||
}
|
||
|
||
// 展开/折叠所有节点
|
||
function toggleExpandAll() {
|
||
if (allExpanded.value) {
|
||
// 折叠所有
|
||
expandedKeys.value = []
|
||
allExpanded.value = false
|
||
}
|
||
else {
|
||
// 展开所有
|
||
expandedKeys.value = getAllMenuKeys(menuData.value)
|
||
allExpanded.value = true
|
||
}
|
||
}
|
||
|
||
// 全选/全不选
|
||
function toggleSelectAll() {
|
||
if (allSelected.value) {
|
||
// 全不选
|
||
checkedKeys.value = []
|
||
allSelected.value = false
|
||
}
|
||
else {
|
||
// 全选
|
||
checkedKeys.value = getAllMenuKeys(menuData.value)
|
||
allSelected.value = true
|
||
}
|
||
}
|
||
|
||
// 切换父子联动
|
||
function toggleCascade() {
|
||
cascadeEnabled.value = !cascadeEnabled.value
|
||
}
|
||
|
||
// 切换权限标识显示
|
||
function togglePermissionCode() {
|
||
showPermissionCode.value = !showPermissionCode.value
|
||
}
|
||
|
||
// 渲染权限标识
|
||
function renderPermissionCode({ option }: { option: any }) {
|
||
const code = option.auth || option.perms || option.permissionCode || ''
|
||
|
||
// 如果没有权限标识,返回null
|
||
if (!code || code.trim() === '') {
|
||
return null
|
||
}
|
||
|
||
return h(NTag, {
|
||
type: 'primary',
|
||
size: 'small',
|
||
class: 'ml-2 font-mono',
|
||
style: 'font-size: 11px; line-height: 1.2; transform: translateY(-5px); display: inline-flex; align-items: center;',
|
||
}, { default: () => code })
|
||
}
|
||
|
||
// 提交表单
|
||
async function handleSubmit() {
|
||
if (!formRef.value)
|
||
return
|
||
|
||
try {
|
||
// 先进行表单验证
|
||
await formRef.value.validate()
|
||
}
|
||
catch {
|
||
// 表单验证失败,提示用户检查填写内容
|
||
coiMsgWarning('验证失败,请检查填写内容')
|
||
return
|
||
}
|
||
|
||
// 表单验证通过,执行API调用
|
||
try {
|
||
const submitData = { ...formData.value }
|
||
|
||
const { isSuccess } = isEdit.value ? await updateRole(submitData) : await addRole(submitData)
|
||
|
||
if (isSuccess) {
|
||
coiMsgSuccess(isEdit.value ? '角色信息更新成功' : '角色创建成功')
|
||
roleDialogRef.value?.coiClose()
|
||
await getRoleList()
|
||
}
|
||
else {
|
||
coiMsgError(isEdit.value ? '更新失败,请稍后重试' : '创建失败,请稍后重试')
|
||
}
|
||
}
|
||
catch {
|
||
coiMsgError(isEdit.value ? '更新失败,请检查网络连接' : '创建失败,请检查网络连接')
|
||
}
|
||
}
|
||
|
||
// 取消操作
|
||
function handleCancel() {
|
||
roleDialogRef.value?.coiClose()
|
||
}
|
||
|
||
// 判断是否有搜索条件
|
||
function hasSearchConditions() {
|
||
return Object.values(searchForm.value).some((value) => {
|
||
if (value === null || value === undefined || value === '') {
|
||
return false
|
||
}
|
||
if (Array.isArray(value) && value.length === 0) {
|
||
return false
|
||
}
|
||
return true
|
||
})
|
||
}
|
||
|
||
// 获取空状态类型
|
||
function getEmptyType() {
|
||
if (hasSearchConditions()) {
|
||
return 'search'
|
||
}
|
||
return 'default'
|
||
}
|
||
|
||
// 获取空状态标题
|
||
function getEmptyTitle() {
|
||
if (hasSearchConditions()) {
|
||
return '搜索无结果'
|
||
}
|
||
return '暂无角色数据'
|
||
}
|
||
|
||
// 获取空状态描述
|
||
function getEmptyDescription() {
|
||
if (hasSearchConditions()) {
|
||
return '未找到符合搜索条件的角色,请尝试调整搜索条件或重置筛选'
|
||
}
|
||
return '当前还没有角色数据,点击"新增"按钮创建第一个角色'
|
||
}
|
||
|
||
// 获取空状态操作文字
|
||
function getEmptyActionText() {
|
||
if (hasSearchConditions()) {
|
||
return '重置筛选'
|
||
}
|
||
return '新增角色'
|
||
}
|
||
|
||
// 处理空状态操作
|
||
function handleEmptyAction() {
|
||
if (hasSearchConditions()) {
|
||
handleReset()
|
||
}
|
||
else {
|
||
handleAdd()
|
||
}
|
||
}
|
||
// 监听选中状态变化,自动更新全选按钮状态
|
||
watch(checkedKeys, (newKeys) => {
|
||
const allKeys = getAllMenuKeys(menuData.value)
|
||
allSelected.value = allKeys.length > 0 && newKeys.length === allKeys.length
|
||
}, { deep: true })
|
||
|
||
// 监听展开状态变化,自动更新展开按钮状态
|
||
watch(expandedKeys, (newKeys) => {
|
||
const allKeys = getAllMenuKeys(menuData.value)
|
||
allExpanded.value = allKeys.length > 0 && newKeys.length === allKeys.length
|
||
}, { deep: true })
|
||
|
||
// 组件挂载时获取数据
|
||
onMounted(() => {
|
||
getRoleList()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.role-management {
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-td) {
|
||
padding: 7px 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-th) {
|
||
padding: 9px 12px;
|
||
background-color: #fafafa;
|
||
font-weight: normal;
|
||
font-size: 14px;
|
||
color: #262626;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-tr:hover .n-data-table-td) {
|
||
background-color: #f8faff;
|
||
}
|
||
|
||
.search-form :deep(.n-form-item-label) {
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: #262626;
|
||
}
|
||
|
||
/* 操作按钮样式优化 */
|
||
.custom-table :deep(.n-button--small-type) {
|
||
font-size: 12px;
|
||
padding: 4px 8px;
|
||
}
|
||
|
||
/* 标签尺寸优化 */
|
||
.custom-table :deep(.n-tag--small-type) {
|
||
font-size: 11px;
|
||
padding: 2px 6px;
|
||
}
|
||
|
||
/* 自定义滚动条样式 */
|
||
.table-wrapper {
|
||
min-height: 0;
|
||
}
|
||
|
||
.table-wrapper::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
.table-wrapper::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.table-wrapper::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.table-wrapper::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
|
||
.table-wrapper::-webkit-scrollbar-corner {
|
||
background: #f1f1f1;
|
||
}
|
||
|
||
/* 表格滚动条样式 */
|
||
.custom-table :deep(.n-data-table-base-table) {
|
||
overflow: auto;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-track {
|
||
background: #f8f9fa;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb {
|
||
background: #dee2e6;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.custom-table :deep(.n-data-table-base-table)::-webkit-scrollbar-thumb:hover {
|
||
background: #adb5bd;
|
||
}
|
||
|
||
.role-modal :deep(.n-card-header) {
|
||
padding: 24px 24px 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.role-modal :deep(.n-card__content) {
|
||
padding: 24px;
|
||
}
|
||
|
||
/* 优化后的菜单权限分配模态框样式 */
|
||
.menu-assign-modal {
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card) {
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card-header) {
|
||
padding: 24px 28px 0;
|
||
border-bottom: 1px solid #f1f5f9;
|
||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||
border-radius: 16px 16px 0 0;
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card__content) {
|
||
padding: 24px 28px;
|
||
background: #ffffff;
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card__footer) {
|
||
padding: 20px 28px 24px;
|
||
border-top: 1px solid #f1f5f9;
|
||
background: #fafbfc;
|
||
border-radius: 0 0 16px 16px;
|
||
}
|
||
|
||
/* 简洁的菜单树样式 */
|
||
.clean-menu-tree {
|
||
background: transparent;
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node) {
|
||
margin: 2px 0;
|
||
border-radius: 4px;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node:hover) {
|
||
background: rgba(59, 130, 246, 0.05);
|
||
}
|
||
|
||
/* 去掉选中时的背景色 */
|
||
.clean-menu-tree :deep(.n-tree-node--selected) {
|
||
background: transparent !important;
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node--selected:hover) {
|
||
background: rgba(59, 130, 246, 0.05) !important;
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node-content) {
|
||
padding: 6px 8px;
|
||
min-height: 32px;
|
||
align-items: center;
|
||
display: flex;
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node-content__text) {
|
||
flex: 1;
|
||
font-size: 14px;
|
||
color: #374151;
|
||
line-height: 1.5;
|
||
font-weight: 400;
|
||
display: flex;
|
||
align-items: center;
|
||
transform: translateY(-5px);
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-checkbox) {
|
||
margin-right: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 移除选中状态的背景色 */
|
||
.clean-menu-tree :deep(.n-tree-node--selected),
|
||
.clean-menu-tree :deep(.n-tree-node--selected .n-tree-node-content),
|
||
.clean-menu-tree :deep(.n-tree-node-content--selected) {
|
||
background-color: transparent !important;
|
||
}
|
||
|
||
/* 移除悬停状态的背景色变化 */
|
||
.clean-menu-tree :deep(.n-tree-node-content:hover) {
|
||
background-color: transparent !important;
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node-switcher) {
|
||
margin-right: 4px;
|
||
width: 16px;
|
||
height: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transform: translateY(5px);
|
||
}
|
||
|
||
.clean-menu-tree :deep(.n-tree-node-switcher .n-base-icon) {
|
||
font-size: 14px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
/* 隐藏叶子节点的展开图标 */
|
||
.clean-menu-tree :deep(.n-tree-node--leaf .n-tree-node-switcher) {
|
||
visibility: hidden;
|
||
}
|
||
|
||
/* 模态框样式优化 */
|
||
.menu-assign-modal :deep(.n-card) {
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card-header) {
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card__content) {
|
||
padding: 16px 20px;
|
||
}
|
||
|
||
.menu-assign-modal :deep(.n-card__footer) {
|
||
padding: 12px 20px;
|
||
border-top: 1px solid #e5e7eb;
|
||
background: #fafafa;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 900px) {
|
||
.menu-assign-modal :deep(.n-card) {
|
||
margin: 10px;
|
||
width: calc(100% - 20px) !important;
|
||
max-height: calc(100vh - 20px);
|
||
}
|
||
}
|
||
|
||
@media (min-width: 1200px) {
|
||
.menu-assign-modal :deep(.n-card) {
|
||
width: 900px !important;
|
||
}
|
||
}
|
||
|
||
/* 紧凑表单样式 */
|
||
.compact-form :deep(.n-form-item) {
|
||
margin-bottom: 8px !important;
|
||
}
|
||
|
||
.compact-form :deep(.n-form-item .n-form-item-feedback-wrapper) {
|
||
min-height: 0 !important;
|
||
padding-top: 2px !important;
|
||
}
|
||
|
||
.compact-form :deep(.n-form-item .n-form-item-label) {
|
||
padding-bottom: 2px !important;
|
||
}
|
||
|
||
.compact-form :deep(.n-input),
|
||
.compact-form :deep(.n-input-number),
|
||
.compact-form :deep(.n-select),
|
||
.compact-form :deep(.n-radio-group) {
|
||
font-size: 14px !important;
|
||
}
|
||
|
||
.compact-form :deep(.n-input .n-input__input-el),
|
||
.compact-form :deep(.n-input-number .n-input__input-el) {
|
||
padding: 2px 1px !important;
|
||
min-height: 32px !important;
|
||
}
|
||
|
||
.compact-form :deep(.n-select .n-base-selection) {
|
||
min-height: 32px !important;
|
||
padding: 2px 1px !important;
|
||
}
|
||
|
||
.compact-form :deep(.n-select .n-base-selection .n-base-selection-label) {
|
||
padding: 0 6px !important;
|
||
min-height: 30px !important;
|
||
line-height: 30px !important;
|
||
}
|
||
|
||
/* 统一按钮样式系统 - 支持动态主题色彩 */
|
||
.action-btn-primary {
|
||
background: var(--primary-color) !important;
|
||
border-color: var(--primary-color) !important;
|
||
color: #ffffff !important;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
box-shadow: 0 2px 4px var(--primary-color-suppl) !important;
|
||
}
|
||
|
||
.action-btn-primary:hover {
|
||
background: var(--primary-color-hover) !important;
|
||
border-color: var(--primary-color-hover) !important;
|
||
transform: translateY(-1px) !important;
|
||
box-shadow: 0 4px 8px var(--primary-color-suppl) !important;
|
||
}
|
||
|
||
.action-btn-primary:active {
|
||
background: var(--primary-color-pressed) !important;
|
||
border-color: var(--primary-color-pressed) !important;
|
||
transform: translateY(0) !important;
|
||
}
|
||
|
||
.action-btn-secondary {
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08) !important;
|
||
}
|
||
|
||
.action-btn-secondary:hover {
|
||
transform: translateY(-1px) !important;
|
||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12) !important;
|
||
}
|
||
|
||
.action-btn-secondary:active {
|
||
transform: translateY(0) !important;
|
||
}
|
||
|
||
.action-btn-danger {
|
||
border-color: var(--error-color) !important;
|
||
color: var(--error-color) !important;
|
||
}
|
||
|
||
.action-btn-danger:hover {
|
||
border-color: var(--error-color-hover) !important;
|
||
color: var(--error-color-hover) !important;
|
||
background: var(--error-color-suppl) !important;
|
||
}
|
||
|
||
.action-btn-warning {
|
||
border-color: var(--warning-color) !important;
|
||
color: var(--warning-color) !important;
|
||
}
|
||
|
||
.action-btn-warning:hover {
|
||
border-color: var(--warning-color-hover) !important;
|
||
color: var(--warning-color-hover) !important;
|
||
background: var(--warning-color-suppl) !important;
|
||
}
|
||
|
||
.action-btn-info {
|
||
border-color: var(--info-color) !important;
|
||
color: var(--info-color) !important;
|
||
}
|
||
|
||
.action-btn-info:hover {
|
||
border-color: var(--info-color-hover) !important;
|
||
color: var(--info-color-hover) !important;
|
||
background: var(--info-color-suppl) !important;
|
||
}
|
||
</style>
|