Vue3编写cmd命令补全

需求预览

01.png

2.png

03.png

04.png

需求,做一个类似CMD命令提示工具

1、有命令提示; 2、有层级关系 ; 3、以 / 开头,空格提示下一步;

分析

(一)用户输入进行调用方法实现命令提示;Ele-Plus的Autocomplete组件方法无法以Tree的方式进行补齐.
    ---> 不以 / 开头 
    ---> 仅 / 
    ---> /a 显示包含 /a 所有 
    ---> /add和提示的一致,则不提示 
    ---> /add空格,提示下一步;
(二)创建Tree结构
     S--> root 
        A---> /a 
            B-----> 120 
                C-------> 111 
                C-------> 222 
        A---> /b 
            B-----> 123 
                C-------> 111 
            B-----> 741 
                C-------> 111
 (三)递归匹配树 分两个方法,来调用是否存在空格进行匹配
     matchPathWithFullPath()
     handlePathWithSpace()
 (**)核心是构建树结构,通过递归去遍历匹配树的节点和子节点

代码

1.1 定义clas类,基于OOP

   class InputNode {
      private children: { [key: string]: InputNode } = {} // 存储子节点的字典
      private value: string

      constructor(value: string) {
        this.value = value
      }

      // 添加子节点
      addChild(key: string, child: InputNode) {
        this.children[key] = child
      }

      // 获取子节点
      getChild(key: string): InputNode | undefined {
        return this.children[key]
      }

      // 获取当前节点的值
      getValue(): string {
        return this.value
      }

      // 获取所有子节点
      getChildren(): { [key: string]: InputNode } {
        return this.children
      }
    }
    

1.2 解析路径并构建树结构

function initCmd(path: string, root: InputNode) {
  const parts = path.split(' ').filter(Boolean) // 按空格分割路径并过滤空字符串
  let currentNode = root

  // 遍历路径部分,逐层构建树
  for (const part of parts) {
    if (!currentNode.getChild(part)) {
      const newNode = new InputNode(part)
      currentNode.addChild(part, newNode)
    }
    currentNode = currentNode.getChild(part)! // 移动到下一个节点
  }
}

1.3 递归检查路径与树中节点的匹配关系,并返回包含路径的完整路径

function matchPathWithFullPath(path: string, node: InputNode, currentPath: string = ''): string[] {
  const pathParts = path.split(' ').filter(Boolean) // 拆分路径
  let results: string[] = []

  // 当前路径部分
  const currentPart = pathParts[0]
  const remainingPath = pathParts.slice(1).join(' ') // 剩余部分路径

  // 遍历当前节点的子节点,进行递归匹配
  for (const key in node.getChildren()) {
    const childNode = node.getChild(key)

    if (childNode && childNode.getValue().includes(currentPart)) {
      // 如果匹配到当前节点的值包含路径部分,则继续递归
      const newPath = currentPath + childNode.getValue()
      if (remainingPath) {
        const childMatches = matchPathWithFullPath(remainingPath, childNode, newPath + ' ')
        results = results.concat(childMatches) // 合并匹配结果
      } else {
        // 如果路径已经完全匹配到当前节点,返回该节点的完整路径
        results.push(newPath)
      }
    }
  }

  return results
}

1.4 处理路径输入,如果输入路径以空格结尾,提示下一层子节点

function handlePathWithSpace(path: string, root: InputNode): string[] {
  const trimmedPath = path.trim() // 去掉多余的空格
  const results: string[] = []

  // 查找路径的最后一个部分
  const pathParts = trimmedPath.split(' ').filter(Boolean)
  let currentNode: InputNode | undefined = root

  // 找到路径中的最后一个节点
  for (const part of pathParts) {
    currentNode = currentNode?.getChild(part)
  }

  if (currentNode) {
    // 如果节点存在,获取它的所有子节点,并返回它们的完整路径
    for (const key in currentNode.getChildren()) {
      const childNode = currentNode.getChild(key)
      if (childNode) {
        results.push(trimmedPath + ' ' + childNode.getValue())
      }
    }
  }

  return results
}

1.5 前端部分

Template

<template>
  <div class="user-input-container">
    <el-input
      type="text"
      class="user-input"
      v-model="cmdCode"
      @keydown="handleKeyDown"
      placeholder="请以 / 开头"
    />
    <ul v-if="tempResult.length" class="suggestions-list">
      <li
        v-for="(item, index) in tempResult"
        :key="index"
        class="suggestion-item"
        :class="{ 'is-selected': index === activeIndex }"
        @click="selectItem(item)"
      >
        {{ item }}
      </li>
    </ul>
  </div>
</template>

Script setup lang-ts

// 创建根节点
let root = new InputNode('root')

// 使用命令初始化树结构
initCmd('/add1 105 330', root)
initCmd('/add1 105 430', root)
initCmd('/acc2 233 530', root)
initCmd('/dcc2 133 930', root)

const cmdCode = ref('')
const info = ref<string[]>([])
const activeIndex = ref(0) // 新增一个响应式变量用于跟踪当前选中的索引
let tempResult = ref<string[]>([])

监听输入框,可以自行加一个防抖
watch(
  () => cmdCode.value,
  (newValue) => {
    if (!newValue) {
      info.value = [] // 清空info数组
      tempResult.value = [] // 清空tempResult数组
      return
    }
    if (!newValue.startsWith('/')) {
      info.value = [] // 清空info数组
      tempResult.value = [] // 清空tempResult数组
      failInfo('请以 / 开头')
      return
    }
    const parts = newValue.split(' ')
    switch (parts.length) {
      case 1:
        // 不带空格的输入
        tempResult.value = []
        info.value = matchPathWithFullPath(newValue, root)
        if (info.value.length) {
          info.value.filter((one) => {
            if (one != newValue) {
              tempResult.value.push(one)
            }
          })
        }
        break
      case 2:
        tempResult.value = []
        // 带一个空格,空格后面没有值或有值
        if (parts[1] === '') {
          info.value = handlePathWithSpace(newValue, root)
        } else {
          info.value = matchPathWithFullPath(newValue, root)
        }

        if (info.value.length) {
          info.value.forEach((one) => {
            if (one !== newValue) {
              let temp = one.split(' ')
              tempResult.value.push(temp[1])
            }
          })
        }

        break
      case 3:
        tempResult.value = []
        // 带两个空格,第二个空格后面没有值或有值
        if (parts[2] === '') {
          info.value = handlePathWithSpace(newValue, root)
        } else {
          info.value = matchPathWithFullPath(newValue, root)
        }
        if (info.value.length) {
          info.value.forEach((one) => {
            if (one !== newValue) {
              let temp = one.split(' ')
              if (temp.length > 2) {
                // 确保有足够的分割部分
                tempResult.value.push(temp[2])
              }
            }
          })
        }
        break
      default:
        console.log('Invalid input or more complex path structure')
    }
    activeIndex.value = 0 // 每次输入时重置活动索引
  }
)

// 处理键盘按下事件
const handleKeyDown = (event: KeyboardEvent) => {
  if (event.key === 'ArrowDown') {
    navigateDown()
    event.preventDefault() // 阻止默认行为
  } else if (event.key === 'ArrowUp') {
    navigateUp()
    event.preventDefault()
  } else if (event.key === 'Enter') {
    selectItem()
    event.preventDefault()
  } else if (event.key === 'Tab') {
    selectItem()
    event.preventDefault()
  }
}

// 导航到下一项
const navigateDown = () => {
  if (info.value.length && activeIndex.value < info.value.length - 1) {
    activeIndex.value++
  }
}

// 导航到上一项
const navigateUp = () => {
  if (activeIndex.value > -1) {
    activeIndex.value--
  }
}

// 选择当前项
const selectItem = (value?: string) => {
  if (value) {
    if (value == '/add1') {
      cmdCode.value = value
    } else {
      cmdCode.value += value
    }
  } else {
    if (info.value.length && activeIndex.value !== -1) {
      cmdCode.value = info.value[activeIndex.value]
      // info.value = [] // 选择后清空下拉列表
    }
  }
}
</script>

style

.suggestions-list {
  background-color: #fff;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  z-index: 1000;
  max-height: 200px;
  overflow-y: auto;
}

.suggestion-item {
  margin-left: 0;
  padding: 10px;
  cursor: pointer;
  transition: background-color 0.3s ease;
  margin-left: -40px;
}

.suggestion-item:hover,
.suggestion-item:focus {
  background-color: #f0f0f0;
}

/* 为选中的列表项添加特殊样式 */
.suggestion-item.is-selected {
  background-color: #e0e0e0;
}

这个方法可能不是最优,可以作为参考,不想造的工具告诉我,我来帮你造,一起加油新时代的打工人!⛽