| | |
| | | <template> |
| | | <ElDialog |
| | | v-model="visible" |
| | | title="菜单权限" |
| | | width="520px" |
| | | align-center |
| | | class="el-dialog-border" |
| | | @close="handleClose" |
| | | <ElDrawer |
| | | :model-value="visible" |
| | | title="角色权限" |
| | | size="860px" |
| | | destroy-on-close |
| | | @update:model-value="handleVisibleChange" |
| | | @closed="handleClosed" |
| | | > |
| | | <ElScrollbar height="70vh"> |
| | | <ElTree |
| | | ref="treeRef" |
| | | :data="processedMenuList" |
| | | show-checkbox |
| | | node-key="name" |
| | | :default-expand-all="isExpandAll" |
| | | :default-checked-keys="[1, 2, 3]" |
| | | :props="defaultProps" |
| | | @check="handleTreeCheck" |
| | | > |
| | | <template #default="{ data }"> |
| | | <div style="display: flex; align-items: center"> |
| | | <span v-if="data.isAuth"> |
| | | {{ data.label }} |
| | | </span> |
| | | <span v-else>{{ defaultProps.label(data) }}</span> |
| | | </div> |
| | | </template> |
| | | </ElTree> |
| | | </ElScrollbar> |
| | | <template #footer> |
| | | <ElButton @click="outputSelectedData" style="margin-left: 8px">获取选中数据</ElButton> |
| | | <div class="mb-4 text-sm text-[var(--art-text-secondary)]"> |
| | | 当前角色:{{ roleLabel }} |
| | | </div> |
| | | |
| | | <ElButton @click="toggleExpandAll">{{ isExpandAll ? '全部收起' : '全部展开' }}</ElButton> |
| | | <ElButton @click="toggleSelectAll" style="margin-left: 8px">{{ |
| | | isSelectAll ? '取消全选' : '全部选择' |
| | | }}</ElButton> |
| | | <ElButton type="primary" @click="savePermission">保存</ElButton> |
| | | </template> |
| | | </ElDialog> |
| | | <ElTabs v-model="activeScopeType" class="role-scope-tabs"> |
| | | <ElTabPane |
| | | v-for="config in scopeConfigs" |
| | | :key="config.scopeType" |
| | | :label="config.title" |
| | | :name="config.scopeType" |
| | | > |
| | | <div v-if="scopeState[config.scopeType].loading" class="py-6"> |
| | | <ElSkeleton :rows="10" animated /> |
| | | </div> |
| | | <div v-else class="space-y-3"> |
| | | <div class="flex items-center justify-between gap-3"> |
| | | <ElSpace wrap> |
| | | <ElButton @click="handleSelectAll(config.scopeType)">全选</ElButton> |
| | | <ElButton @click="handleClear(config.scopeType)">清空</ElButton> |
| | | </ElSpace> |
| | | <ElButton type="primary" @click="handleSave(config.scopeType)">保存当前权限</ElButton> |
| | | </div> |
| | | |
| | | <div class="flex items-center gap-3"> |
| | | <ElInput |
| | | v-model.trim="scopeState[config.scopeType].condition" |
| | | clearable |
| | | placeholder="搜索权限树" |
| | | @clear="handleSearch(config.scopeType)" |
| | | @keyup.enter="handleSearch(config.scopeType)" |
| | | /> |
| | | <ElButton @click="handleSearch(config.scopeType)">搜索</ElButton> |
| | | </div> |
| | | |
| | | <ElScrollbar height="56vh"> |
| | | <ElTree |
| | | :ref="(el) => setTreeRef(config.scopeType, el)" |
| | | :data="scopeState[config.scopeType].treeData" |
| | | node-key="id" |
| | | show-checkbox |
| | | :default-expand-all="true" |
| | | :default-checked-keys="scopeState[config.scopeType].checkedKeys" |
| | | :props="treeProps" |
| | | @check="handleTreeCheck(config.scopeType)" |
| | | > |
| | | <template #default="{ data }"> |
| | | <div class="flex items-center gap-2"> |
| | | <span>{{ resolveScopeNodeLabel(data) }}</span> |
| | | <ElTag v-if="data.isAuthButton" type="info" effect="plain" size="small"> |
| | | 按钮 |
| | | </ElTag> |
| | | </div> |
| | | </template> |
| | | </ElTree> |
| | | </ElScrollbar> |
| | | </div> |
| | | </ElTabPane> |
| | | </ElTabs> |
| | | </ElDrawer> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { useMenuStore } from '@/store/modules/menu' |
| | | import { formatMenuTitle } from '@/utils/router' |
| | | import { |
| | | buildRoleScopeSubmitPayload, |
| | | getRoleScopeConfig, |
| | | normalizeRoleScopeTreeData |
| | | } from '../rolePage.helpers' |
| | | import { fetchGetRoleScopeList, fetchGetRoleScopeTree, fetchUpdateRoleScope } from '@/api/system-manage' |
| | | import { resolveBackendMenuTitle } from '@/utils/backend-menu-title' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | | const props = defineProps({ |
| | | modelValue: { required: false, default: false }, |
| | | roleData: { required: false, default: void 0 } |
| | | visible: { required: false, default: false }, |
| | | roleData: { required: false, default: () => ({}) }, |
| | | scopeType: { required: false, default: 'menu' } |
| | | }) |
| | | const emit = defineEmits(['update:modelValue', 'success']) |
| | | const { menuList } = storeToRefs(useMenuStore()) |
| | | const treeRef = ref() |
| | | const isExpandAll = ref(true) |
| | | const isSelectAll = ref(false) |
| | | const visible = computed({ |
| | | get: () => props.modelValue, |
| | | set: (value) => emit('update:modelValue', value) |
| | | }) |
| | | const processedMenuList = computed(() => { |
| | | const processNode = (node) => { |
| | | const processed = { ...node } |
| | | if (node.meta?.authList?.length) { |
| | | const authNodes = node.meta.authList.map((auth) => ({ |
| | | id: `${node.id}_${auth.authMark}`, |
| | | name: `${node.name}_${auth.authMark}`, |
| | | label: auth.title, |
| | | authMark: auth.authMark, |
| | | isAuth: true, |
| | | checked: auth.checked || false |
| | | })) |
| | | processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes |
| | | } |
| | | if (processed.children) { |
| | | processed.children = processed.children.map(processNode) |
| | | } |
| | | return processed |
| | | } |
| | | return menuList.value.map(processNode) |
| | | }) |
| | | const defaultProps = { |
| | | children: 'children', |
| | | label: (data) => formatMenuTitle(data.meta?.title) || data.label || '' |
| | | |
| | | const emit = defineEmits(['update:visible', 'success']) |
| | | |
| | | const scopeConfigs = ['menu', 'pda', 'matnr', 'warehouse'].map((scopeType) => getRoleScopeConfig(scopeType)) |
| | | const activeScopeType = ref(props.scopeType || 'menu') |
| | | const treeRefs = reactive({}) |
| | | const treeProps = { |
| | | label: 'label', |
| | | children: 'children' |
| | | } |
| | | watch( |
| | | () => props.modelValue, |
| | | (newVal) => { |
| | | if (newVal && props.roleData) { |
| | | console.log('设置权限:', props.roleData) |
| | | } |
| | | } |
| | | const scopeState = reactive( |
| | | Object.fromEntries( |
| | | scopeConfigs.map((config) => [ |
| | | config.scopeType, |
| | | { |
| | | loading: false, |
| | | loaded: false, |
| | | treeData: [], |
| | | checkedKeys: [], |
| | | halfCheckedKeys: [], |
| | | condition: '' |
| | | } |
| | | ]) |
| | | ) |
| | | ) |
| | | const handleClose = () => { |
| | | visible.value = false |
| | | treeRef.value?.setCheckedKeys([]) |
| | | } |
| | | const savePermission = () => { |
| | | ElMessage.success('权限保存成功') |
| | | emit('success') |
| | | handleClose() |
| | | } |
| | | const toggleExpandAll = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | const nodes = tree.store.nodesMap |
| | | Object.values(nodes).forEach((node) => { |
| | | node.expanded = !isExpandAll.value |
| | | }) |
| | | isExpandAll.value = !isExpandAll.value |
| | | } |
| | | const toggleSelectAll = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | if (!isSelectAll.value) { |
| | | const allKeys = getAllNodeKeys(processedMenuList.value) |
| | | tree.setCheckedKeys(allKeys) |
| | | } else { |
| | | tree.setCheckedKeys([]) |
| | | |
| | | const visible = computed({ |
| | | get: () => props.visible, |
| | | set: (value) => emit('update:visible', value) |
| | | }) |
| | | |
| | | const roleLabel = computed(() => props.roleData?.name || props.roleData?.code || '未选择角色') |
| | | |
| | | const loadScopeData = async (scopeType, { reloadSelection = true } = {}) => { |
| | | const config = getRoleScopeConfig(scopeType) |
| | | const state = scopeState[scopeType] |
| | | state.loading = true |
| | | try { |
| | | const requests = [fetchGetRoleScopeTree(config.scopeType, { condition: state.condition || '' })] |
| | | if (reloadSelection) { |
| | | requests.unshift(fetchGetRoleScopeList(config.scopeType, props.roleData.id)) |
| | | } |
| | | |
| | | const [checkedIds, treeData] = reloadSelection ? await Promise.all(requests) : [state.checkedKeys, await requests[0]] |
| | | state.treeData = normalizeRoleScopeTreeData(config.scopeType, treeData) |
| | | state.checkedKeys = normalizeScopeKeys(checkedIds) |
| | | state.halfCheckedKeys = [] |
| | | state.loaded = true |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || `加载${config.title}失败`) |
| | | } finally { |
| | | state.loading = false |
| | | nextTick(() => { |
| | | treeRefs[scopeType]?.setCheckedKeys(scopeState[scopeType].checkedKeys) |
| | | }) |
| | | } |
| | | isSelectAll.value = !isSelectAll.value |
| | | } |
| | | |
| | | const ensureScopeLoaded = async (scopeType, options = {}) => { |
| | | if (!props.roleData?.id || !scopeType) { |
| | | return |
| | | } |
| | | |
| | | const { force = false, reloadSelection = true } = options |
| | | if (!force && scopeState[scopeType].loaded) { |
| | | return |
| | | } |
| | | |
| | | await loadScopeData(scopeType, { reloadSelection }) |
| | | } |
| | | |
| | | const normalizeScopeKeys = (keys = []) => { |
| | | if (!Array.isArray(keys)) { |
| | | return [] |
| | | } |
| | | |
| | | return Array.from( |
| | | new Set( |
| | | keys |
| | | .map((key) => normalizeScopeKey(key)) |
| | | .filter((key) => key !== '') |
| | | ) |
| | | ) |
| | | } |
| | | |
| | | const normalizeScopeKey = (value) => { |
| | | if (value === '' || value === null || value === void 0) { |
| | | return '' |
| | | } |
| | | const numeric = Number(value) |
| | | if (Number.isNaN(numeric)) { |
| | | return String(value) |
| | | } |
| | | return String(numeric) |
| | | } |
| | | |
| | | const setTreeRef = (scopeType, el) => { |
| | | if (el) { |
| | | treeRefs[scopeType] = el |
| | | } |
| | | } |
| | | |
| | | const handleTreeCheck = (scopeType) => { |
| | | const tree = treeRefs[scopeType] |
| | | if (!tree) return |
| | | scopeState[scopeType].checkedKeys = normalizeScopeKeys(tree.getCheckedKeys()) |
| | | scopeState[scopeType].halfCheckedKeys = normalizeScopeKeys(tree.getHalfCheckedKeys()) |
| | | } |
| | | |
| | | const handleSelectAll = (scopeType) => { |
| | | const tree = treeRefs[scopeType] |
| | | if (!tree) return |
| | | const allKeys = getAllNodeKeys(scopeState[scopeType].treeData) |
| | | tree.setCheckedKeys(allKeys) |
| | | handleTreeCheck(scopeType) |
| | | } |
| | | |
| | | const handleClear = (scopeType) => { |
| | | const tree = treeRefs[scopeType] |
| | | if (!tree) return |
| | | tree.setCheckedKeys([]) |
| | | handleTreeCheck(scopeType) |
| | | } |
| | | |
| | | const handleSave = async (scopeType) => { |
| | | if (!props.roleData?.id) return |
| | | try { |
| | | await fetchUpdateRoleScope( |
| | | scopeType, |
| | | buildRoleScopeSubmitPayload( |
| | | props.roleData.id, |
| | | scopeState[scopeType].checkedKeys, |
| | | scopeState[scopeType].halfCheckedKeys |
| | | ) |
| | | ) |
| | | ElMessage.success('权限保存成功') |
| | | emit('success') |
| | | visible.value = false |
| | | } catch (error) { |
| | | ElMessage.error(error?.message || '权限保存失败') |
| | | } |
| | | } |
| | | |
| | | const handleVisibleChange = (value) => { |
| | | visible.value = value |
| | | } |
| | | |
| | | const handleSearch = async (scopeType) => { |
| | | await loadScopeData(scopeType, { reloadSelection: false }) |
| | | } |
| | | |
| | | const resolveScopeNodeLabel = (data) => { |
| | | const rawLabel = typeof data?.label === 'string' ? data.label.trim() : '' |
| | | if (!rawLabel) { |
| | | return '' |
| | | } |
| | | return resolveBackendMenuTitle(rawLabel) |
| | | } |
| | | |
| | | const handleClosed = () => { |
| | | activeScopeType.value = props.scopeType || 'menu' |
| | | Object.keys(scopeState).forEach((key) => { |
| | | scopeState[key].loading = false |
| | | scopeState[key].loaded = false |
| | | scopeState[key].treeData = [] |
| | | scopeState[key].checkedKeys = [] |
| | | scopeState[key].halfCheckedKeys = [] |
| | | scopeState[key].condition = '' |
| | | }) |
| | | } |
| | | |
| | | const getAllNodeKeys = (nodes) => { |
| | | const keys = [] |
| | | const traverse = (nodeList) => { |
| | | nodeList.forEach((node) => { |
| | | if (node.name) keys.push(node.name) |
| | | if (node.children?.length) traverse(node.children) |
| | | if (node.id !== void 0 && node.id !== null && node.id !== '') { |
| | | keys.push(String(node.id)) |
| | | } |
| | | if (node.children?.length) { |
| | | traverse(node.children) |
| | | } |
| | | }) |
| | | } |
| | | traverse(nodes) |
| | | traverse(Array.isArray(nodes) ? nodes : []) |
| | | return keys |
| | | } |
| | | const handleTreeCheck = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | const checkedKeys = tree.getCheckedKeys() |
| | | const allKeys = getAllNodeKeys(processedMenuList.value) |
| | | isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0 |
| | | } |
| | | const outputSelectedData = () => { |
| | | const tree = treeRef.value |
| | | if (!tree) return |
| | | const selectedData = { |
| | | checkedKeys: tree.getCheckedKeys(), |
| | | halfCheckedKeys: tree.getHalfCheckedKeys(), |
| | | checkedNodes: tree.getCheckedNodes(), |
| | | halfCheckedNodes: tree.getHalfCheckedNodes(), |
| | | totalChecked: tree.getCheckedKeys().length, |
| | | totalHalfChecked: tree.getHalfCheckedKeys().length |
| | | |
| | | watch( |
| | | () => props.visible, |
| | | async (isVisible) => { |
| | | if (isVisible) { |
| | | activeScopeType.value = props.scopeType || 'menu' |
| | | await ensureScopeLoaded(activeScopeType.value, { force: true }) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | watch( |
| | | () => props.scopeType, |
| | | async (scopeType) => { |
| | | if (scopeType) { |
| | | activeScopeType.value = scopeType |
| | | if (props.visible) { |
| | | await ensureScopeLoaded(scopeType, { force: true }) |
| | | } |
| | | } |
| | | } |
| | | console.log('=== 选中的权限数据 ===', selectedData) |
| | | ElMessage.success(`已输出选中数据到控制台,共选中 ${selectedData.totalChecked} 个节点`) |
| | | } |
| | | ) |
| | | |
| | | watch( |
| | | activeScopeType, |
| | | async (scopeType) => { |
| | | if (props.visible && scopeType) { |
| | | await ensureScopeLoaded(scopeType) |
| | | } |
| | | } |
| | | ) |
| | | </script> |