zhou zhou
12 小时以前 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa
rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -1,153 +1,304 @@
<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>