|  | @@ -0,0 +1,165 @@
 | 
	
		
			
				|  |  | +<script lang="ts" setup>
 | 
	
		
			
				|  |  | +import { useRouter } from "vue-router";
 | 
	
		
			
				|  |  | +import SearchResult from "./SearchResult.vue";
 | 
	
		
			
				|  |  | +import SearchFooter from "./SearchFooter.vue";
 | 
	
		
			
				|  |  | +import { deleteChildren } from "/@/utils/tree";
 | 
	
		
			
				|  |  | +import { transformI18n } from "/@/plugins/i18n";
 | 
	
		
			
				|  |  | +import { useDebounceFn, onKeyStroke } from "@vueuse/core";
 | 
	
		
			
				|  |  | +import { ref, watch, computed, nextTick, shallowRef } from "vue";
 | 
	
		
			
				|  |  | +import { usePermissionStoreHook } from "/@/store/modules/permission";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +interface Props {
 | 
	
		
			
				|  |  | +  /** 弹窗显隐 */
 | 
	
		
			
				|  |  | +  value: boolean;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +interface Emits {
 | 
	
		
			
				|  |  | +  (e: "update:value", val: boolean): void;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const emit = defineEmits<Emits>();
 | 
	
		
			
				|  |  | +const props = withDefaults(defineProps<Props>(), {});
 | 
	
		
			
				|  |  | +const router = useRouter();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const keyword = ref("");
 | 
	
		
			
				|  |  | +const activePath = ref("");
 | 
	
		
			
				|  |  | +const inputRef = ref<HTMLInputElement | null>(null);
 | 
	
		
			
				|  |  | +const resultOptions = shallowRef([]);
 | 
	
		
			
				|  |  | +const handleSearch = useDebounceFn(search, 300);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** 菜单树形结构 */
 | 
	
		
			
				|  |  | +const menusData = computed(() => {
 | 
	
		
			
				|  |  | +  return deleteChildren(usePermissionStoreHook().menusTree);
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const show = computed({
 | 
	
		
			
				|  |  | +  get() {
 | 
	
		
			
				|  |  | +    return props.value;
 | 
	
		
			
				|  |  | +  },
 | 
	
		
			
				|  |  | +  set(val: boolean) {
 | 
	
		
			
				|  |  | +    emit("update:value", val);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +watch(show, async val => {
 | 
	
		
			
				|  |  | +  if (val) {
 | 
	
		
			
				|  |  | +    /** 自动聚焦 */
 | 
	
		
			
				|  |  | +    await nextTick();
 | 
	
		
			
				|  |  | +    inputRef.value?.focus();
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +});
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** 将菜单树形结构扁平化为一维数组,用于菜单查询 */
 | 
	
		
			
				|  |  | +function flatTree(arr) {
 | 
	
		
			
				|  |  | +  const res = [];
 | 
	
		
			
				|  |  | +  function deep(arr) {
 | 
	
		
			
				|  |  | +    arr.forEach(item => {
 | 
	
		
			
				|  |  | +      res.push(item);
 | 
	
		
			
				|  |  | +      item.children && deep(item.children);
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  deep(arr);
 | 
	
		
			
				|  |  | +  return res;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** 查询 */
 | 
	
		
			
				|  |  | +function search() {
 | 
	
		
			
				|  |  | +  const flatMenusData = flatTree(menusData.value);
 | 
	
		
			
				|  |  | +  resultOptions.value = flatMenusData.filter(
 | 
	
		
			
				|  |  | +    menu =>
 | 
	
		
			
				|  |  | +      keyword.value &&
 | 
	
		
			
				|  |  | +      transformI18n(menu.meta?.title, menu.meta?.i18n)
 | 
	
		
			
				|  |  | +        .toLocaleLowerCase()
 | 
	
		
			
				|  |  | +        .includes(keyword.value.toLocaleLowerCase().trim())
 | 
	
		
			
				|  |  | +  );
 | 
	
		
			
				|  |  | +  if (resultOptions.value?.length > 0) {
 | 
	
		
			
				|  |  | +    activePath.value = resultOptions.value[0].path;
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    activePath.value = "";
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +function handleClose() {
 | 
	
		
			
				|  |  | +  show.value = false;
 | 
	
		
			
				|  |  | +  /** 延时处理防止用户看到某些操作 */
 | 
	
		
			
				|  |  | +  setTimeout(() => {
 | 
	
		
			
				|  |  | +    resultOptions.value = [];
 | 
	
		
			
				|  |  | +    keyword.value = "";
 | 
	
		
			
				|  |  | +  }, 200);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** key up */
 | 
	
		
			
				|  |  | +function handleUp() {
 | 
	
		
			
				|  |  | +  const { length } = resultOptions.value;
 | 
	
		
			
				|  |  | +  if (length === 0) return;
 | 
	
		
			
				|  |  | +  const index = resultOptions.value.findIndex(
 | 
	
		
			
				|  |  | +    item => item.path === activePath.value
 | 
	
		
			
				|  |  | +  );
 | 
	
		
			
				|  |  | +  if (index === 0) {
 | 
	
		
			
				|  |  | +    activePath.value = resultOptions.value[length - 1].path;
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    activePath.value = resultOptions.value[index - 1].path;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** key down */
 | 
	
		
			
				|  |  | +function handleDown() {
 | 
	
		
			
				|  |  | +  const { length } = resultOptions.value;
 | 
	
		
			
				|  |  | +  if (length === 0) return;
 | 
	
		
			
				|  |  | +  const index = resultOptions.value.findIndex(
 | 
	
		
			
				|  |  | +    item => item.path === activePath.value
 | 
	
		
			
				|  |  | +  );
 | 
	
		
			
				|  |  | +  if (index + 1 === length) {
 | 
	
		
			
				|  |  | +    activePath.value = resultOptions.value[0].path;
 | 
	
		
			
				|  |  | +  } else {
 | 
	
		
			
				|  |  | +    activePath.value = resultOptions.value[index + 1].path;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** key enter */
 | 
	
		
			
				|  |  | +function handleEnter() {
 | 
	
		
			
				|  |  | +  const { length } = resultOptions.value;
 | 
	
		
			
				|  |  | +  if (length === 0 || activePath.value === "") return;
 | 
	
		
			
				|  |  | +  router.push(activePath.value);
 | 
	
		
			
				|  |  | +  handleClose();
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +onKeyStroke("Enter", handleEnter);
 | 
	
		
			
				|  |  | +onKeyStroke("ArrowUp", handleUp);
 | 
	
		
			
				|  |  | +onKeyStroke("ArrowDown", handleDown);
 | 
	
		
			
				|  |  | +</script>
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +<template>
 | 
	
		
			
				|  |  | +  <el-dialog top="5vh" v-model="show" :before-close="handleClose">
 | 
	
		
			
				|  |  | +    <el-input
 | 
	
		
			
				|  |  | +      ref="inputRef"
 | 
	
		
			
				|  |  | +      v-model="keyword"
 | 
	
		
			
				|  |  | +      clearable
 | 
	
		
			
				|  |  | +      placeholder="请输入关键词搜索"
 | 
	
		
			
				|  |  | +      @input="handleSearch"
 | 
	
		
			
				|  |  | +    >
 | 
	
		
			
				|  |  | +      <template #prefix>
 | 
	
		
			
				|  |  | +        <el-icon class="el-input__icon">
 | 
	
		
			
				|  |  | +          <IconifyIconOffline icon="search" />
 | 
	
		
			
				|  |  | +        </el-icon>
 | 
	
		
			
				|  |  | +      </template>
 | 
	
		
			
				|  |  | +    </el-input>
 | 
	
		
			
				|  |  | +    <div class="search-result-container">
 | 
	
		
			
				|  |  | +      <el-empty v-if="resultOptions.length === 0" description="暂无搜索结果" />
 | 
	
		
			
				|  |  | +      <SearchResult
 | 
	
		
			
				|  |  | +        v-else
 | 
	
		
			
				|  |  | +        v-model:value="activePath"
 | 
	
		
			
				|  |  | +        :options="resultOptions"
 | 
	
		
			
				|  |  | +        @click="handleEnter"
 | 
	
		
			
				|  |  | +      />
 | 
	
		
			
				|  |  | +    </div>
 | 
	
		
			
				|  |  | +    <template #footer>
 | 
	
		
			
				|  |  | +      <SearchFooter />
 | 
	
		
			
				|  |  | +    </template>
 | 
	
		
			
				|  |  | +  </el-dialog>
 | 
	
		
			
				|  |  | +</template>
 | 
	
		
			
				|  |  | +<style lang="scss" scoped>
 | 
	
		
			
				|  |  | +.search-result-container {
 | 
	
		
			
				|  |  | +  margin-top: 20px;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +</style>
 |