src/views/archiveManagement/index.vue
@@ -34,13 +34,21 @@
            :expand-on-click-node="false"
            :filter-node-method="filterNode"
            :props="props"
            :lazy="false"
            :load="undefined"
            :render-after-expand="true"
            :auto-expand-parent="true"
            :indent="20"
            class="custom-tree"
            node-key="id"
            @node-click="handleNodeClick"
            @node-expand="handleNodeExpand"
          >
            <template #default="{ node, data }">
              <div
                class="tree-node-content"
                :data-temp-id="data._tempId"
                :data-node-id="data.id"
                @dblclick="headerDbClick(node, data)"
              >
                <div class="node-icon">
@@ -61,7 +69,7 @@
                  }}</span>
                  <el-input
                    v-else
                    :ref="(el) => setInputRef(el, data)"
                    :ref="(el) => setInputRef(data.id || data._tempId, el)"
                    v-model="newName"
                    autofocus
                    class="tree-input"
@@ -69,6 +77,7 @@
                    size="small"
                    @blur="(event) => handleInputBlur(event, data, node)"
                    @keyup.enter="(event) => handleInputBlur(event, data, node)"
                    @keyup.esc="() => cancelEdit(data, node)"
                  />
                </div>
                <div v-show="!data.isEdit" class="node-actions">
@@ -76,7 +85,8 @@
                    icon="Plus"
                    link
                    size="small"
                    title="新增子节点"
                    :title="getNodeDepth(data) >= 7 ? '已达到最大嵌套层级(7层)' : '新增子节点'"
                    :disabled="getNodeDepth(data) >= 7"
                    @click.stop="append(data)"
                  ></el-button>
                  <el-button
@@ -165,7 +175,7 @@
  </el-card>
</template>
<script setup>
import { nextTick, onMounted, reactive, ref } from "vue";
import { computed, nextTick, onMounted, reactive, ref } from "vue";
import ETable from "@/components/Table/ETable.vue";
import {
  ElButton,
@@ -226,6 +236,41 @@
  label: "name",
  children: "children",
  isLeaf: "leaf",
};
// ===== 计算属性 =====
// 计算总节点数
const totalNodeCount = computed(() => {
  const countNodes = (nodes) => {
    let count = 0;
    for (const node of nodes || []) {
      count += 1;
      if (node.children) {
        count += countNodes(node.children);
      }
    }
    return count;
  };
  return countNodes(treeData.value);
});
// 检查是否为大量节点(超过1000个节点时提示性能优化)
const isLargeTree = computed(() => totalNodeCount.value > 1000);
// 获取节点深度的函数
const getNodeDepth = (nodeData) => {
  if (!nodeData || !nodeData.id) return 0;
  let depth = 1;
  const node = treeRef.value?.getNode(nodeData.id);
  let parentNode = node?.parent;
  while (parentNode && parentNode.data && parentNode.data.id) {
    depth++;
    parentNode = parentNode.parent;
  }
  return depth;
};
// ===== 工具函数 =====
@@ -337,6 +382,20 @@
  getArchiveListData();
};
// 节点展开事件处理
const handleNodeExpand = (data, node, instance) => {
  // 展开后稍微延迟,确保子节点渲染完成
  setTimeout(() => {
    // 如果有新添加的编辑状态节点,聚焦到它
    if (data.children && data.children.length > 0) {
      const editingChild = data.children.find(child => child.isEdit && child._tempId);
      if (editingChild) {
        focusInput(editingChild._tempId, 200);
      }
    }
  }, 100);
};
const handlePageChange = (pagination) => {
  try {
    const { page, limit } = pagination;
@@ -427,22 +486,62 @@
  }
};
// ===== 树节点编辑函数 =====
const setInputRef = (el, data) => {
  if (el) {
    inputRefs.value.set(data.id || data, el);
const setInputRef = (key, el) => {
  if (el && key) {
    inputRefs.value.set(key, el);
  }
};
const headerDbClick = (node, data) => {
  data.isEdit = true;
  newName.value = data.name;
  nextTick(() => {
    const inputEl = inputRefs.value.get(data.id || data);
    if (inputEl) {
      inputEl.focus();
      inputEl.select();
  try {
    data.isEdit = true;
    newName.value = data.name;
    nextTick(() => {
      const key = data._tempId || data.id;
      if (key) {
        focusInput(key, 50);
      }
    });
  } catch (error) {
    console.error('进入编辑模式失败:', error);
    ElMessage.error('进入编辑模式失败');
  }
};
// 取消编辑功能
const cancelEdit = (data, node) => {
  try {
    data.isEdit = false;
    // 如果是新创建的临时节点,删除它
    if (data._tempId && !data.id) {
      if (node.parent) {
        const parent = node.parent.data;
        const index = parent.children?.indexOf(data);
        if (index > -1) {
          parent.children.splice(index, 1);
        }
      } else {
        // 根节点
        const index = treeData.value.indexOf(data);
        if (index > -1) {
          treeData.value.splice(index, 1);
        }
      }
      // 清理输入框引用
      const key = data._tempId;
      if (inputRefs.value.has(key)) {
        inputRefs.value.delete(key);
      }
    }
  });
    // 重置名称
    newName.value = "";
  } catch (error) {
    console.error('取消编辑失败:', error);
  }
};
const expandParentNodes = (node) => {
@@ -460,11 +559,21 @@
    comeTreeData.isEdit = false;
    const newValue = newName.value.trim();
    if (comeTreeData.name === newValue) return;
    // 如果名称为空,处理空名称情况
    if (!newValue) {
      newName.value = comeTreeData.name || "新节点";
      // 如果是新创建的临时节点,删除它
      if (comeTreeData._tempId && !comeTreeData.id) {
        cancelEdit(comeTreeData, node);
      } else {
        // 已存在的节点,恢复原名称
        newName.value = comeTreeData.name || "新节点";
      }
      ElMessage.warning("节点名称不能为空");
      return;
    }
    // 如果名称没有改变,直接返回
    if (comeTreeData.name === newValue) {
      return;
    }
@@ -480,6 +589,20 @@
      comeTreeData.name = newValue;
      if (!comeTreeData.id && result.data) {
        comeTreeData.id = result.data.id || result.data;
        // 清理临时ID
        if (comeTreeData._tempId) {
          const tempKey = comeTreeData._tempId;
          delete comeTreeData._tempId;
          // 更新引用映射
          if (inputRefs.value.has(tempKey)) {
            const inputEl = inputRefs.value.get(tempKey);
            inputRefs.value.delete(tempKey);
            if (comeTreeData.id && inputEl) {
              inputRefs.value.set(comeTreeData.id, inputEl);
            }
          }
        }
      }
      showSuccess("保存成功");
@@ -490,18 +613,42 @@
        if (currentNodeId && treeRef.value) {
          const targetNode = treeRef.value.getNode(currentNodeId);
          if (targetNode) {
            // 展开当前节点
            targetNode.expanded = true;
            // 展开所有父节点
            expandParentNodes(targetNode);
            // 滚动到节点位置
            setTimeout(() => {
              const nodeElement = document.querySelector(`[data-node-id="${currentNodeId}"]`);
              if (nodeElement) {
                nodeElement.scrollIntoView({
                  behavior: 'smooth',
                  block: 'center'
                });
              }
            }, 300);
          }
        }
      });
    } else {
      comeTreeData.name = comeTreeData.name || "新节点";
      // 保存失败,恢复原名称或删除临时节点
      if (comeTreeData._tempId && !comeTreeData.id) {
        cancelEdit(comeTreeData, node);
      } else {
        comeTreeData.name = comeTreeData.name || "新节点";
      }
      ElMessage.error("保存失败: " + (result.msg || "未知错误"));
    }
  } catch (error) {
    handleError(error, "保存节点失败");
    comeTreeData.name = comeTreeData.name || "新节点";
    // 出错时处理临时节点
    if (comeTreeData._tempId && !comeTreeData.id) {
      cancelEdit(comeTreeData, node);
    } else {
      comeTreeData.name = comeTreeData.name || "新节点";
    }
  }
};
@@ -509,51 +656,167 @@
const createNewNode = (name, isEdit = true) => ({
  name,
  isEdit,
  _tempId: Date.now() + Math.random(), // 添加临时ID
});
const focusInput = (nodeData, delay = 50) => {
const focusInput = (nodeKey, delay = 100) => {
  setTimeout(() => {
    const inputEl = inputRefs.value.get(nodeData.id || nodeData);
    const inputEl = inputRefs.value.get(nodeKey);
    if (inputEl) {
      inputEl.focus();
      inputEl.select();
      inputEl.$el?.scrollIntoView?.({
        behavior: "smooth",
        block: "nearest",
      });
      try {
        // 先滚动到可视区域
        inputEl.$el?.scrollIntoView?.({
          behavior: "smooth",
          block: "center",
        });
        // 聚焦并选中所有文本,确保可以直接编辑
        setTimeout(() => {
          inputEl.focus();
          inputEl.select();
          // 确保光标在输入框末尾(如果select失败的话)
          const inputElement = inputEl.ref || inputEl.input || inputEl.$el?.querySelector('input');
          if (inputElement) {
            inputElement.setSelectionRange(0, inputElement.value.length);
            // 确保输入框在视口中央
            setTimeout(() => {
              inputElement.scrollIntoView({
                behavior: 'smooth',
                block: 'center'
              });
            }, 100);
          }
        }, 100);
      } catch (error) {
        console.warn('聚焦输入框失败:', error);
        // 备用方案:直接聚焦
        try {
          inputEl.focus();
          // 尝试选中文本
          setTimeout(() => {
            const inputElement = inputEl.ref || inputEl.input || inputEl.$el?.querySelector('input');
            if (inputElement) {
              inputElement.select();
            }
          }, 200);
        } catch (e) {
          console.warn('备用聚焦方案也失败:', e);
        }
      }
    } else {
      console.warn('未找到输入框元素,key:', nodeKey);
    }
  }, delay);
};
const append = async (data) => {
  if (data === "") {
    // 新增根节点
    const newNode = createNewNode("新节点");
    treeData.value.push(newNode);
    newName.value = "新节点";
  try {
    // 检查嵌套层级限制
    const getNodeDepth = (nodeData, currentDepth = 1) => {
      if (!nodeData || data === "") return 0; // 根节点不算层级
      let depth = currentDepth;
      let current = nodeData;
      // 通过树组件获取父节点来计算深度
      if (current.id) {
        const node = treeRef.value?.getNode(current.id);
        let parentNode = node?.parent;
        while (parentNode && parentNode.data && parentNode.data.id) {
          depth++;
          parentNode = parentNode.parent;
        }
      }
      return depth;
    };
    nextTick(() => focusInput(newNode));
  } else {
    const hasChildren = data.children;
    const nodeKey = data.id || data;
    const node = treeRef.value?.getNode(nodeKey);
    const isExpanded = node?.expanded;
    // 如果有子级且未展开,先展开节点
    if (hasChildren && !isExpanded && treeRef.value?.store?.nodesMap[nodeKey]) {
      treeRef.value.store.nodesMap[nodeKey].expanded = true;
    const currentDepth = getNodeDepth(data);
    // 限制最多7层嵌套
    if (currentDepth >= 7) {
      ElMessage.warning('最多只能嵌套7层节点,当前已达到最大层级限制');
      return;
    }
    const newNode = createNewNode("新子节点");
    if (!data.children) {
      data.children = [];
    // 在大量节点时提示性能注意事项
    if (isLargeTree.value && totalNodeCount.value > 2000) {
      const confirmed = await ElMessageBox.confirm(
        `当前树结构包含 ${totalNodeCount.value} 个节点,节点较多可能影响性能。建议考虑分层管理。是否继续添加?`,
        '性能提示',
        {
          confirmButtonText: '继续添加',
          cancelButtonText: '取消',
          type: 'warning',
        }
      ).catch(() => false);
      if (!confirmed) return;
    }
    data.children.push(newNode);
    newName.value = "新子节点";
    const delay = hasChildren && !isExpanded ? 200 : 50;
    nextTick(() => focusInput(newNode, delay));
    if (data === "") {
      // 新增根节点
      const newNode = createNewNode("新节点");
      treeData.value.push(newNode);
      newName.value = "新节点";
      await nextTick();
      focusInput(newNode._tempId, 200);
    } else {
      const hasChildren = data.children && data.children.length > 0;
      const nodeKey = data.id || data;
      const node = treeRef.value?.getNode(nodeKey);
      // 创建新子节点
      const newNode = createNewNode("新子节点");
      if (!data.children) {
        data.children = [];
      }
      data.children.push(newNode);
      newName.value = "新子节点";
      // 强制更新树结构
      await nextTick();
      // 确保父节点展开以显示新节点
      if (node) {
        node.expanded = true;
        // 如果是第一次添加子节点,等待展开动画并确保可见
        if (!hasChildren) {
          await new Promise(resolve => setTimeout(resolve, 300));
          // 展开后滚动到新节点位置
          setTimeout(() => {
            const newNodeElement = document.querySelector(`[data-temp-id="${newNode._tempId}"]`);
            if (newNodeElement) {
              newNodeElement.scrollIntoView({
                behavior: 'smooth',
                block: 'center'
              });
            }
          }, 100);
        }
        // 展开所有父节点确保完全可见
        let parentNode = node.parent;
        while (parentNode && parentNode.data && parentNode.data.id) {
          parentNode.expanded = true;
          parentNode = parentNode.parent;
        }
      }
      // 聚焦到新创建的输入框
      const delay = hasChildren ? 150 : 500; // 如果之前没有子节点,延迟更长时间等展开
      focusInput(newNode._tempId, delay);
    }
  } catch (error) {
    console.error('新增节点失败:', error);
    ElMessage.error('新增节点失败,请重试');
  }
};
@@ -618,18 +881,27 @@
  border-radius: 8px;
  background: #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  max-height: calc(100vh - 240px); // 限制最大高度,启用滚动
  .custom-tree {
    padding: 8px;
    background: transparent;
    // 使用GPU加速提升滚动性能
    transform: translateZ(0);
    will-change: scroll-position;
    :deep(.el-tree-node) {
      // 减少不必要的重绘
      contain: layout style;
      .el-tree-node__content {
        height: 36px;
        padding: 0 8px;
        border-radius: 6px;
        margin: 2px 0;
        transition: all 0.2s ease;
        // 优化渲染性能
        will-change: background-color;
        &:hover {
          background-color: #f0f9ff;
@@ -709,13 +981,19 @@
      color: #909399;
      min-height: auto;
      &:hover {
      &:hover:not(:disabled) {
        color: #1890ff;
        background-color: #f0f9ff;
      }
      &.el-button--text:hover {
      &.el-button--text:hover:not(:disabled) {
        background-color: #f0f9ff;
      }
      &:disabled {
        color: #c0c4cc;
        cursor: not-allowed;
        background-color: transparent;
      }
    }
  }
@@ -731,11 +1009,13 @@
  :deep(.el-input__wrapper) {
    border-radius: 4px;
    border: 1px solid #d9d9d9;
    border: 1px solid #40a9ff;
    transition: all 0.2s ease;
    box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.1);
    &:hover {
      border-color: #40a9ff;
      border-color: #1890ff;
      box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.15);
    }
    &.is-focus {
@@ -748,10 +1028,15 @@
    padding: 4px 8px;
    font-size: 14px;
    color: #303133;
    background-color: #fff;
    &::placeholder {
      color: #bfbfbf;
    }
    &:focus {
      background-color: #f8fcff;
    }
  }
}