element目录树组件el-tree使用相关笔记

文章详细介绍了Vue中el-tree组件的配置、懒加载数据处理、递归遍历、搜索功能、节点高亮、图标添加、可编辑树以及点击事件的处理。

默认配置

            <el-tree
              ref="tree"
              highlight-current // 高亮当前选中节点
              accordion // 只打开一个同级树节点展开
              :data="treeData" 
              :expand-on-click-node="false" // 单击节点是否展开(懒加载模式下无效)
              :props="treeDefaultProps" // 映射属性值
              node-key="id" // 标识属性
              :default-expanded-keys="['-1']"  // 默认展开的节点
              @node-click="handleNodeClick" 
              :filter-node-method="filterNode" // 对树节点进行筛选时执行的方法,返回 true则显示
              :check-on-click-node="true" // 点击节点时选中
            >
data() {
   
   
    return {
   
    
	   	treeData:[
                id: '-1',
                label: '全部',
                leaf: false,
                children: [
				{
   
   
				...
				},
				{
   
   
				...
				}
			],
		],
		treeDefaultProps:{
   
   
        children: 'children',
        label: 'label',
        isLeaf: 'leaf',
      },
    }        
}

懒加载

   <el-tree
     :load="queryTreeData"
     lazy
   >
    // 获取树目录
    queryTreeData(node, resolve) {
   
   
      const {
   
    level } = node
      // nodeId 为 -1代表第一次请求
      let nodeId = level == 0 ? '-1' : node.data.id
      const params = {
   
   
        displayTable: true,
        parentId: nodeId,
      }
      api
        .queryDirectoryLazyLoad(params)
        .then((res) => {
   
   
          const nodes = res.map((item) => ({
   
   
            id: item.id,
            label: item.name,
            leaf: !item.hasChildren,
          }))
          // 初始化请求
          if (level == 0) {
   
   
            resolve([
              {
   
   
                id: '-1',
                label: '全部',
                leaf: false,
                children: nodes, // 非必要
              },
            ])
          } else {
   
   
            resolve(nodes)
          }
        })
        .then(() => {
   
   
          this.$refs.tree.setCurrentKey(this.directoryId)
        })
        .catch((error) => {
   
   
          console.log(error)
        })
    },

每一级分页懒加载

(待补充)

递归处理数据

递归遍历树级结构,进行字段映射

    mapTree(data) {
   
   
      const haveChildren =
        Array.isArray(data.childNode) && data.childNode.length
      return {
   
   
        label: data.nodeName,
        id: data.nodeId,
        children: haveChildren
          ? data.childNode.map((i) => this
修改代码,添加文件时要求弹出文件名输入框。 <template> <div class="markdown-editor-container"> <!-- 主体布局 --> <el-container class="main-layout"> <!-- 左侧文件树 --> <el-aside width="300px" class="file-tree-container"> <div class="tree-header"> <div> <span style="margin: 10px;">知识库</span> </div> <div style="align-items: right;"> <el-button text @click="handleRefreshFileTree"> <el-icon> <Refresh /> </el-icon> </el-button> <el-button text @click="handleCreateNewFile"> <el-icon> <DocumentAdd /> </el-icon> </el-button> <el-button text @click="handlecreateNewFolder"> <el-icon> <FolderAdd /> </el-icon> </el-button> </div> </div> <el-scrollbar max-height="calc(100vh - 120px)"> <el-tree ref="treeRef" :data="fileTreeData" :props="treeProps" node-key="id" :default-expanded-keys="['root']" :default-selected-keys="selectedFileId ? [selectedFileId] : []" @node-click="handleTreeNodeClick" @node-contextmenu="handleRightClick" draggable :allow-drag="allowDrag" :allow-drop="allowDrop" highlight-current> <template #default="{ node, data }"> <div class="tree-node-content"> <el-icon v-if="data.is_dir" class="node-icon"> <Folder /> </el-icon> <el-icon v-else class="node-icon"> <Document /> </el-icon> <span class="node-label">{{ data.name }}</span> <div class="node-actions" v-if="data.id !== 'root'"> <el-button v-if="!data.is_dir" type="text" size="small" icon="Edit" @click.stop="renameFile(data)" /> <el-button type="text" size="small" icon="Delete" @click.stop="deleteNode(data)" /> </div> </div> </template> </el-tree> </el-scrollbar> </el-aside> <!-- 右侧编辑器 --> <el-main class="editor-container"> <div v-if="selectedFile" class="editor-header"> <h3>{{ selectedFile.name }}</h3> <div class="editor-actions"> <el-button size="small" @click="previewMode = !previewMode"> {{ previewMode ? '编辑' : '预览' }} </el-button> </div> </div> <div v-else class="empty-editor"> <el-empty description="请选择一个文件进行编辑" /> </div> <div class="editor-wrapper"> <MdEditor v-model="mdContent" :preview="previewMode" previewTheme="default" theme="light" @on-save="saveCurrentFile" /> </div> </el-main> </el-container> <!-- 右键菜单 --> <el-popover ref="contextMenuRef" placement="bottom-start" trigger="manual" :visible="contextMenuVisible" :virtual-ref="contextMenuTarget" virtual-triggering> <div class="context-menu"> <el-button v-if="contextMenuData && contextMenuData.is_dir" type="text" size="small" @click="handleCreateNewFileInFolder(contextMenuData)"> <el-icon> <DocumentAdd /> </el-icon> 新建文件 </el-button> <el-button v-if="contextMenuData" type="text" size="small" @click="renameFile(contextMenuData)"> <el-icon> <Edit /> </el-icon> 重命名 </el-button> <el-button v-if="contextMenuData" type="text" size="small" @click="deleteNode(contextMenuData)"> <el-icon> <Delete /> </el-icon> 删除 </el-button> </div> </el-popover> <!-- 文件/文件夹重命名对话框 --> <el-dialog v-model="renameDialogVisible" title="重命名" width="400px"> <el-input v-model="renameValue" placeholder="请输入新名称" @keyup.enter="confirmRename" /> <template #footer> <span class="dialog-footer"> <el-button @click="renameDialogVisible = false">取消</el-button> <el-button type="primary" @click="confirmRename">确定</el-button> </span> </template> </el-dialog> </div> </template> <script setup> import { ref, reactive, onMounted, nextTick } from 'vue' import { ElMessage, ElMessageBox, ElTree } from 'element-plus' import { Folder, Document, FolderAdd, DocumentAdd, Edit, Delete, Refresh } from '@element-plus/icons-vue' import { MdEditor } from 'md-editor-v3'; import 'md-editor-v3/lib/style.css'; import { kbnote_addfile, kbnote_filetree, kbnote_getfile, kbnote_updatefile } from '@/api/kbnote'; // 响应式数据 const treeRef = ref() const contextMenuRef = ref() const contextMenuVisible = ref(false) const contextMenuTarget = ref(null) const contextMenuData = ref(null) const renameDialogVisible = ref(false) const renameValue = ref('') const selectedFileId = ref(null) const selectedFile = ref(null) const previewMode = ref(false) const darkMode = ref(false) const mdContent = ref('') // 文件树数据 const fileTreeData = ref([]) // 树节点属性配置 const treeProps = { children: 'children', label: 'name' } // 允许拖拽的节点 const allowDrag = (node) => { return node.data.id !== 'root' } // 允许放置的节点 const allowDrop = (draggingNode, dropNode, type) => { // 不允许将节点拖拽到根节点下 if (dropNode.data.id === 'root' && type === 'inner') { return !draggingNode.data.is_dir } // 只允许在文件夹内放置 if (type === 'inner') { return dropNode.data.is_dir } return true } const handleRefreshFileTree = () => { kbnote_filetree().then((res) => { console.log(res) fileTreeData.value = res.data.filetree const rootNode = { id: 'root', name: '根目录', path: '/', is_dir: true, children: fileTreeData.value } fileTreeData.value = [rootNode] }).catch((err) => { console.error('Error loading file tree:', err) }) } // 处理树节点点击 const handleTreeNodeClick = (data) => { if (!data.is_dir) { selectedFileId.value = data.id selectedFile.value = data // 重置预览模式 previewMode.value = false console.log(data) kbnote_getfile({ path: data.path }).then((res) => { mdContent.value = res.data.body console.log(res) }).catch((err) => { console.error('Error loading file:', err) }) } } // 处理右键点击 const handleRightClick = (event, data, node, e) => { event.preventDefault() contextMenuData.value = data contextMenuTarget.value = e.target contextMenuVisible.value = true } // 保存当前文件 const saveCurrentFile = () => { if (selectedFile.value) { kbnote_updatefile({ path: selectedFile.value.path, body: mdContent.value }).then((res) => { console.log(res) ElMessage.success('文件已保存') }).catch((err) => { console.error('Error saving file:', err) }) } else { ElMessage.warning('请先选择一个文件') } } // 创建新文件 const handleCreateNewFile = () => { kbnote_addfile({ path: '', filename: 'file' + Date.now() + '.md', body: '# 新文件\n\n开始编辑...' }).then((res) => { console.log(res) ElMessage.success('文件创建成功') }).catch((err) => { console.error('Error creating file:', err) }) } // 在指定文件夹内创建新文件 const handleCreateNewFileInFolder = (folderData) => { const newFile = { id: `file${Date.now()}`, name: '新文件.md', path: `${folderData.path}/file${Date.now()}.md`, is_dir: false, content: '# 新文件\n\n开始编辑...' } // 选中新建的文件 selectedFileId.value = newFile.id selectedFile.value = newFile previewMode.value = false ElMessage.success('文件创建成功') contextMenuVisible.value = false } // 创建新文件夹 const handlecreateNewFolder = () => { const newFolder = { id: `folder${Date.now()}`, name: '新文件夹', path: `/new/path/folder${Date.now()}`, is_dir: true, children: [] } // 添加到根目录 const rootNode = fileTreeData.value[0] if (!rootNode.children) { rootNode.children = [] } rootNode.children.push(newFolder) ElMessage.success('文件夹创建成功') } // 重命名文件/文件夹 const renameFile = (data) => { if (!data) return renameValue.value = data.name contextMenuVisible.value = false renameDialogVisible.value = true // 临时存储当前操作的数据 contextMenuData.value = data } // 确认重命名 const confirmRename = () => { if (!renameValue.value.trim()) { ElMessage.error('名称不能为空') return } // 找到对应的节点并更新 const updateNodeLabel = (nodes) => { for (let node of nodes) { if (node.id === contextMenuData.value.id) { node.name = renameValue.value break } if (node.children) { updateNodeLabel(node.children) } } } updateNodeLabel(fileTreeData.value) renameDialogVisible.value = false ElMessage.success('重命名成功') } // 删除节点 const deleteNode = async (data) => { if (!data) return try { await ElMessageBox.confirm( `确认删除 "${data.name}" 吗?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' } ) // 找到父节点并删除 const deleteFromParent = (nodes, targetId) => { for (let i = 0; i < nodes.length; i++) { if (nodes[i].id === targetId) { nodes.splice(i, 1) return true } if (nodes[i].children) { if (deleteFromParent(nodes[i].children, targetId)) { return true } } } return false } deleteFromParent(fileTreeData.value, data.id) // 如果删除的是当前选中的文件,清空编辑器 if (selectedFileId.value === data.id) { selectedFileId.value = null selectedFile.value = null } contextMenuVisible.value = false ElMessage.success('删除成功') } catch { // 用户取消删除 } } // 页面加载时的初始化 onMounted(() => { handleRefreshFileTree() }) </script> <style scoped> .markdown-editor-container { height: calc(100vh - 60px); display: flex; flex-direction: column; background-color: #f5f7fa; } .top-toolbar { display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background: white; border-bottom: 1px solid #e4e7ed; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); z-index: 100; } .top-toolbar h1 { margin: 0; color: #303133; } .toolbar-actions { display: flex; gap: 10px; } .main-layout { flex: 1; overflow: hidden; } .file-tree-container { background: white; border-right: 1px solid #e4e7ed; display: flex; flex-direction: column; } .tree-header { padding: 2px; border-bottom: 1px solid #e4e7ed; display: flex; justify-content: space-between; align-items: center; font-weight: bold; color: #606266; } .editor-container { padding: 0; display: flex; flex-direction: column; } .editor-header { padding: 15px 20px; border-bottom: 1px solid #e4e7ed; display: flex; justify-content: space-between; align-items: center; background: white; } .editor-header h3 { margin: 0; color: #303133; } .editor-actions { display: flex; gap: 10px; } .editor-wrapper { flex: 1; overflow: hidden; } .empty-editor { display: flex; align-items: center; justify-content: center; height: 100%; background: white; } .tree-node-content { flex: 1; display: flex; align-items: center; font-size: 14px; padding-right: 8px; } .node-icon { margin-right: 8px; color: #409eff; } .node-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .node-actions { display: none; margin-left: 8px; } :deep(.el-tree-node__content:hover) .node-actions { display: flex; } .context-menu { display: flex; flex-direction: column; gap: 4px; } .context-menu .el-button { justify-content: flex-start; padding: 8px 12px; } .context-menu .el-button .el-icon { margin-right: 8px; } :deep(.md-editor) { height: calc(100vh - 10px); } :deep(.el-tree) { background: transparent; } :deep(.el-tree-node.is-current > .el-tree-node__content) { background-color: #f0f9ff; color: #409eff; } </style>
最新发布
11-24
你希望在 **添加文件时弹出一个输入框,让用户输入新文件名**,而不是直接使用默认的 `file${Date.now()}.md`。当前代码中点击“新建文件”按钮或右键菜单中的“新建文件”时,并没有让用户输入文件名。 我们需要做以下修改: 1. 创建一个新的对话框(`el-dialog`),用于输入新文件名。 2. 修改 `handleCreateNewFile` 和 `handleCreateNewFileInFolder` 方法,在创建文件前弹出输入框。 3. 支持用户输入 `.md` 文件名,带校验(如不能为空、必须以 `.md` 结尾等)。 --- ### ✅ 修改后的完整解决方案如下: #### 1. 在模板中新增「新建文件对话框」 ```html <!-- 新建文件对话框 --> <el-dialog v-model="newFileDialogVisible" title="新建文件" width="400px"> <el-form :model="newFileForm" :rules="newFileRules" ref="newFileFormRef"> <el-form-item label="文件名" prop="filename"> <el-input v-model="newFileForm.filename" placeholder="请输入文件名,例如:笔记.md" @keyup.enter="confirmCreateFile" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="newFileDialogVisible = false">取消</el-button> <el-button type="primary" @click="confirmCreateFile">确定</el-button> </span> </template> </el-dialog> ``` #### 2. 在 `<script setup>` 中添加响应式数据和方法 ```js // 新建文件对话框控制 const newFileDialogVisible = ref(false) const newFileForm = ref({ filename: '' }) const newFileFormRef = ref(null) // 校验规则 const newFileRules = { filename: [ { required: true, message: '文件名不能为空', trigger: 'blur' }, { pattern: /^.+\.md$/, message: '文件名必须以 .md 结尾', trigger: 'blur' } ] } // 当前要创建文件的目标父目录(用于区分根目录 or 子文件夹) let pendingParentPath = ref('/') ``` #### 3. 修改两个创建文件的方法:打开输入框而非直接创建 ##### 替换原 `handleCreateNewFile` 方法: ```js const handleCreateNewFile = () => { pendingParentPath.value = '/' // 根目录 newFileForm.value.filename = `未命名-${Date.now()}.md` newFileDialogVisible.value = true } ``` ##### 替换原 `handleCreateNewFileInFolder` 方法: ```js const handleCreateNewFileInFolder = (folderData) => { pendingParentPath.value = folderData.path // 指定父路径 newFileForm.value.filename = `未命名-${Date.now()}.md` newFileDialogVisible.value = true } ``` #### 4. 添加确认创建文件的方法 ```js // 确认创建文件 const confirmCreateFile = () => { newFileFormRef.value.validate((valid) => { if (!valid) { ElMessage.error('请正确填写文件名') return } const filename = newFileForm.value.filename.trim() const filepath = `${pendingParentPath.value}/${filename}`.replace(/\/+/g, '/') // 调用 API 创建文件 kbnote_addfile({ path: filepath, filename: filename, body: '# 新文件\n\n开始编辑...' }) .then((res) => { ElMessage.success('文件创建成功') // 刷新文件树 handleRefreshFileTree() // 自动选中并打开新文件?可选功能 // 这里需要后端返回 fileId 或支持通过 path 查找,否则前端无法立即定位 }) .catch((err) => { console.error('Error creating file:', err) ElMessage.error('创建失败,请重试') }) .finally(() => { newFileDialogVisible.value = false }) }) } ``` --- #### 5. 最后,在合适位置加入 `newFileFormRef` 的引用(确保表单可用) 确保你在 `<script setup>` 开头有导入 `ref` 和 `nextTick`,并且已经用了 `ElForm`, `ElFormItem` 组件Element Plus 默认支持)。 --- ### ✅ 效果说明 现在当你: - 点击顶部“新建文件”按钮 → 弹出输入框,提示输入文件名 - 右键文件夹 → “新建文件” → 同样弹出输入框,且自动关联到该文件夹路径下 用户可以自定义名称,比如:`我的笔记.md`,防止生成一堆看不懂的 `file123.md` --- ### 🧩 补充建议(可选优化) - 如果后端不返回新文件 ID,建议调用 `handleRefreshFileTree()` 来刷新树结构,确保能看到新文件。 - 可扩展为支持选择是否创建 `.md` 还是其他类型(但目前只做 Markdown)。 - 可增加防重名校验(检查同级目录是否有重名文件)。 --- ### ✅ 完整插入位置示意(片段) 将下面这段 `<el-dialog>` 插入到模板中 `</el-popover>` 之后: ```html <!-- 新建文件对话框 --> <el-dialog v-model="newFileDialogVisible" title="新建文件" width="400px"> <el-form :model="newFileForm" :rules="newFileRules" ref="newFileFormRef"> <el-form-item label="文件名" prop="filename"> <el-input v-model="newFileForm.filename" placeholder="请输入文件名,例如:笔记.md" @keyup.enter="confirmCreateFile" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="newFileDialogVisible = false">取消</el-button> <el-button type="primary" @click="confirmCreateFile">确定</el-button> </span> </template> </el-dialog> ``` 并在 `<script setup>` 中添加对应的数据与方法。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值